Files
loadtest/History/plan-2026-08-23-vm-upload-pull.md
T

301 lines
12 KiB
Markdown
Raw 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) — реализация во 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` ВМ.