feat: retry 3x + warmup health check for upload (Phase 2), bump 1.0.5→1.0.6
Deploy loadtest / validate (push) Waiting to run

This commit is contained in:
2026-07-10 21:19:31 +04:00
parent d303985877
commit 6ce7bd9720
7 changed files with 328 additions and 2 deletions
+11
View File
@@ -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
+97
View File
@@ -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 не рвёт.
+22
View File
@@ -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`
+57
View File
@@ -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
View File
@@ -4,7 +4,7 @@ from flask import Flask, render_template, request
app = Flask(__name__) app = Flask(__name__)
VERSION = '1.0.5' VERSION = '1.0.6'
@app.route('/') @app.route('/')
+43 -1
View File
@@ -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() — последовательная загрузка всех файлов */ /** uploadAll() — последовательная загрузка всех файлов */
async function uploadAll() { async function uploadAll() {
if (uploading || files.length === 0) return; if (uploading || files.length === 0) return;
@@ -137,6 +150,9 @@ async function uploadAll() {
uploadBtn.disabled = true; uploadBtn.disabled = true;
clearBtn.disabled = true; clearBtn.disabled = true;
// Прогрев соединения
await warmup();
var totalBytes = 0; var totalBytes = 0;
var totalElapsed = 0; var totalElapsed = 0;
var okCount = 0; var okCount = 0;
@@ -151,7 +167,7 @@ async function uploadAll() {
renderTable(); renderTable();
try { try {
var result = await uploadOne(f, i); var result = await uploadOneWithRetry(f, i);
f.status = 'ok'; f.status = 'ok';
f.elapsed = result.elapsed_ms; f.elapsed = result.elapsed_ms;
f.speed = result.elapsed_ms > 0 ? Math.round(result.bytes / (result.elapsed_ms / 1000)) : null; 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; 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, отправляет как поле формы */ /** uploadOne(fileEntry, idx) — читает файл в base64, отправляет как поле формы */
function uploadOne(f, idx) { function uploadOne(f, idx) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {