# ПАТТЕРН «ВМ-буфер + pull» — загрузка больших файлов на managed-сервисы Nubes _Дата: 2026-08-21. Проверено на LoadTest (`http-12.containerk8s.dev.nubes.ru`), образ `naeel/loadtest:v2.0.2`._ _Назначение: руководство для переделки логики загрузки в ДРУГИХ сервисах, где большой файл не проходит через managed-кластер._ --- ## 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.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`). --- ## 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`.