Files
loadtest/History/2026-08-21-pattern-vm-buffer-pull.md

195 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ПАТТЕРН «ВМ-буфер + pull» — загрузка больших файлов на managed-сервисы Nubes
_Дата: 2026-08-21. Проверено на LoadTest (`http-12.containerk8s.dev.nubes.ru`), образ `naeel/loadtest:v2.0.2`._
_Назначение: руководство для переделки логики загрузки в ДРУГИХ сервисах, где большой файл
не проходит через managed-кластер._
---
## 0. ПОЧЕМУ через ВМ — хронология попыток через кластер (все провалились)
Что пробовали, чтобы большой файл прошёл ВХОД на managed-кластер, — и итог:
| Попытка | Результат | Итог |
|---|---|---|
| Прямой `POST /upload` (multipart) | тела >~64КБ → HTTP 000 ~10с | блокер входа |
| Аннотации ingress (`proxy-body-size`, таймауты, `request-buffering`, `client-body-*`) | не влияют | бесполезно (обрыв на внешнем шлюзе) |
| `Transfer-Encoding: chunked` (чанки 32КБ) | обрыв ~10-15с при любом размере | бесполезно |
| Чанки на клиенте (POST по 50КБ, в т.ч. в БД) | только первый чанк доходит | бесполезно |
| ConfigMap ingress (`client-body-timeout 60`, `proxy-body-size 1024m`) + replicas=число нод | РАБОТАЕТ, но ТОЛЬКО на своём кластере (kubectl) | костыль; на чужом кластере нельзя |
| **Egress (Flask сам тянет с ВМ)** | 1-50МБ стабильно 200 | ✅ единственный универсальный путь |
**Вывод:** вход в managed-кластер физически ограничен (~64КБ / ~10с) и не настраивается через
UI/аннотации. Выход (egress) — НЕ ограничен. Поэтому: файлы входят через ВМ, а Flask тянет их
сам (pull). Детали неудач: `History/2026-08-21-annotation-tests.md`,
`History/2026-08-21-root-cause-found.md`.
---
## 1. Проблема (что и почему не работает)
**Входной шлюз managed-кластеров Nubes** (внешний балансировщик перед кластером):
- тела >~64КБ обрываются: HTTP 000 через ~10с (TTFB=0) — на общих кластерах
(`k8s-4-sandbox`, `containerk8s`); на iot-naeel была задержка ~51с, но 200.
- случайная граница в зоне ~64-84КБ (68КБ рвётся, 84КБ проходит), ≥100КБ — стабильно рвётся.
- это НЕ ingress-nginx кластера: аннотации `proxy-body-size`/таймауты на ingress-ресурсе
**не помогают** (проверено; поле `ingress_annotations` платформы «поддерживает пока простые аннотации»).
- **chunked (Transfer-Encoding, чанки 32КБ) тоже рвётся** — обрыв ~10-15с при любом размере
→ фиксированный таймаут шлюза на приём тела, способ передачи не влияет.
- **чанковая загрузка на клиенте** (несколько POST по 50КБ) — тоже НЕ работает на managed:
только первый чанк доходит, остальные рвутся (история contracts v1.15-1.21, 2026-06-17).
- `/upload` напрямую на managed: 1МБ → HTTP 000 ~10с (подтверждено 2026-08-21).
**Вывод:** большой файл (или много POST'ов) через внешний URL на managed-кластер — не проходит.
Правка возможна ТОЛЬКО на стороне платформы (входной шлюз), а не в коде/аннотациях/чанках.
---
## 2. Решение: ВМ = временный склад + Flask тянет сам (pull)
```
Браузер ──(большой файл)──▶ ВМ (nginx, БЕЗ лимитов платформы)
│ маленький POST (метаданные: id, имя, размер, url)
Flask (managed-контейнер, в кластере)
ВМ ◀──────(egress GET по url)┘ ← ИСХОДЯЩИЙ запрос из кластера
```
**Почему работает:** входной шлюз ограничивает **входящие тела запросов** (POST/upload).
**Исходящий (egress) GET** из кластера наружу — НЕ ограничен: большие ОТВЕТЫ проходят.
Проверено: 10МБ с ВМ → 200 за 0.9с, 50МБ → 200 за 5.3с.
**Принципы:**
1. ВМ принимает файл напрямую (nginx, `client_max_body_size` свой, без лимита 64КБ).
2. ВМ хранит файл временно и отдаёт по HTTP (nginx static/alias).
3. ВМ шлёт во Flask маленький POST (<64КБ — проходит шлюз) с метаданными и ссылкой на файл.
4. Flask САМ делает исходящий GET к ВМ и читает файл (egress).
5. Обработка/сборка/БД/UI — во Flask. ВМ — только приём+хранение (минимум логики).
6. Данные наружу НЕ выходят: браузер → ВМ напрямую; ВМ отдаёт файл только Flask'у (по запросу).
---
## 3. Как повторить (пошагово)
### 3.0 Приём файла на ВМ (первая половина схемы)
1. Браузер шлёт файл на ВМ напрямую (минуя managed). На ВМ нужен приёмный endpoint.
На ВМ уже работает `convert_server.py` (:8766) — добавить туда endpoint, либо отдельный
маленький сервис. Пример (Flask на ВМ):
```python
# ВМ: приём и временное хранение
@app.route("/upload-lt", methods=["POST"])
def upload_lt():
f = request.files["file"]
fid = uuid4().hex
path = f"/var/www/lt-serve/{fid}"
f.save(path)
return jsonify({
"ok": True, "id": fid, "size": os.path.getsize(path),
"url": f"https://contracts.kube5s.ru/lt-serve/{fid}",
})
```
2. nginx на ВМ: для этого пути `client_max_body_size 100m` (или больше) — БЕЗ лимита 64КБ.
3. После сохранения ВМ сообщает Flask: **маленький POST (<64КБ — проходит шлюз)** с метаданными:
```json
{ "id": "...", "url": "https://contracts.kube5s.ru/lt-serve/<id>", "name": "файл.pdf", "size": 47729215 }
```
4. Flask получает метаданные и САМ тянет файл по `url` (egress GET, см. `/fetch` в 3.2).
### 3.1 На ВМ (5.172.178.213) — раздача файлов через существующий nginx
1. Каталог: `/var/www/lt-serve/` (nginx-юзер `www-data` имеет доступ; `/home/naeel` — НЕТ, 403).
2. В `nginx-contracts.conf` (sites-enabled) добавлен location (отдельный блок, рядом с `/docs/`):
```nginx
location /lt-serve/ {
alias /var/www/lt-serve/;
}
```
3. Безопасно: backup (`/tmp/nginx-contracts.conf.bak.20260821`) → правка → `nginx -t` → `systemctl reload nginx`.
4. Проверка: `curl --noproxy '*' https://contracts.kube5s.ru/lt-serve/<файл>`.
⚠️ **Прокси:** локальные curl на Krupski идут через HTTP-прокси (`172.17.192.1:10808`),
который душит передачу (обрыв ~16КБ). Внешние тесты — ТОЛЬКО с `--noproxy '*'`.
### 3.2 Во Flask (managed) — endpoint pull
Временный endpoint `/fetch?url=...` (исходящий GET, возвращает статус+размер):
```python
@app.route("/fetch")
def fetch_url():
import requests
url = request.args.get("url", "")
if not url:
return jsonify({"ok": False, "error": "нет параметра url"}), 400
t0 = time.time()
try:
r = requests.get(url, timeout=120)
size = len(r.content)
return jsonify({
"ok": True, "url": url, "status": r.status_code,
"size_bytes": size, "size_mb": round(size / (1024 * 1024), 3),
"total_ms": round((time.time() - t0) * 1000, 2),
})
except Exception as e:
return jsonify({"ok": False, "error": type(e).__name__ + ": " + str(e), "url": url}), 502
```
В прод-варианте — `stream=True` и чтение по частям (см. раздел 5), и ОБЯЗАТЕЛЬНО
авторизация/тайм-лимит ссылок (ВМ не должен раздавать файлы кому попало).
### 3.3 Образ и деплой
- Dockerfile loadtest: `python:3.12-slim` + gunicorn.
- **ВАЖНО (грабли):** папка `site/` конфликтует со stdlib Python `site.py` →
`import site.app` падает (`ModuleNotFoundError`). Рабочий CMD:
```
gunicorn --bind 0.0.0.0:5000 --chdir /app/site --timeout 300 --workers 1 app:app
```
- Образы: `naeel/loadtest:v2.0.1` (фикс gunicorn), `v2.0.2` (+`/fetch`).
- Реестр: managed-кластеры (`k8s-3/4-sandbox`, `containerk8s`) тянут образы ТОЛЬКО из
внутреннего nexus `nexus-sa.tst.nubes.ru/docker-nubes/...` (дефолт `jolt`), НЕ из Docker Hub!
Для `k8s-4-sandbox` образ `naeel/loadtest` из Docker Hub завёлся (см. ниже «кластеры»).
- Аннотация: работает ТОЛЬКО одна — `nginx.ingress.kubernetes.io/proxy-body-size: 1024m`.
Добавление остальных (таймауты, request-buffering) → падение скрипта платформы
(`script returned exit code 1`, `No such property: name for class: Script2`).
### 3.4 Полный сквозной сценарий (5 шагов)
1. **Браузер** → `POST https://contracts.kube5s.ru/upload-lt` (nginx ВМ), файл 45МБ.
2. **ВМ** сохраняет в `/var/www/lt-serve/<id>`, отвечает `{id, size, url}`. Без лимита 64КБ.
3. **ВМ** (или клиент) → маленький `POST` на managed `.../upload-meta` `{url, name, size}` — проходит шлюз.
4. **Flask** по метаданным делает исходящий `GET url` (egress) и читает файл.
5. Flask обрабатывает (парсинг/БД/статусы). ВМ удаляет файл после загрузки или по TTL.
---
## 4. Кластеры (наблюдения 2026-08-21)
- `k8s-3-sandbox-nubes-ru` — НЕ запускается вообще (баги платформы: скрипт манифестов падает).
- `k8s-4-sandbox-nubes-ru` / `containerk8s` — работает; образ из Docker Hub подтянулся;
сервис поднялся после фикса gunicorn; одна аннотация `proxy-body-size` ставится.
---
## 5. Ограничения и рекомендации для прод-варианта
1. **OOM при больших файлах:** `/fetch` читает весь файл в память (`r.content`). Квота пода
200МБ → 100МБ-файл убивает под (502, под перезапускается, health потом 200).
**Решение:** `requests.get(..., stream=True)` + чтение/запись по частям (буфер 64-256КБ),
ИЛИ поднять квоту памяти (resourceMemory 1024МБ). Проверено: до 50МБ стабильно.
2. **1 worker gunicorn (sync):** параллельные запросы сериализуются. Для параллельной
обработки — `--workers 2-4` (учитывая память).
3. **Безопасность ВМ-раздачи:** файлы на ВМ должны быть доступны только Flask'у
(токен/секрет в URL, TTL, удаление после загрузки). Иначе — открытый статический хостинг.
4. **Жизненный цикл:** файл на ВМ удалять после того, как Flask его забрал (или по TTL).
5. **Входной шлюз** остаётся с лимитом ~64КБ — все НОВЫЕ входы больших данных — через ВМ.
---
## 6. Ссылки (история LoadTest)
- `History/2026-08-21-container-build.md` — сборка образа, аннотации.
- `History/2026-08-21-gunicorn-fix-v201.md` — фикс site.app → --chdir /app/site.
- `History/2026-08-21-annotation-tests.md` — тесты лимита, аннотация не помогла, chunked не помог.
- `History/2026-08-21-egress-confirmed.md` — подтверждение egress (Flask тянет с ВМ).
- `History/2026-08-21-stress-test-egress.md` — стресс-тест (7 сценариев).
- История contracts (чанки на managed не работают): `History/sessions/session-07-chunks.md`,
`History/features/chunk-analysis-request.md`, `History/features/connection-reset-analysis.md`.
- ВМ: `nginx-contracts.conf`, каталог `/var/www/lt-serve/`, backup `/tmp/nginx-contracts.conf.bak.20260821`.