diff --git a/History/session-2026-08-17-loadtest.md b/History/session-2026-08-17-loadtest.md new file mode 100644 index 0000000..09d88ca --- /dev/null +++ b/History/session-2026-08-17-loadtest.md @@ -0,0 +1,158 @@ +# Сессия 2026-08-17 — LoadTest (проверка загрузки больших файлов) + +_Файл: подробная запись содержимого чата. Дата: 2026-08-17._ +_Сервис: `loadtest.pythonk8s.dev.nubes.ru` (платформа Nubes, pythonk8s)._ +_Цель: проверить, доходят ли файлы (в т.ч. большие) до бэкенда или нет._ + +--- + +## 1. Задача (из чата) + +1. Создан пустой git-репозиторий `https://gitea.services.ngcloud.ru/sqs/loadtest.git`. +2. Склонировать его в `/home/naeel/nubes/contracts/loadtest`. +3. Сделать код по `howto-flask-nubes.md` для деплоя на Flask-платформу Nubes. +4. Функционал: меню выбора одного файла из локали + загрузка в память Flask. +5. Индикация прогресса загрузки — `%` и таймер (позже таймер признан неважным). +6. **Главное** — однозначный результат: файл загрузился ИЛИ нет, максимум статистики. +7. Код — простейший, с комментариями. + +## 2. Инстанс платформы (из параметров) + +- `instanceUid`: `bb9ed30b-1794-44ff-81a2-6ff2731200c9` +- `connectionUrl`: `https://loadtest.pythonk8s.dev.nubes.ru/` +- `resourceRealm`: `iot-naeel` +- `appConfiguration` изначально: `gitPath=https://github.com/Foxyhhd/Baldurs-Gate-test.git`, `version=3.12`, `healthPath=""` +- `clusterConfiguration`: cpu 500, memory 1024, replicas 1 +- Мониторинг: Grafana kubernetes-pods по namespace `bb9ed30b-…` + +**Для деплоя нужно сменить `gitPath` на `https://gitea.services.ngcloud.ru/sqs/loadtest.git`.** + +## 3. Что сделано + +### 3.1 Клонирование +- `git clone https://gitea.services.ngcloud.ru/sqs/loadtest.git .` — успешно, ветка `master`, коммитов не было. + +### 3.2 Структура (обязательна по howto-flask-nubes — без `site/` платформа не запустится) +``` +loadtest/ +├── .gitignore # __pycache__, *.pyc +├── requirements.txt # Flask>=3.0, gunicorn>=21.2, requests>=2.31 +└── site/ + ├── app.py # точка входа (python site/app.py) + ├── static/style.css + └── templates/index.html +``` +- НЕТ `site/__init__.py` (конфликт со stdlib `site.py`). +- В `app.py` обязателен `app.run(host="0.0.0.0", port=5000)`. + +### 3.3 Код +**`site/app.py`**: +- `GET /` — страница; `GET /health` — `ok`. +- `POST /upload` — принимает multipart-файл в поле `file`, читает **в память** (`f.read()`), на диск не пишет. +- `MAX_CONTENT_LENGTH` **не задан** — цель проверить реальные лимиты платформы/прокси, а не Flask. +- Ответ — максимум статистики: `file` (name, mime, size_bytes, size_mb), `server` (read_ms, total_ms, speed_mb_s), `md5`, `meta` (server_start, remote_addr, content_length_header, content_type_header, user_agent). + +**`site/templates/index.html`**: +- Выбор одного файла + кнопка «Загрузить». +- Загрузка через **`XMLHttpRequest`** + `xhr.upload.onprogress` — реальный `%` и скорость МБ/с (`fetch` не даёт upload progress). +- Минимальный таймер (секунды), timeout XHR = 10 мин. +- Исход всегда однозначный: ✅ «ФАЙЛ ЗАГРУЖЕН» / ❌ «НЕ ЗАГРУЖЕН (HTTP …)» / «СЕТЕВАЯ ОШИБКА» / «ТАЙМАУТ (10 мин)». +- При ошибке показывается HTTP-статус и текст ответа (в т.ч. не-JSON: 413, HTML ошибок прокси). +- Статистика: клиент (имя, размер, время старта, длительность, средняя скорость) + сервер (принято байт, чтение мс, обработка мс, скорость, MD5, заголовки, remote addr, время сервера, User-Agent). + +### 3.4 Локальный тест (curl, 127.0.0.1:5000) +| Файл | Результат | Примечание | +|---|---|---| +| 1 МБ | ok | read 4.86 мс | +| 10 МБ | ok | read 10.61 мс | +| 100 МБ | ok | total 0.78 с, ~133 МБ/с | + +- `GET /`, `/health`, `/static/style.css` — 200. + +### 3.5 Коммит и пуш +- 2 коммита (второй — убрал `__pycache__/*.pyc` из индекса, добавил `.gitignore`). +- Пуш в gitea: ветка `master` создана, `origin/master` отслеживается. +- Файлы в репо: `.gitignore`, `requirements.txt`, `site/app.py`, `site/static/style.css`, `site/templates/index.html`. + +## 4. Анализ: скорость «клиент» vs «сервер» + +Пример из реального теста (VirtualBox-7.2.8-173730-Win.exe, 177 853 032 байт = 169.614 МБ): + +``` +-- КЛИЕНТ -- +Длительность: 20.84 с +Средняя скорость (клиент): 8.14 МБ/с +-- СЕРВЕР -- +Чтение в память: 816.43 мс +Обработка всего: 3308.25 мс +Скорость (сервер): 207.75 МБ/с +``` + +- **Скорость клиента** (8.14 МБ/с) — браузер: размер ÷ полное время (нажатие → ответ). Реальная скорость передачи по сети клиент → прокси → сервер, со всеми задержками. **Главный показатель для диагностики** загрузки. +- **Скорость сервера** (207.75 МБ/с) — `app.py`: размер ÷ `f.read()`. Это локальное чтение уже дошедших байт из памяти, сеть ни при чём. Всегда высокая. +- Признак проблемы загрузки: клиентская скорость низкая/нулевая или обрыв до 100%; серверная при этом «красивая». + +## 5. Ключевая находка: «64KB» в contracts-flask — это СИМПТОМ, не лимит + +### 5.1 Факты из History (проект contracts) +- `History/llm-analysis/sonnet-upload-from-scratch.md` — гипотеза «POST > 64KB → Ingress рвёт TCP (HTTP 000), 64KB — жёсткий предел платформы». **Гипотеза.** +- `History/sessions/session-08-upload-saga.md` — реальные корневые причины: + 1. `original_bytes BYTEA NOT NULL` → PostgreSQL 500 при вставке `original_b64`. + 2. `base64.b64decode()` + BYTEA INSERT > 30 с → таймаут Ingress → обрыв соединения. + 3. `session-07`: до 7 отдельных DB-подключений на сборку чанков → медленно. +- `History/sessions/session-10-lucee-pipeline.md` — **ddos-guard рвёт HTTP/2-стримы на POST > ~50KB** (HTTP/2 PROTOCOL_ERROR) на платформе `services.ngcloud.ru`. +- `History/features/upload-analysis-v40.md` — даже Redis-очередь (RPUSH → 200 сразу, v1.38-1.39) падала ✗; curl 100KB → случайные HTTP 000. + +**Вывод:** «64KB» — порог, после которого обработка на сервере не успевала до таймаута прокси, + ddos-guard рвал крупные POST на `services`. Это не объективный лимит размера тела. + +### 5.2 Порядок HTTP-запроса (важно для понимания) +1. Браузер передаёт тело (сеть). Обработка ещё не идёт. +2. Тело полностью дошло до сервера. +3. Вызывается обработчик (декодирование, INSERT, парсинг). +4. Сервер отвечает. + +«Параллельности» обработки с передачей нет. Проблема — **медленный ответ после приёма тела** (`proxy_read_timeout` на Ingress): файл дошёл, а соединение оборвано до ответа → «Сеть»/HTTP 000. + +### 5.3 Почему loadtest грузит без проблем +- Бэкенд тривиальный: `f.read()` → MD5 → JSON. Никакой БД/Redis/декодирования/парсинга → ответ мгновенный → прокси не успевает оборвать. +- Плюс это dev-платформа (`pythonk8s.dev.nubes.ru`), где нет ddos-guard-обрыва крупных POST, как на `services`. + +## 6. Текущее состояние contracts-flask `/upload` (диагноз) + +`contracts-flask/site/routes/upload_bp.py`, `/upload` — всё синхронно до ответа: +1. `data = f.read()` +2. `content_hash = hashlib.sha256(data)…` +3. `documents.insert(..., original_bytes=base64.b64encode(data).decode(), ...)` — base64 ВСЕГО файла → БД +4. `result = parse_file(f.filename, data)` — **синхронный парсинг** +5. `documents.set_parsed(...)` / `set_error(...)` +6. `return jsonify(...)` + +Прочее: +- `site/config.py`: `MAX_CONTENT_LENGTH = 200 * 1024 * 1024` (200 МБ) — не причина. +- `site/app.py`: `app.run(..., threaded=True)`, БД — SQLite (thread-local). +- Фронт `site/static/files.js:uploadFile` — `fetch` (реального `%` нет, только таймер), v2.0.3. + +## 7. Предложенные правки contracts-flask (по приоритету, НЕ ВНЕСЕНЫ) + +1. **Вынести парсинг из `/upload`** (главное): `/upload` = принять → сохранить → мгновенный `ok`; `parse_file` — в фоновый `threading.Thread` (паттерн есть в `pipeline_bp.py`) или отдельный endpoint `/parse`. +2. **Убрать `base64.b64encode(data)`** при вставке — хранить сырые байты (BLOB/BYTEA) или не хранить исходник. +3. **Клиент: `XMLHttpRequest` + `xhr.upload.onprogress`** — реальный `%` + явный статус «дошёл/не дошёл». +4. `MAX_CONTENT_LENGTH` 200 МБ — оставить; ограничения ddos-guard/Ingress на `services` — вне кода. + +## 8. Открытые вопросы / следующий шаг + +- В UI loadtest нужно показать размер выбранного файла с разбивкой по 3 цифры (например `177 853 032`) — **отложено**, «при следующей правке». +- Правки в `contracts-flask` (раздел 7) — **ожидают команды «делай»** и уточнения объёма (всё 4 пункта или только п.1). + +## 9. Полезные команды + +```bash +# локальный запуск +cd /home/naeel/nubes/contracts/loadtest && python3 site/app.py + +# тест загрузки +curl -s --max-time 180 -F "file=@/tmp/lt_100mb.bin" http://127.0.0.1:5000/upload + +# логи пода на платформе +kubectl -n bb9ed30b-1794-44ff-81a2-6ff2731200c9 logs deploy/pythonk8s --tail=50 +```