feat: retry 3x + warmup health check for upload (Phase 2), bump 1.0.5→1.0.6
Deploy loadtest / validate (push) Waiting to run
Deploy loadtest / validate (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# 2026-07-10 — Fix: base64 как файл (multipart file-part)
|
||||
|
||||
## План
|
||||
Обойти `SecRequestBodyNoFilesLimit = 128KB` — отправлять base64 как файловую часть multipart (с filename), а не как поле формы.
|
||||
|
||||
Файловые части идут под `SecRequestBodyLimit = 12.5MB`.
|
||||
|
||||
## Изменения
|
||||
1. `site/static/app.js` — `fd.append('data', ...)` → `fd.append('data', new Blob([b64]), name + '.b64')`
|
||||
2. `site/app.py` — добавить приём base64 из `request.files`, совместить с существующим file-режимом
|
||||
3. Bump версии 1.0.5 → 1.0.6
|
||||
@@ -0,0 +1,97 @@
|
||||
# 2026-07-10 — ИТОГ РАССЛЕДОВАНИЯ: почему режется загрузка
|
||||
|
||||
## Методология
|
||||
Тестирование от внутреннего к внешнему — послойная изоляция каждого компонента.
|
||||
|
||||
## Результаты тестов
|
||||
|
||||
| # | Тест | Маршрут | Размер | Результат |
|
||||
|---|------|---------|--------|-----------|
|
||||
| 1 | Python внутри пода → Flask | pod → localhost:5000 | 100KB base64 (136KB POST) | ✅ 102400 байт, 4ms |
|
||||
| 2 | Python внутри пода → Ingress HTTPS | pod → 10.105.13.158:443 | 136KB POST | ✅ 102400 байт, 2ms |
|
||||
| 3 | Python снаружи → Ingress | интернет → 185.247.187.151:443 | 136KB POST | ❌ **СЛУЧАЙНО** |
|
||||
| 4 | 10×100KB снаружи | интернет → 185.247.187.151:443 | 136KB POST | ~50% OK, ~50% RemoteDisconnected |
|
||||
|
||||
## Бинарный поиск предела (снаружи)
|
||||
|
||||
| Размер POST | Результат |
|
||||
|-------------|-----------|
|
||||
| 1-48 KB | ✅ всегда |
|
||||
| 50 KB | ❌ |
|
||||
| 52 KB | ✅ |
|
||||
| 55-80 KB | ❌ |
|
||||
| 100 KB | ✅ (иногда!) |
|
||||
|
||||
**Твёрдого предела НЕТ.** Сбои случайные.
|
||||
|
||||
## Исключённые причины
|
||||
|
||||
| Компонент | Статус | Доказательство |
|
||||
|-----------|--------|----------------|
|
||||
| Flask dev-сервер | ✅ OK | Тест #1 |
|
||||
| nginx ingress | ✅ OK | Тест #2 |
|
||||
| ModSecurity (128KB NoFilesLimit) | ✅ OK | Тест #2, DetectionOnly |
|
||||
| Gunicorn field_size | ❌ не используется | Shturval запускает `python app.py` |
|
||||
|
||||
## Где проблема
|
||||
|
||||
```
|
||||
Клиент (интернет)
|
||||
│
|
||||
▼
|
||||
??? Edge-инфраструктура хостера (NUBES LLC, AS212136) ???
|
||||
│ ← здесь рвётся TCP-соединение для POST > ~48KB
|
||||
▼
|
||||
185.247.187.151:443 (hostPort на ноде)
|
||||
│
|
||||
▼
|
||||
nginx ingress pod ─── ModSecurity ─── pythonk8s pod (Flask)
|
||||
✅ всё работает
|
||||
```
|
||||
|
||||
IP в логах ingress: `192.168.255.x` — приватный IP хостинг-провайдера.
|
||||
На ноде порт 443 слушает kube-vip/hostPort напрямую (без proxy_protocol).
|
||||
Внутри кластера всё идеально. Снаружи — случайные обрывы.
|
||||
|
||||
## Аналогичный опыт (contracts)
|
||||
`/home/naeel/nubes/contracts/History/`:
|
||||
- `connection-reset-analysis.md`: curl 100KB → HTTP 000 случайно (5 тестов: 2 OK, 3 обрыв)
|
||||
- `sonnet-upload-from-scratch.md`: "64KB — жёсткий предел платформы" (43 версии)
|
||||
- `session-08-upload-saga.md`: чанки 50KB — только первый доходит
|
||||
|
||||
## Ложные правки (эта сессия)
|
||||
- Dockerfile `--limit-request-field_size 0` — бесполезно (Gunicorn не используется)
|
||||
- Версия 1.0.4 → 1.0.5 — задеплоено, но проблемы не решает (причина вне кода)
|
||||
|
||||
## Компоненты инфраструктуры (найдены)
|
||||
- Shturval v2.12.1
|
||||
- kube-vip (DaemonSet) — VIP для LoadBalancer, IP 185.247.187.151
|
||||
- nginx ingress через hostPort (порты 80/443 прямо на ноде)
|
||||
- ModSecurity OWASP CRS 4.10.0, SecRuleEngine DetectionOnly
|
||||
- `SecRequestBodyNoFilesLimit 131072` (128KB) — НЕ причина (внутри кластера работает)
|
||||
|
||||
## РЕШЕНИЕ
|
||||
|
||||
Contracts на этой же ВМ работает потому, что использует **родной nginx** с `proxy_request_buffering off`:
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-enabled/nginx-contracts.conf
|
||||
location /upload {
|
||||
proxy_pass http://127.0.0.1:8766;
|
||||
client_max_body_size 100m;
|
||||
proxy_request_buffering off; # ← вот почему работает
|
||||
}
|
||||
```
|
||||
|
||||
Для loadtest добавлена аннотация в k8s ingress:
|
||||
```
|
||||
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
|
||||
```
|
||||
|
||||
Конфиг применился (подтверждено в `/etc/nginx/nginx.conf` внутри ingress pod):
|
||||
```
|
||||
proxy_request_buffering off;
|
||||
```
|
||||
|
||||
## Причина
|
||||
Edge-инфраструктура хостера (NUBES LLC) рвёт TCP-соединения при буферизации больших POST-тел. `proxy_request_buffering off` стримит тело напрямую в бэкенд — edge не рвёт.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-07-10 — План Соннета: HTTP/2 мультиплексирование
|
||||
|
||||
## Идея Соннета
|
||||
Edge LB дропает ~50% НОВЫХ TCP-соединений. HTTP/2 открывает ОДНО TLS-соединение и мультиплексирует все запросы через него. Edge LB видит одно долгоживущее соединение → дропать нечего.
|
||||
|
||||
## Phase 1 — ❌ ПРОВАЛ (2026-07-10)
|
||||
keep-alive 10→75 ухудшило: 5/10 → 2/10. Edge LB агрессивнее дропает долгие соединения.
|
||||
Откачено.
|
||||
|
||||
## Phase 2 — в работе
|
||||
Ретрай в app.js + warmup /health.
|
||||
|
||||
### Phase 3 — WebSocket (ядерный)
|
||||
- flask-sock, endpoint `/upload-ws`
|
||||
- Handshake GET (маленький), данные binary frames
|
||||
- Edge LB никогда не видит большой POST на новом TCP
|
||||
|
||||
## Файлы
|
||||
- nginx ingress ConfigMap
|
||||
- app.js (`uploadOne`, `uploadAll`)
|
||||
- app.py (версия)
|
||||
- requirements.txt (только Phase 3)
|
||||
@@ -0,0 +1,97 @@
|
||||
# 2026-07-10 — Полное расследование: почему режутся файлы при загрузке
|
||||
|
||||
## Симптом
|
||||
Загрузка файлов > 64KB через веб-интерфейс loadtest обрывается. Даже чанки не проходят.
|
||||
|
||||
## Диагностика
|
||||
|
||||
### Тесты
|
||||
| Тест | Результат |
|
||||
|------|-----------|
|
||||
| Изнутри пода (Flask, 100KB base64) | ✅ 102400 байт, 4ms |
|
||||
| Снаружи (curl, 136KB URL-encoded POST) | ❌ 408 Timeout / Connection reset |
|
||||
| Снаружи (Python urllib, 136KB POST) | ❌ RemoteDisconnected |
|
||||
| Маленький POST (12 байт) | ✅ |
|
||||
|
||||
### Инфраструктура
|
||||
```
|
||||
Клиент → [прокси/LB] → nginx ingress (ModSecurity) → Flask dev-сервер (не Gunicorn!)
|
||||
```
|
||||
|
||||
- Платформа: Shturval (managed Kubernetes)
|
||||
- Сервис: `loadtest.pythonk8s.dev.nubes.ru`, IP `185.247.187.151` (NUBES LLC)
|
||||
- Namespace: `9039a501-df2c-4b85-b049-9cd4f77e6ac0`
|
||||
- Deployment: `pythonk8s`
|
||||
- Запуск: `python app.py` (Flask dev-сервер, НЕ Gunicorn)
|
||||
|
||||
### ModSecurity в nginx ingress
|
||||
```
|
||||
SecRequestBodyLimit 13107200 # 12.5 MB — общий лимит
|
||||
SecRequestBodyNoFilesLimit 131072 # 128 KB — лимит для НЕ-файловых полей
|
||||
SecRuleEngine DetectionOnly # только логирует, не блокирует
|
||||
```
|
||||
Базовая строка 136KB отправляется как URL-encoded form → попадает под `NoFilesLimit`.
|
||||
|
||||
### Ingress-логи
|
||||
- Маленькие POST (146 байт) → 200 OK
|
||||
- Большие POST (49-65KB) → 408 Timeout через 10 секунд
|
||||
- `ingress_request_length` показывает 49-65KB при отправленных 136KB → тело обрезается ДО ingress (прокси/LB)
|
||||
|
||||
## Предыдущий опыт (contracts)
|
||||
Из `/home/naeel/nubes/contracts/History/`:
|
||||
|
||||
### `session-07-chunks.md` (v1.15-1.18)
|
||||
- Чанки 50KB FormData → только первый доходит
|
||||
- Чанки в PostgreSQL → то же самое
|
||||
- 7 отдельных `psycopg2.connect` на сборку → медленно → таймаут Ingress
|
||||
|
||||
### `session-08-upload-saga.md` (v1.12-1.26)
|
||||
- 26 версий до рабочей загрузки
|
||||
- FormData/multipart → ❌
|
||||
- base64 JSON → ❌
|
||||
- v1.26: `original_bytes DROP NOT NULL` + `original_b64 TEXT` → ✓ РАБОТАЛО
|
||||
- Потом регресс на v1.27+
|
||||
|
||||
### `upload-analysis-v40.md` (v1.40)
|
||||
- `request.get_data(as_text=True)` + raw regex (без JSON, без b64decode, без БД) → всё равно HTTP 000
|
||||
- Redis `rpush()` под подозрением
|
||||
- curl: 100KB — 2/5 HTTP 000, 3/5 HTTP 200 (случайно)
|
||||
|
||||
### `connection-reset-analysis.md` (v1.21)
|
||||
- HTTP 000 случайно, не зависит от размера
|
||||
- Health check (GET) всегда 200
|
||||
- Прямой /test (JSON → БД) всегда работает
|
||||
|
||||
### `sonnet-upload-from-scratch.md` — ИТОГ
|
||||
> **"64KB — жёсткий предел платформы"**
|
||||
> 43 версии. FormData, JSON/base64, чанки, Redis, retry — ничего не помогло.
|
||||
|
||||
## Корневая причина
|
||||
Платформа pythonk8s (Shturval) имеет **непреодолимый предел ~64KB** на тело POST-запроса.
|
||||
Это не баг приложения — это ограничение инфраструктуры:
|
||||
- ModSecurity `SecRequestBodyNoFilesLimit` = 128KB
|
||||
- Прокси/LB перед ingress обрезает тело ещё сильнее (до ~50-65KB)
|
||||
- Ingress таймаутит через 10 секунд
|
||||
|
||||
Ни base64, ни JSON, ни чанки не обходят этот предел.
|
||||
|
||||
## Ложная правка
|
||||
- `Dockerfile`: добавлены `--limit-request-field_size 0 --limit-request-body 0` — **бесполезно**, т.к. Gunicorn не используется
|
||||
- `site/app.py`: версия 1.0.4 → 1.0.5 — задеплоено, но проблему не решает
|
||||
|
||||
## Что дальше
|
||||
Для loadtest как инструмента замера скорости загрузки этот предел фатален.
|
||||
Нужно решать на уровне платформы:
|
||||
1. Увеличить `SecRequestBodyNoFilesLimit` в ModSecurity
|
||||
2. Убрать/настроить прокси/LB перед ingress
|
||||
3. Либо перенести сервис на другую платформу без этих ограничений
|
||||
|
||||
## Источники
|
||||
- `/home/naeel/nubes/contracts/History/sessions/session-07-chunks.md`
|
||||
- `/home/naeel/nubes/contracts/History/sessions/session-08-upload-saga.md`
|
||||
- `/home/naeel/nubes/contracts/History/features/upload-analysis-v40.md`
|
||||
- `/home/naeel/nubes/contracts/History/features/upload-analysis-v41.md`
|
||||
- `/home/naeel/nubes/contracts/History/features/connection-reset-analysis.md`
|
||||
- `/home/naeel/nubes/contracts/History/features/chunk-analysis-request.md`
|
||||
- `/home/naeel/nubes/contracts/History/llm-analysis/sonnet-upload-from-scratch.md`
|
||||
- `/home/naeel/nubes/contracts/History/llm-analysis/sonnet-appjs-analysis.md`
|
||||
@@ -0,0 +1,57 @@
|
||||
# Вопрос Sonnet: upload > 64KB падает на k8s-сервисе
|
||||
|
||||
## Суть
|
||||
Сервис loadtest (Flask, замер скорости загрузки файлов) на Shturval k8s v2.12.1.
|
||||
Файлы > ~48KB падают случайно при загрузке из интернета.
|
||||
|
||||
## Инфраструктура
|
||||
```
|
||||
Клиент (интернет)
|
||||
→ Edge LB хостера (NUBES LLC, 185.247.187.151)
|
||||
→ hostPort:443 на ноде k8s
|
||||
→ nginx ingress pod (Shturval, ModSecurity OWASP CRS 4.10 DetectionOnly)
|
||||
→ pythonk8s pod (Flask dev-сервер, python app.py)
|
||||
```
|
||||
|
||||
Ingress аннотации:
|
||||
- proxy-body-size: 1024m
|
||||
- proxy-connect-timeout: 120
|
||||
- proxy-read-timeout: 600
|
||||
- proxy-send-timeout: 600
|
||||
- proxy-request-buffering: off (добавили — помогло частично)
|
||||
|
||||
ModSecurity: SecRuleEngine DetectionOnly, SecRequestBodyNoFilesLimit 131072 (128KB)
|
||||
|
||||
## Тесты (100KB base64 → 136KB POST)
|
||||
|
||||
| Тест | Результат |
|
||||
|------|-----------|
|
||||
| Изнутри пода → Flask | ✅ 102400 байт, 4ms |
|
||||
| Изнутри кластера → ingress HTTPS | ✅ 102400 байт, 2ms |
|
||||
| Снаружи (новые TCP на каждый запрос) | ❌ ~50% Timeout |
|
||||
| Снаружи (HTTP/1.1 keep-alive, одно соединение) | ✅ 8/10 (первые 2 таймаут, остальные OK) |
|
||||
|
||||
## Что исключено
|
||||
- Flask — работает (тест изнутри)
|
||||
- nginx ingress — работает (тест изнутри)
|
||||
- ModSecurity — DetectionOnly, внутри работает
|
||||
- Gunicorn — не используется (Shturval запускает python app.py)
|
||||
- MTU — 1500, ping ок
|
||||
- Размер тела — нет жёсткого предела (иногда 100KB проходит, иногда 50KB нет)
|
||||
|
||||
## Вывод
|
||||
Edge LB хостера дропает ~50% НОВЫХ TCP-соединений. Установленное соединение (keep-alive) — стабильно.
|
||||
|
||||
## Что пробовали
|
||||
1. Dockerfile: --limit-request-field_size 0 — бесполезно (Gunicorn не используется)
|
||||
2. Ingress annotation: proxy-request-buffering off — улучшило с 3/10 до 5/10
|
||||
3. HTTP/1.1 keep-alive — 8/10
|
||||
|
||||
## Ограничения
|
||||
- Менять можно ТОЛЬКО внутри k8s (ingress, поды, деплойменты)
|
||||
- Edge LB хостера — не наш, не контролируем
|
||||
- Base64 должен остаться (требование)
|
||||
|
||||
## Вопрос
|
||||
Как сделать загрузку 100% надёжной? Идеи по обходу проблемы с edge LB?
|
||||
Может WebSocket? Может gRPC? Может HTTP/2 multiplexing? Может что-то на уровне ingress?
|
||||
+1
-1
@@ -4,7 +4,7 @@ from flask import Flask, render_template, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
VERSION = '1.0.5'
|
||||
VERSION = '1.0.6'
|
||||
|
||||
|
||||
@app.route('/')
|
||||
|
||||
+43
-1
@@ -130,6 +130,19 @@ function renderTable() {
|
||||
}
|
||||
}
|
||||
|
||||
/** warmup() — прогрев соединения перед загрузкой */
|
||||
function warmup() {
|
||||
return new Promise(function (resolve) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/health');
|
||||
xhr.timeout = 3000;
|
||||
xhr.onload = function () { resolve(true); };
|
||||
xhr.onerror = function () { resolve(false); };
|
||||
xhr.ontimeout = function () { resolve(false); };
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
/** uploadAll() — последовательная загрузка всех файлов */
|
||||
async function uploadAll() {
|
||||
if (uploading || files.length === 0) return;
|
||||
@@ -137,6 +150,9 @@ async function uploadAll() {
|
||||
uploadBtn.disabled = true;
|
||||
clearBtn.disabled = true;
|
||||
|
||||
// Прогрев соединения
|
||||
await warmup();
|
||||
|
||||
var totalBytes = 0;
|
||||
var totalElapsed = 0;
|
||||
var okCount = 0;
|
||||
@@ -151,7 +167,7 @@ async function uploadAll() {
|
||||
renderTable();
|
||||
|
||||
try {
|
||||
var result = await uploadOne(f, i);
|
||||
var result = await uploadOneWithRetry(f, i);
|
||||
f.status = 'ok';
|
||||
f.elapsed = result.elapsed_ms;
|
||||
f.speed = result.elapsed_ms > 0 ? Math.round(result.bytes / (result.elapsed_ms / 1000)) : null;
|
||||
@@ -180,6 +196,32 @@ async function uploadAll() {
|
||||
fileSummary.textContent = summary;
|
||||
}
|
||||
|
||||
/** uploadOneWithRetry(f, idx) — ретрай до 3 раз */
|
||||
function uploadOneWithRetry(f, idx) {
|
||||
var MAX_RETRIES = 3;
|
||||
var RETRY_DELAY = 500; // ms
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var attempt = 0;
|
||||
|
||||
function tryUpload() {
|
||||
attempt++;
|
||||
uploadOne(f, idx).then(resolve).catch(function (e) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
f.progress = 0;
|
||||
f.status = 'uploading';
|
||||
renderTable();
|
||||
setTimeout(tryUpload, RETRY_DELAY);
|
||||
} else {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tryUpload();
|
||||
});
|
||||
}
|
||||
|
||||
/** uploadOne(fileEntry, idx) — читает файл в base64, отправляет как поле формы */
|
||||
function uploadOne(f, idx) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
||||
Reference in New Issue
Block a user