Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c349ee869 | ||
|
|
48a7cbb7c0 | ||
|
|
bed4d38468 | ||
|
|
731ee67016 | ||
|
|
807ac734bf | ||
|
|
688eee2986 | ||
|
|
48bde14db5 | ||
|
|
3480db994e | ||
|
|
160f6d680a | ||
|
|
820d088d7d | ||
|
|
ab9a82a33d | ||
|
|
428a5c2db9 | ||
|
|
d47b0b3eda | ||
|
|
ae229d13ca | ||
|
|
1b4bf86b71 | ||
|
|
835949931e | ||
|
|
0e5d2942c9 |
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# LoadTest — Flask-сервис проверки загрузки файлов/сообщений.
|
||||||
|
# Образ: naeel/loadtest (Docker Hub, публичный — требование платформы Nubes).
|
||||||
|
#
|
||||||
|
# Деплой через «Простой HTTP контейнер»:
|
||||||
|
# - путь до образа: naeel/loadtest:<tag>
|
||||||
|
# - порт берётся из EXPOSE (ровно один) — 5000
|
||||||
|
# - переменные пода (env) — задаются в ЛК при создании (если нужны)
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY site /app/site
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# gunicorn вместо dev-сервера Flask:
|
||||||
|
# --timeout 300 — чтобы платформа/шлюз не резали долгие POST
|
||||||
|
# --workers 1 — однорепликовый тест-сервис, больше не нужно
|
||||||
|
# ВАЖНО: --chdir /app/site + app:app (НЕ site.app:app!) — папка 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"]
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 2026-08-21 — LoadTest: тесты лимита тела на http-12 (k8s-4-sandbox), аннотация не помогла
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Инстанс: `http-12.containerk8s.dev.nubes.ru` (k8s-4-sandbox-nubes-ru)._
|
||||||
|
_Конфиг: образ `naeel/loadtest:v2.0.1`, одна аннотация `nginx.ingress.kubernetes.io/proxy-body-size: 1024m`, CPU 100, mem 200._
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
Аннотация `proxy-body-size: 1024m` на ingress-ресурсе **НЕ решает** обрыв тел >~64КБ.
|
||||||
|
Лимит стоит на **внешнем входном шлюзе платформы** (вне кластера, до ingress-nginx).
|
||||||
|
|
||||||
|
## Замеры /messages (внешний URL, curl, сырое тело)
|
||||||
|
| Размер | Результат |
|
||||||
|
|---|---|
|
||||||
|
| 60КБ | 200 (0.39с) |
|
||||||
|
| 64КБ | 200 (0.46с) |
|
||||||
|
| 68КБ | **000** (10.3с) |
|
||||||
|
| 84КБ | 200 (0.6с) |
|
||||||
|
| 100КБ ×3 | **000** (10.2с) все три |
|
||||||
|
| 512КБ | **000** (10.2с) |
|
||||||
|
| 1МБ | **000** (10.2с) |
|
||||||
|
| 10МБ | **000** (10.7с) |
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
- Граница случайная в зоне ~64-84КБ (68КБ рвётся, 84КБ проходит) — как в diag-2026-08-18.
|
||||||
|
- Всё ≥100КБ — стабильный обрыв ~10.2с (HTTP 000, TTFB=0).
|
||||||
|
- Картина идентична замеру БЕЗ аннотации → аннотация ingress не влияет на внешний шлюз.
|
||||||
|
- Кластерные аннотации (proxy-body-size, таймауты, request-buffering) до внешнего шлюза не достают.
|
||||||
|
|
||||||
|
## Дальше
|
||||||
|
- Обход: чанкованная передача (Transfer-Encoding: chunked, чанки по 32КБ).
|
||||||
|
- Или тикет на платформу (поднять лимит на входном шлюзе).
|
||||||
|
|
||||||
|
## Тест chunked (Transfer-Encoding: chunked, чанки по 32КБ) — ТОЖЕ ОБРЫВ
|
||||||
|
| Размер | Результат |
|
||||||
|
|---|---|
|
||||||
|
| 100КБ | RemoteDisconnected, 9.6с |
|
||||||
|
| 512КБ | SSLEOFError, 11.3с |
|
||||||
|
| 1МБ | SSLEOFError, 14.6с |
|
||||||
|
| 10МБ | SSLEOFError, 9.6с |
|
||||||
|
|
||||||
|
Вывод: chunked НЕ обходит шлюз. Время обрыва ~10-15с не пропорционально размеру
|
||||||
|
→ фиксированный таймаут внешнего шлюза на приём тела (аналог ~50с на iot-naeel,
|
||||||
|
но ~10с на общем кластере). Способ передачи (Content-Length/chunked) не влияет.
|
||||||
|
Решение — только на стороне платформы (тикет: поднять таймаут/лимит на входном шлюзе),
|
||||||
|
либо дробление на отдельные запросы <64КБ (меняет протокол).
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 2026-08-21 — Новые аннотации (client-body-buffer/timeout, proxy-http-version) НЕ помогли
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Инстанс: http-12.containerk8s.dev.nubes.ru. Всего 8 аннотаций:
|
||||||
|
proxy-body-size, read/send 600, connect 120, request-buffering false,
|
||||||
|
client-body-buffer-size 1024m, client-body-timeout 600, proxy-http-version 1.1._
|
||||||
|
|
||||||
|
## Замеры (входной шлюз)
|
||||||
|
| Тест | Результат |
|
||||||
|
|---|---|
|
||||||
|
| 100КБ ×5 | 000, 000, 200, 000, 000 — 1 из 5 |
|
||||||
|
| 1МБ ×5 | 000 все |
|
||||||
|
| 10МБ ×3 | 000 все |
|
||||||
|
| /upload 1МБ ×3 | 000, 200, 000 — 1 из 3 |
|
||||||
|
|
||||||
|
## Вывод (ОКОНЧАТЕЛЬНЫЙ)
|
||||||
|
- Ни одна комбинация ingress-аннотаций (5 или 8 шт.) не влияет на обрыв >~64КБ.
|
||||||
|
- Обрыв — на ВНЕШНЕМ входном шлюзе платформы (вне кластера), до ingress-nginx.
|
||||||
|
- Случайный успех ~1 из 3-5 — нестабильность шлюза, не следствие аннотаций.
|
||||||
|
- Паттерн «ВМ-буфер + pull» — единственный надёжный путь больших файлов на managed.
|
||||||
|
|
||||||
|
## Рекомендация
|
||||||
|
Возвращаться к 5 рабочим аннотациям не обязательно (все 8 применяются), но и смысла в
|
||||||
|
дополнительных нет. Для больших файлов — только egress/pull с ВМ.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-08-21 — Проверка после починки деплоя: входной шлюз по-прежнему рвёт >64КБ
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Инстанс: http-12.containerk8s.dev.nubes.ru (k8s-4-sandbox).
|
||||||
|
Новый конфиг: CPU 1000, RAM 1024МБ, образ v2.0.2, 5 аннотаций:
|
||||||
|
proxy-body-size 1024m, read/send 600, connect 120, proxy-request-buffering false._
|
||||||
|
|
||||||
|
## Что изменилось
|
||||||
|
Деплой ПОЧИНИЛИ: 5 аннотаций теперь применяются (раньше >1 аннотации валило скрипт),
|
||||||
|
память поднята до 1024МБ. Сервис работает.
|
||||||
|
|
||||||
|
## Тесты ОБЫЧНОЙ загрузки (входной шлюз, НЕ через ВМ)
|
||||||
|
| Размер | Результат |
|
||||||
|
|---|---|
|
||||||
|
| 60КБ / 64КБ | 200 (0.09-0.12с) |
|
||||||
|
| 68КБ / 84КБ | 000 ~10с |
|
||||||
|
| 100КБ ×5 | 000 все (~10-15с) |
|
||||||
|
| 512КБ | 000 ~10с |
|
||||||
|
| 1МБ ×5 | 000 все (~10с) |
|
||||||
|
| 10МБ ×3 | 000, 000, 200 (2.5с) — 1 из 3 |
|
||||||
|
| /upload 1МБ ×3 | 200, 000, 000 — 1 из 3 |
|
||||||
|
|
||||||
|
## Вывод
|
||||||
|
- 5 аннотаций входной шлюз НЕ чинят: обрывы >~64КБ сохраняются, редкий случайный успех (~1 из 3-5).
|
||||||
|
- Паттерн «ВМ-буфер + pull» остаётся ЕДИНСТВЕННЫМ надёжным путём для больших файлов.
|
||||||
|
- Для сравнения (egress, тот же инстанс): 1МБ-50МБ тянутся с ВМ стабильно (см. stress-test-egress).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 2026-08-21 — LoadTest: сборка Docker-образа для «Простой HTTP контейнер»
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Ветка: master. Состояние Flask сохранено в ветке `flask-state` (запушена)._
|
||||||
|
|
||||||
|
## Задача
|
||||||
|
Перевести loadtest с managed-Flask (gitPath, без аннотаций) на «Простой HTTP контейнер»:
|
||||||
|
образ + переменные пода + аннотации к Ingress (раньше правили кубером на своём кластере).
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
1. Ветка `flask-state` — сохранение состояния Flask (запушена в origin).
|
||||||
|
2. `Dockerfile` в loadtest (master):
|
||||||
|
- `python:3.12-slim`, WORKDIR /app, requirements.txt, COPY site/, EXPOSE 5000
|
||||||
|
- CMD gunicorn `--timeout 300 --workers 1 site.app:app`
|
||||||
|
- Один EXPOSE (5000) — требование платформы (порт берётся из EXPOSE).
|
||||||
|
3. Сборка на ВМ (SSH remote-dev, docker 29.1.3): `docker build -t naeel/loadtest:v2.0.0`
|
||||||
|
- Контекст: `/home/naeel/loadtest-build` (rsync из локального loadtest).
|
||||||
|
- Логин Docker Hub на ВМ уже есть (`naeel`).
|
||||||
|
4. Push: `naeel/loadtest:v2.0.0` — digest `sha256:432a8742...`, linux/amd64, ~50MB, публичный.
|
||||||
|
|
||||||
|
## Данные для формы «Простой HTTP контейнер»
|
||||||
|
- **Путь до образа:** `naeel/loadtest:v2.0.0`
|
||||||
|
- **Переменные пода:** `{}` (секретов/окружения нет)
|
||||||
|
- **Дополнительные аннотации к Ingress** (JSON):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-body-size": "1024m",
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-connect-timeout": "120",
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-read-timeout": "600",
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-send-timeout": "600",
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-request-buffering": "false"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Почему эти аннотации
|
||||||
|
Взяты из правок, которые раньше делали кубером на iot-naeel (см. History drhider):
|
||||||
|
- `proxy-body-size 1024m` — лимит тела (на общем кластере >64КБ обрывало: HTTP 000 ~10с).
|
||||||
|
- `connect/read/send-timeout 120/600/600` — таймауты (drhider ingress те же значения).
|
||||||
|
- `proxy-request-buffering false` — отключение буферизации тела (план, не выполненный на iot-naeel).
|
||||||
|
|
||||||
|
## Обновление образа
|
||||||
|
Только пересоздание контейнера с новым тегом (`latest` кэшируется зеркалом платформы).
|
||||||
|
В своём кластере — `kubectl -n <ns> set image deployment/containerk8s app=naeel/loadtest:<tag>`.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 2026-08-21 — EGRESS подтверждён: Flask (managed) тянет большие файлы с ВМ
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Инстанс: `http-12.containerk8s.dev.nubes.ru` (k8s-4-sandbox), образ v2.0.2._
|
||||||
|
|
||||||
|
## Суть
|
||||||
|
Проверена схема «ВМ-буфер + Flask тянет сам (pull)»:
|
||||||
|
браузер → ВМ (большой файл, без шлюза); ВМ шлёт маленький POST с метаданными;
|
||||||
|
Flask САМ тянет файл с ВМ исходящим GET (egress).
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
1. ВМ (5.172.178.213): раздача статических файлов через существующий nginx
|
||||||
|
(`nginx-contracts.conf`, добавлен `location /lt-serve/` → `/var/www/lt-serve/`):
|
||||||
|
- `https://contracts.kube5s.ru/lt-serve/big-1m.bin` — 200, 1MB, ~0.2с
|
||||||
|
- `https://contracts.kube5s.ru/lt-serve/big-10m.bin` — 200, 10MB, ~1.0с
|
||||||
|
- Backup конфига: /tmp/nginx-contracts.conf.bak.20260821
|
||||||
|
2. LoadTest: временный endpoint `/fetch?url=...` (исходящий GET, возвращает
|
||||||
|
статус+размер). Образ `naeel/loadtest:v2.0.2`.
|
||||||
|
|
||||||
|
## Результат egress (ЗАПРОС ИЗ КЛАСТЕРА, managed-контейнер)
|
||||||
|
| Тест | Результат |
|
||||||
|
|---|---|
|
||||||
|
| `/fetch` 1MB (contracts.kube5s.ru) | 200, size 1048576, ~302мс |
|
||||||
|
| `/fetch` 10MB | 200, size 10485760, ~889мс |
|
||||||
|
|
||||||
|
## Вывод
|
||||||
|
- Egress из managed-кластера наружу РАЗРЕШЁН; большие ОТВЕТЫ проходят без обрывов.
|
||||||
|
- Входной шлюз платформы (лимит ~64КБ, обрывы POST ~10с) в egress НЕ участвует.
|
||||||
|
- Схема «ВМ = временный склад + pull» РАБОТАЕТ: данные наружу не выходят
|
||||||
|
(браузер → ВМ напрямую, Flask тянет с ВМ; ВМ → внешний вход не используется).
|
||||||
|
|
||||||
|
## Наблюдение (прокси)
|
||||||
|
Локальные curl-ы на Krupski идут через HTTP-прокси (172.17.192.1:10808), который
|
||||||
|
душит передачу (обрыв ~16КБ). Тесты извне — ТОЛЬКО с `--noproxy '*'`.
|
||||||
|
|
||||||
|
## Замечание
|
||||||
|
Версия в морде/health осталась v2.0.0 (VERSION в коде не меняли — по команде пользователя).
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 2026-08-21 — LoadTest: фикс gunicorn (site.app → --chdir /app/site app:app), v2.0.1
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Ветка: master. Инстанс: http-12.containerk8s.dev.nubes.ru (503)._
|
||||||
|
|
||||||
|
## Симптом
|
||||||
|
Инстанс отдавал 503 на всех путях (ingress nginx отвечал, бэкенд недоступен).
|
||||||
|
Локальный прогон образа на ВМ показал падение gunicorn:
|
||||||
|
```
|
||||||
|
ModuleNotFoundError: No module named 'site.app'; 'site' is not a package
|
||||||
|
```
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
Папка `site/` в проекте конфликтует со **стандартным модулем Python `site.py`**
|
||||||
|
(автоматически импортируется интерпретатором при старте). `import site.app` всегда
|
||||||
|
резолвится в stdlib-`site` → ModuleNotFoundError → worker падает → под не Ready → 503.
|
||||||
|
На managed-Flask работало, потому что платформа запускала ФАЙЛ `python site/app.py`,
|
||||||
|
а не импорт пакета `site.app`.
|
||||||
|
|
||||||
|
## Фикс
|
||||||
|
`Dockerfile` CMD:
|
||||||
|
```
|
||||||
|
gunicorn --bind 0.0.0.0:5000 --chdir /app/site --timeout 300 --workers 1 app:app
|
||||||
|
```
|
||||||
|
- `--chdir /app/site` + импорт `app:app` (без пути через `site`) — stdlib-`site` не мешает.
|
||||||
|
- Проверено на ВМ: `docker run naeel/loadtest:v2.0.1` → `/health` = `ok v2.0.0`, HTTP 200.
|
||||||
|
|
||||||
|
## Образ
|
||||||
|
- `naeel/loadtest:v2.0.1` — digest `sha256:9ad5cd52...`, запушен в Docker Hub.
|
||||||
|
- В ЛК редеплой с путём `naeel/loadtest:v2.0.1` (новый тег — старый v2.0.0 мог кэшироваться зеркалом).
|
||||||
|
|
||||||
|
## Кластеры (наблюдения 2026-08-21)
|
||||||
|
- `k8s-3-sandbox-nubes-ru` — НЕ запускается вообще (баги платформы: script returned exit code 1,
|
||||||
|
No such property: name for class: Script2, поды не работают).
|
||||||
|
- `k8s-4-sandbox-nubes-ru` — запустилось, но ТОЛЬКО с одной аннотацией:
|
||||||
|
`nginx.ingress.kubernetes.io/proxy-body-size: 1024m`.
|
||||||
|
Добавление других аннотаций (таймауты/request-buffering) → падение скрипта платформы
|
||||||
|
(поле `ingress_annotations` «поддерживает пока простые аннотации»).
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# ПАТТЕРН «ВМ-буфер + 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`.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 2026-08-21 — НАЙДЕНО: что править кубером, чтобы закачки шли (вне аннотаций)
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Кластер: example-timasbxz (свой, kubectl-admin). Инстанс: http-13.containerk8s.dev.nubes.ru._
|
||||||
|
_Ответ на вопрос: «что мы вручную кубером меняли на iot-naeel, чего нет в аннотациях, и закачки начинали работать»._
|
||||||
|
|
||||||
|
## ДВЕ правки, после которых закачки пошли (проверено по одной)
|
||||||
|
|
||||||
|
### 1. ConfigMap ingress: `client-body-timeout: 10 → 60` (ГЛАВНЫЙ виновник обрыва ~10с)
|
||||||
|
```
|
||||||
|
kubectl -n ingress patch cm shturval-ingress-controller-controller \
|
||||||
|
--type merge -p '{"data":{"client-body-timeout":"60"}}'
|
||||||
|
```
|
||||||
|
- До: `client-body-timeout: 10` → POST >~64КБ обрывался РОВНО через ~10.2с (HTTP 000, TTFB=0).
|
||||||
|
- После: обрыв на 10с исчез, тело принимается (соединение держится до таймаута).
|
||||||
|
- НЕ переопределяется аннотациями ingress-ресурса (аннотации для client-body-timeout НЕТ).
|
||||||
|
- Совпадает с iot-naeel: там стояло 60 → задержка ~51с (не обрыв); на общих кластерах 10 → обрыв 10с.
|
||||||
|
|
||||||
|
### 2. Ingress replicas: 2 → 3 (по поду на ноду) — убрало дропы при externalTrafficPolicy: Local
|
||||||
|
```
|
||||||
|
kubectl -n ingress scale deploy shturval-ingress-controller-controller --replicas=3
|
||||||
|
```
|
||||||
|
- Кластер: 3 ноды (control-plane + 2 workers), ingress было 2 пода, `externalTrafficPolicy: Local`.
|
||||||
|
- При Local трафик на ноду БЕЗ ingress-пода дропается/зависает → нестабильность (часть POST зависала).
|
||||||
|
- После scale до 3 (под на каждой ноде) — закачки стабильны.
|
||||||
|
|
||||||
|
## Результат (внешний путь, https://IP:443 -k, Host http-13)
|
||||||
|
| Тест | До правок | После правок |
|
||||||
|
|---|---|---|
|
||||||
|
| 100КБ | обрыв ровно 10.2с | 200 (0.06-0.1с) ×3 |
|
||||||
|
| 1МБ | случайно 200/000 | 200 (0.2-0.4с) |
|
||||||
|
| 10МБ | обрыв ~10-16с | 200 (1.9-2.1с) ×3 |
|
||||||
|
| /upload 1МБ | случайно | 200 (0.4с) |
|
||||||
|
|
||||||
|
## Итог (ответ на вопрос)
|
||||||
|
На iot-naeel «лечили закачки» именно этими правками кубером (вне аннотаций):
|
||||||
|
1. **client-body-timeout** в глобальном ConfigMap контроллера (60) — лечит обрыв ~10с.
|
||||||
|
2. **реплики ingress = число нод** (при externalTrafficPolicy: Local) — лечит дропы/зависания.
|
||||||
|
Дополнительно на iot-naeel: keep-alive 75, proxy-body-size 1024m (ConfigMap), MTU Cilium 1400, MSS clamping.
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
- Через UI-аннотации эти параметры НЕ задаются (client-body-timeout и реплики — не аннотации).
|
||||||
|
- На обычном/чужом кластере (без kubectl) их может поменять только платформа.
|
||||||
|
- Аннотации (proxy-body-size и пр.) на входной обрыв >64КБ не влияли — влиял именно client-body-timeout.
|
||||||
|
|
||||||
|
## ДОПОЛНЕНИЕ: аннотации НЕ НУЖНЫ (проверено)
|
||||||
|
Эксперимент: удалил ВСЕ nginx-аннотации с ingress-ресурса (фактических: []),
|
||||||
|
оставил только ConfigMap (client-body-timeout 60, proxy-body-size 1024m) + replicas 3.
|
||||||
|
Закачки работают:
|
||||||
|
| Тест (без аннотаций) | Результат |
|
||||||
|
|---|---|
|
||||||
|
| 100КБ | 200 (0.07с) |
|
||||||
|
| 1МБ | 200 (0.16с) |
|
||||||
|
| 10МБ | 200 (1.44с) |
|
||||||
|
|
||||||
|
ВЫВОД: для закачек нужен ТОЛЬКО ConfigMap контроллера (client-body-timeout, proxy-body-size)
|
||||||
|
и реплики по числу нод при Local. Per-ingress аннотации (proxy-body-size и пр.) НЕ влияют и не нужны.
|
||||||
|
|
||||||
|
## ПОДТВЕРЖДЕНИЕ передеплоем (2-й раз)
|
||||||
|
Снова удалил аннотации ([]), сделал `kubectl -n NS rollout restart deploy/containerk8s`.
|
||||||
|
После передеплоя без аннотаций закачки работают: health 200, 100КБ 200 (0.06с),
|
||||||
|
1МБ 200 (0.25с), 10МБ 200 (1.4с), /upload 1МБ 200 (0.2с).
|
||||||
|
ВЫВОД подтверждён: аннотации не влияют; решают ConfigMap + replicas по числу нод.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 2026-08-21 — LoadTest: стресс-тест egress (Flask тянет с ВМ) — результаты
|
||||||
|
|
||||||
|
_Дата: 2026-08-21. Инстанс: http-12.containerk8s.dev.nubes.ru (k8s-4-sandbox), образ v2.0.2.
|
||||||
|
Квота пода: CPU 100m, RAM 200MB, 1 реплика, gunicorn 1 worker (sync)._
|
||||||
|
|
||||||
|
## Сценарии и результаты
|
||||||
|
|
||||||
|
| Сценарий | Результат |
|
||||||
|
|---|---|
|
||||||
|
| Одиночный 1МБ / 10МБ | 200 (0.2с / 0.9с) |
|
||||||
|
| Одиночный 50МБ | 200 (5.3с) |
|
||||||
|
| Одиночный 100МБ | **502 ~10с + health 502** → OOM (квота 200МБ) |
|
||||||
|
| Серия 30×1МБ | 30/30 ok |
|
||||||
|
| Серия 10×10МБ | 10/10 ok |
|
||||||
|
| Параллельно 5×1МБ | 5/5 ok (сериализовано 1 worker'ом, 0.17-0.86с) |
|
||||||
|
| Долгий микс 50 (1МБ+10МБ) | 50/50 ok, health стабилен 200 |
|
||||||
|
| Повтор 3×50МБ | 3/3 ok (~5.3с стабильно) |
|
||||||
|
| /fetch на несуществующий URL | 200 от /fetch, status 404 от upstream (обработка ок) |
|
||||||
|
| /upload 1МБ (входной шлюз) | **HTTP 000 ~10с** — входной шлюз по-прежнему режет >64КБ (ожидаемо) |
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
1. **Egress стабилен**: 1МБ-50МБ тянутся надёжно, серии/миксы без сбоев, health держится.
|
||||||
|
2. **Ограничение — память пода**: файл >~50-80МБ при квоте 200МБ → OOM → 502 (под перезапускается,
|
||||||
|
потом health 200). Это НЕ проблема egress — это квота + `r.content` (весь файл в памяти).
|
||||||
|
3. **1 worker (sync)** — параллельные запросы сериализуются (не баг, а режим; для теста ок).
|
||||||
|
4. Входной шлюз (>64КБ) НЕ обойдён для /upload — но схема «ВМ-буфер» его и не использует
|
||||||
|
(браузер → ВМ, Flask тянет с ВМ по egress).
|
||||||
|
|
||||||
|
## Рекомендации для прод-варианта
|
||||||
|
- Поднять квоту памяти (например 1024МБ) ИЛИ потоковая передача (не `r.content`, а `stream=True`
|
||||||
|
+ чтение по частям) — уберёт OOM на 100МБ+.
|
||||||
|
- При росте нагрузки — увеличить worker'ов gunicorn (сейчас 1).
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
# ⛔ НЕ ИСПОЛЬЗОВАТЬ !!! СОЗДАНО ОШИБОЧНО !!! ⛔
|
||||||
|
|
||||||
|
# ПЛАН: загрузка больших файлов через ВМ (pull) — реализация во Flask
|
||||||
|
|
||||||
|
_Дата: 2026-08-23. Цель: переделать `/upload` так, чтобы большой файл входил через ВМ, а Flask сам тянул его (egress)._
|
||||||
|
_Язык: Python/Flask. Основа: `site/app.py` (текущий), паттерн `History/2026-08-21-pattern-vm-buffer-pull.md`._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Что меняем (кратко)
|
||||||
|
|
||||||
|
Сейчас `/upload` принимает multipart напрямую на managed → тело >64КБ рвётся на входном шлюзе.
|
||||||
|
Меняем на 3 хопа:
|
||||||
|
|
||||||
|
```
|
||||||
|
Браузер ──POST файл──▶ ВМ /upload-lt (nginx, client_max_body_size 100m) → {id, size, url}
|
||||||
|
Браузер ──POST метаданные (<64КБ)──▶ Flask /upload-vm (managed)
|
||||||
|
Flask ──GET url (egress, stream)──▶ ВМ /lt-serve/<id> → читает файл по частям
|
||||||
|
```
|
||||||
|
|
||||||
|
Список правок:
|
||||||
|
|
||||||
|
| # | Где | Что |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | ВМ: nginx | `location /upload-lt/` + `client_max_body_size 100m`; `location /lt-serve/` уже есть |
|
||||||
|
| 2 | ВМ: Flask-приём (`convert_server.py` или отдельный) | endpoint `/upload-lt` (сохранить, вернуть url) + `/lt-serve/<id>` DELETE |
|
||||||
|
| 3 | managed Flask `app.py` | новый `/upload-vm` (метаданные + egress pull со stream) + `/config` (отдать браузеру URL ВМ) |
|
||||||
|
| 4 | `templates/index.html` | загрузка в 2 хопа вместо `/upload` |
|
||||||
|
| 5 | `requirements.txt` | `requests` уже есть — ничего нового |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ВМ: nginx
|
||||||
|
|
||||||
|
В `nginx-contracts.conf` (sites-enabled) — два блока.
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# приём файла (без лимита 64КБ), проброс на локальный Flask ВМ (:8766)
|
||||||
|
location /upload-lt {
|
||||||
|
proxy_pass http://127.0.0.1:8766;
|
||||||
|
client_max_body_size 100m;
|
||||||
|
proxy_request_buffering off; # не буферизовать тело на nginx
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# раздача временного файла (уже добавлен 2026-08-21)
|
||||||
|
location /lt-serve/ {
|
||||||
|
alias /var/www/lt-serve/;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
CORS для браузера (кросс-домен: браузер на managed, POST на ВМ) — в блоке `/upload-lt`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
add_header Access-Control-Allow-Origin "https://http-12.containerk8s.dev.nubes.ru" always;
|
||||||
|
add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||||
|
if ($request_method = OPTIONS) { return 204; }
|
||||||
|
```
|
||||||
|
|
||||||
|
_Примечание: `Access-Control-Allow-Origin` — подставить реальный origin managed-сервиса; для диагностики можно `*`._
|
||||||
|
|
||||||
|
Порядок: `cp nginx-contracts.conf /tmp/nginx-contracts.conf.bak.$(date +%Y%m%d)` → правка → `nginx -t` → `systemctl reload nginx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ВМ: приём файла (Flask на ВМ, порт :8766)
|
||||||
|
|
||||||
|
Добавить в `convert_server.py` (или отдельный `upload_server.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os, uuid, hmac, hashlib, time
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
VM_TOKEN = os.environ.get("VM_TOKEN", "CHANGE_ME") # общий секрет с managed-Flask
|
||||||
|
SERVE_DIR = "/var/www/lt-serve" # nginx-юзер www-data имеет доступ
|
||||||
|
|
||||||
|
def _sign(fid: str) -> str:
|
||||||
|
return hmac.new(VM_TOKEN.encode(), fid.encode(), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
@app.route("/upload-lt", methods=["POST"])
|
||||||
|
def upload_lt():
|
||||||
|
if "file" not in request.files:
|
||||||
|
return jsonify({"ok": False, "error": "нет поля file"}), 400
|
||||||
|
f = request.files["file"]
|
||||||
|
if not f or f.filename == "":
|
||||||
|
return jsonify({"ok": False, "error": "пустое имя"}), 400
|
||||||
|
|
||||||
|
fid = uuid.uuid4().hex
|
||||||
|
path = os.path.join(SERVE_DIR, fid)
|
||||||
|
f.save(path) # потоковая запись на диск
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
url = f"https://contracts.kube5s.ru/lt-serve/{fid}?t={_sign(fid)}"
|
||||||
|
return jsonify({"ok": True, "id": fid, "size": size, "url": url})
|
||||||
|
|
||||||
|
@app.route("/lt-serve/<fid>", methods=["DELETE"])
|
||||||
|
def delete_lt(fid):
|
||||||
|
token = request.args.get("t", "")
|
||||||
|
if not hmac.compare_digest(token, _sign(fid)):
|
||||||
|
return jsonify({"ok": False, "error": "bad token"}), 403
|
||||||
|
path = os.path.join(SERVE_DIR, fid)
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.remove(path)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
```
|
||||||
|
|
||||||
|
_Каталог `SERVE_DIR` должен существовать и быть writable для пользователя Flask-ВМ (www-data)._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Managed Flask (`site/app.py`)
|
||||||
|
|
||||||
|
### 3.1 Новый endpoint `/upload-vm`
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os, hmac, hashlib, time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
VM_TOKEN = os.environ.get("VM_TOKEN", "CHANGE_ME")
|
||||||
|
CHUNK = 256 * 1024 # буфер чтения при pull (не грузить файл в память целиком)
|
||||||
|
|
||||||
|
def _sign(fid: str) -> str:
|
||||||
|
return hmac.new(VM_TOKEN.encode(), fid.encode(), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
@app.route("/upload-vm", methods=["POST"])
|
||||||
|
def upload_vm():
|
||||||
|
meta = request.get_json(force=True)
|
||||||
|
fid = meta.get("id")
|
||||||
|
url = meta.get("url")
|
||||||
|
name = meta.get("name", "")
|
||||||
|
expected_size = meta.get("size", 0)
|
||||||
|
if not fid or not url:
|
||||||
|
return jsonify({"ok": False, "error": "нужны id и url"}), 400
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
# egress pull — STREAM (не r.content, чтобы не убить под по OOM)
|
||||||
|
r = requests.get(url, timeout=300, stream=True)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
md5 = hashlib.md5()
|
||||||
|
size = 0
|
||||||
|
out_path = os.path.join("/tmp", fid)
|
||||||
|
with open(out_path, "wb") as out:
|
||||||
|
for chunk in r.iter_content(chunk_size=CHUNK):
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
md5.update(chunk)
|
||||||
|
size += len(chunk)
|
||||||
|
out.write(chunk)
|
||||||
|
total_s = time.time() - t0
|
||||||
|
|
||||||
|
# целостность
|
||||||
|
ok = (size == expected_size)
|
||||||
|
|
||||||
|
# сообщить ВМ удалить файл
|
||||||
|
try:
|
||||||
|
requests.delete(url.split("?")[0] + f"?t={_sign(fid)}", timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass # удалит TTL-очистка на ВМ
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"ok": ok,
|
||||||
|
"id": fid, "name": name,
|
||||||
|
"size_bytes": size,
|
||||||
|
"size_mb": round(size / (1024 * 1024), 3),
|
||||||
|
"md5": md5.hexdigest(),
|
||||||
|
"pull_ms": round(total_s * 1000, 2),
|
||||||
|
"speed_mb_s": round(size / (1024 * 1024) / total_s, 3) if total_s > 0 else 0,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 `/config` — отдать браузеру URL ВМ (чтобы не хардкодить в JS)
|
||||||
|
|
||||||
|
```python
|
||||||
|
VM_UPLOAD_URL = os.environ.get("VM_UPLOAD_URL", "https://contracts.kube5s.ru/upload-lt")
|
||||||
|
|
||||||
|
@app.route("/config")
|
||||||
|
def config():
|
||||||
|
return jsonify({"vm_upload_url": VM_UPLOAD_URL, "version": VERSION})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Убрать временное
|
||||||
|
|
||||||
|
- `/fetch` — удалить или оставить как диагностику (на прод не выносить).
|
||||||
|
- `/upload` (multipart) — оставить для справки/старых тестов, но фронтенд переключить на `/upload-vm`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Фронтенд (`templates/index.html`)
|
||||||
|
|
||||||
|
Заменить логику `startUpload()`: два XHR вместо одного.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function startUpload() {
|
||||||
|
var input = document.getElementById('file');
|
||||||
|
var file = input.files[0];
|
||||||
|
if (!file) { /* ошибка */ return; }
|
||||||
|
|
||||||
|
var startMs = Date.now();
|
||||||
|
|
||||||
|
// 1) файл напрямую на ВМ
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('file', file, file.name);
|
||||||
|
|
||||||
|
var x1 = new XMLHttpRequest();
|
||||||
|
x1.open('POST', VM_UPLOAD_URL); // из /config, либо хардкод
|
||||||
|
x1.timeout = 600000;
|
||||||
|
x1.upload.onprogress = function(e) { /* прогресс как сейчас */ };
|
||||||
|
|
||||||
|
x1.onload = function() {
|
||||||
|
var up = JSON.parse(x1.responseText); // {ok, id, size, url}
|
||||||
|
if (!up.ok) { /* ошибка ВМ */ return; }
|
||||||
|
|
||||||
|
// 2) маленький POST метаданных во Flask (same-origin)
|
||||||
|
var meta = { id: up.id, url: up.url, name: file.name, size: up.size };
|
||||||
|
var x2 = new XMLHttpRequest();
|
||||||
|
x2.open('POST', '/upload-vm');
|
||||||
|
x2.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
x2.onload = function() {
|
||||||
|
var data = JSON.parse(x2.responseText);
|
||||||
|
renderUploadStats(file, startMs, data); // вывести размер/md5/pull_ms
|
||||||
|
};
|
||||||
|
x2.onerror = function() { /* сетевая ошибка */ };
|
||||||
|
x2.send(JSON.stringify(meta));
|
||||||
|
};
|
||||||
|
x1.onerror = function() { /* сетевая ошибка на ВМ */ };
|
||||||
|
x1.send(fd);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Получить `VM_UPLOAD_URL` при загрузке страницы:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var VM_UPLOAD_URL;
|
||||||
|
fetch('/config').then(r => r.json()).then(c => { VM_UPLOAD_URL = c.vm_upload_url; });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Конфиг/переменные (пода managed)
|
||||||
|
|
||||||
|
Задать в ЛК при создании пода (или манифесте):
|
||||||
|
|
||||||
|
| Переменная | Значение |
|
||||||
|
|---|---|
|
||||||
|
| `VM_TOKEN` | общий секрет (одинаковый на ВМ и в поде) |
|
||||||
|
| `VM_UPLOAD_URL` | `https://contracts.kube5s.ru/upload-lt` |
|
||||||
|
|
||||||
|
На ВМ: `VM_TOKEN` в окружении `convert_server.py`/systemd-юнита.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Безопасность (обязательно)
|
||||||
|
|
||||||
|
1. `id` = `uuid4().hex` (128 бит, не угадать).
|
||||||
|
2. `url` содержит подписанный `t=<hmac>` — Flask перепроверяет при DELETE.
|
||||||
|
3. Файл удаляется сразу после pull (DELETE) + TTL-очистка на ВМ (cron: `find /var/www/lt-serve -mmin +60 -delete`).
|
||||||
|
4. `/lt-serve/` отдаёт файл только по полному url с токеном; без него 404 (опционально проверка в nginx `secure_link`).
|
||||||
|
5. `VM_TOKEN` — НЕ в коде, только env.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Деплой
|
||||||
|
|
||||||
|
1. Правки ВМ: nginx + `upload_server.py` → `nginx -t` → reload → перезапуск Flask ВМ.
|
||||||
|
2. Правки managed: `app.py`, `index.html`.
|
||||||
|
3. Bump версии (`VERSION` в `app.py`) + Dockerfile-тег (`naeel/loadtest:v2.1.0`).
|
||||||
|
4. `docker build -t naeel/loadtest:v2.1.0 .` → `docker push`.
|
||||||
|
5. Redeploy пода с env `VM_TOKEN`, `VM_UPLOAD_URL`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Проверка (по шагам)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) приём на ВМ (локально, с --noproxy — иначе прокси Krupski душит)
|
||||||
|
curl --noproxy '*' -F "file=@/tmp/big.bin" https://contracts.kube5s.ru/upload-lt
|
||||||
|
# → {"ok":true,"id":"...","size":...,"url":".../lt-serve/...?t=..."}
|
||||||
|
|
||||||
|
# 2) egress pull из пода (managed)
|
||||||
|
curl "https://http-12.containerk8s.dev.nubes.ru/fetch?url=<url_из_шага_1>"
|
||||||
|
|
||||||
|
# 3) полный сценарий через /upload-vm (из пода)
|
||||||
|
curl -X POST https://http-12.containerk8s.dev.nubes.ru/upload-vm \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"id":"<id>","url":"<url>","name":"big.bin","size":<size>}'
|
||||||
|
# → {"ok":true,"md5":"...","pull_ms":...,"speed_mb_s":...}
|
||||||
|
```
|
||||||
|
|
||||||
|
Тест-файл: `dd if=/dev/urandom of=/tmp/big.bin bs=1M count=45`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Ограничения (помнить при прод-варианте)
|
||||||
|
|
||||||
|
1. **OOM** — решено: pull через `stream=True` + `iter_content` (буфер 256КБ), не `r.content`.
|
||||||
|
2. **1 worker gunicorn** — параллельные pull сериализуются. При росте нагрузки: `--workers 2-4` (следить за памятью).
|
||||||
|
3. **Входной шлюз ~64КБ** остаётся — все НОВЫЕ входы больших данных только через ВМ.
|
||||||
|
4. **CORS** — origin managed-сервиса должен быть в `Access-Control-Allow-Origin` ВМ.
|
||||||
+29
@@ -140,6 +140,35 @@ def messages():
|
|||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/fetch")
|
||||||
|
def fetch_url():
|
||||||
|
"""ВРЕМЕННЫЙ: исходящий GET по URL (проверка egress из кластера).
|
||||||
|
|
||||||
|
Нужен для проверки схемы «Flask сам тянет большой файл с ВМ».
|
||||||
|
Делает GET по переданному url и возвращает HTTP-статус + размер ответа.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# ОБЯЗАТЕЛЬНО для платформы: `python site/app.py` должен стартовать сервер.
|
# ОБЯЗАТЕЛЬНО для платформы: `python site/app.py` должен стартовать сервер.
|
||||||
# debug=False - прод-режим, без reloader (иначе поднимается второй процесс).
|
# debug=False - прод-режим, без reloader (иначе поднимается второй процесс).
|
||||||
|
|||||||
Reference in New Issue
Block a user