14 KiB
ПАТТЕРН «ВМ-буфер + 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с.
Принципы:
- ВМ принимает файл напрямую (nginx,
client_max_body_sizeсвой, без лимита 64КБ). - ВМ хранит файл временно и отдаёт по HTTP (nginx static/alias).
- ВМ шлёт во Flask маленький POST (<64КБ — проходит шлюз) с метаданными и ссылкой на файл.
- Flask САМ делает исходящий GET к ВМ и читает файл (egress).
- Обработка/сборка/БД/UI — во Flask. ВМ — только приём+хранение (минимум логики).
- Данные наружу НЕ выходят: браузер → ВМ напрямую; ВМ отдаёт файл только Flask'у (по запросу).
3. Как повторить (пошагово)
3.0 Приём файла на ВМ (первая половина схемы)
-
Браузер шлёт файл на ВМ напрямую (минуя managed). На ВМ нужен приёмный endpoint. На ВМ уже работает
convert_server.py(:8766) — добавить туда endpoint, либо отдельный маленький сервис. Пример (Flask на ВМ):# ВМ: приём и временное хранение @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}", }) -
nginx на ВМ: для этого пути
client_max_body_size 100m(или больше) — БЕЗ лимита 64КБ. -
После сохранения ВМ сообщает Flask: маленький POST (<64КБ — проходит шлюз) с метаданными:
{ "id": "...", "url": "https://contracts.kube5s.ru/lt-serve/<id>", "name": "файл.pdf", "size": 47729215 } -
Flask получает метаданные и САМ тянет файл по
url(egress GET, см./fetchв 3.2).
3.1 На ВМ (5.172.178.213) — раздача файлов через существующий nginx
- Каталог:
/var/www/lt-serve/(nginx-юзерwww-dataимеет доступ;/home/naeel— НЕТ, 403). - В
nginx-contracts.conf(sites-enabled) добавлен location (отдельный блок, рядом с/docs/):location /lt-serve/ { alias /var/www/lt-serve/; } - Безопасно: backup (
/tmp/nginx-contracts.conf.bak.20260821) → правка →nginx -t→systemctl reload nginx. - Проверка:
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, возвращает статус+размер):
@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 Pythonsite.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) тянут образы ТОЛЬКО из внутреннего nexusnexus-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 шагов)
- Браузер →
POST https://contracts.kube5s.ru/upload-lt(nginx ВМ), файл 45МБ. - ВМ сохраняет в
/var/www/lt-serve/<id>, отвечает{id, size, url}. Без лимита 64КБ. - ВМ (или клиент) → маленький
POSTна managed.../upload-meta{url, name, size}— проходит шлюз. - Flask по метаданным делает исходящий
GET url(egress) и читает файл. - Flask обрабатывает (парсинг/БД/статусы). ВМ удаляет файл после загрузки или по TTL.
4. Кластеры (наблюдения 2026-08-21)
k8s-3-sandbox-nubes-ru— НЕ запускается вообще (баги платформы: скрипт манифестов падает).k8s-4-sandbox-nubes-ru/containerk8s— работает; образ из Docker Hub подтянулся; сервис поднялся после фикса gunicorn; одна аннотацияproxy-body-sizeставится.
5. Ограничения и рекомендации для прод-варианта
- OOM при больших файлах:
/fetchчитает весь файл в память (r.content). Квота пода 200МБ → 100МБ-файл убивает под (502, под перезапускается, health потом 200). Решение:requests.get(..., stream=True)+ чтение/запись по частям (буфер 64-256КБ), ИЛИ поднять квоту памяти (resourceMemory 1024МБ). Проверено: до 50МБ стабильно. - 1 worker gunicorn (sync): параллельные запросы сериализуются. Для параллельной
обработки —
--workers 2-4(учитывая память). - Безопасность ВМ-раздачи: файлы на ВМ должны быть доступны только Flask'у (токен/секрет в URL, TTL, удаление после загрузки). Иначе — открытый статический хостинг.
- Жизненный цикл: файл на ВМ удалять после того, как Flask его забрал (или по TTL).
- Входной шлюз остаётся с лимитом ~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.