Паттерн ВМ-буфер+pull: подробное руководство для переноса на другие сервисы
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# ПАТТЕРН «ВМ-буфер + 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`.
|
||||
Reference in New Issue
Block a user