Docs: подробная запись сессии 2026-08-17 (loadtest, анализ 64KB contracts-flask)
This commit is contained in:
@@ -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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user