# Сессия 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 ```