docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes)

This commit is contained in:
“Naeel”
2026-08-24 15:23:31 +03:00
parent 25e4b46e76
commit db7cdbdd84
69 changed files with 0 additions and 0 deletions
@@ -0,0 +1,237 @@
# Диагностика ERR_CONNECTION_RESET в браузере
**Дата:** 2026-07-14
---
## Хронология
### Фаза 1: MTU (решено)
Проблема: 63KB POST из браузера → `ERR_CONNECTION_RESET`. Причина: MTU 1500 + Geneve 50 = 1550 > underlay 1450.
Решение: Cilium MTU = 1400. Проверено curl с ВМ (30/30).
### Фаза 2: HTTP/2
HTTP/2 → `ERR_HTTP2_PROTOCOL_ERROR`. HTTP/1.1 → `ERR_CONNECTION_RESET`.
Решение: `use-http2: "false"`.
### Фаза 3: Поиск корневой причины RST
Баг: браузер → свежая страница → POST 63KB → `ERR_CONNECTION_RESET`.
## Полная матрица тестов
| # | Сценарий | Результат |
|---|----------|-----------|
| 1 | Браузер: свежая страница → POST 63KB | ❌ RST / `⏳ 100%` hang |
| 2 | Браузер: свежая страница → GET (до Flask) → POST 63KB | ✅ 200 OK |
| 3 | Браузер: свежая страница → POST 10B → POST 63KB | ✅ 200 OK |
| 4 | Браузер: свежая страница → GET (nginx сам, 405) → POST 63KB | ❌ RST |
| 5 | `curl` с ВМ (5.172.178.213) → POST 63KB | ✅ всегда <100ms |
| 6 | Сразу после `kubectl rollout restart` ingress → браузер | ✅ работает |
| 7 | Через 5-10 мин после рестарта → браузер | ❌ снова RST |
## Что исключено
| Гипотеза | Статус | Почему |
|----------|--------|--------|
| MTU | ❌ исключено | 1400, curl с ВМ всегда работает |
| HTTP/2 | ❌ исключено | `use-http2: false`, та же проблема |
| XHR vs fetch | ❌ исключено | оба метода падают одинаково |
| `proxy_request_buffering: off` | ❌ исключено | `on` для `location /` |
| `client-body-timeout: 10` | ❌ исключено | 63KB < 1 сек |
| FD/worker-лимиты | ❌ исключено | 1M FD, 16K connections, 62MB mem |
| OOM/Restart | ❌ исключено | 0 рестартов, нет OOMKilled |
| Stale upstream keepalive | ❌ исключено | `keepalive=0` не помогло |
| nginx→Flask (upstream) | ❌ исключено | curl с ВМ доказывает что работает |
## Эксперименты
### Перезапуск ingress временно чинит
После `kubectl rollout restart` — браузер работает. Через 5-10 мин — снова RST. Накопление состояния.
### Ресурсы в норме
```
worker_rlimit_nofile: 1047552 (1M FD)
worker_connections: 16384
worker_processes: 4
Memory: 62 MiB, CPU: 3-7m
Restarts: 0, OOMKilled: нет
```
### `upstream-keepalive-connections: 0` — НЕ фикс
Баг вернулся через 5-10 минут. Keepalive pool не при чём.
## Критическое противоречие
- `curl` с ВМ (тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды.
Разница: клиент (curl vs Chrome) и сетевой путь (локальная сеть ДЦ vs интернет/VPN).
## Текущая конфигурация (2026-07-14)
| Параметр | Значение |
|----------|----------|
| `use-http2` | `"false"` |
| `upstream-keepalive-connections` | `"0"` |
| `error-log-level` | `debug` |
| `proxy-body-size` | `1024m` |
| `client-body-timeout` | `"10"` |
| `keep-alive` | `"10"` |
| Cilium MTU | `1400` |
| `externalTrafficPolicy` | `Cluster` |
| Ingress controller | `1.12.6` (Штурвал) |
---
## Вопрос к Claude (Sonnet/Opus)
### Симптом
Браузер (Chrome/Electron, Windows) → POST 63KB multipart/form-data → `ERR_CONNECTION_RESET` или зависание на `⏳ 100%`. Тело уходит полностью, ответ не приходит — TCP RST.
### Окружение
```
Кластер: bare-metal K8s 1.34.1, 4 ноды (3 worker + 1 control-plane)
CNI: Cilium Geneve, MTU подов = 1400
VIP: kube-vip 185.247.187.151 (ARP на все 4 ноды)
Ingress: nginx-ingress (Штурвал Helm), контроллер 1.12.6
2 реплики, externalTrafficPolicy: Cluster
Приложение: Flask/Waitress, 1 реплика (Штурвал managed)
```
Конфиг ingress:
```yaml
use-http2: "false"
upstream-keepalive-connections: "0"
client-body-timeout: "10"
keep-alive: "10"
worker-processes: 4
worker-rlimit-nofile: 1047552
worker-connections: 16384
proxy-body-size: 1024m
proxy-request-buffering: on # для location /
error-log-level: debug
```
### Критическое противоречие
- `curl` с ВМ (5.172.178.213, тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
- Сразу после рестарта ingress → браузер работает
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды. Разница: клиент (curl vs Chrome) и сетевой путь до VIP (локальная сеть ДЦ vs интернет/VPN).
### Вопросы
1. Почему рестарт ingress помогает на 5-10 минут, а потом браузерные запросы снова получают RST, при том что curl с ВМ продолжает работать?
2. Может ли `externalTrafficPolicy: Cluster` + kube-vip создавать conntrack-записи, которые со временем ломают соединения из интернета (с другими TCP options/MSS) но не из локальной сети?
3. Стоит ли попробовать `externalTrafficPolicy: Local`? Какие риски?
4. Какие ещё эксперименты можно провести без tcpdump (нет доступа к нодам по SSH, нет tcpdump в подах)?
---
## TODO
- [ ] Ответ от Claude
- [ ] `externalTrafficPolicy: Local` — эксперимент
- [ ] Воспроизвести с `curl` из WSL на машине пользователя
- [ ] tcpdump через `kubectl debug` (требует координации)
- [ ] Обновить ingress controller 1.12.6 → актуальный
- [ ] Проверить `client-body-timeout: 60` + `keep-alive: 75`
---
## Ответ Claude (Sonnet) — 2026-07-14
### Два независимых механизма
Симптом объясняется наложением двух механизмов:
### Механизм 1: kube-vip ARP flapping + conntrack asymmetry (основной)
Именно это объясняет 5-10-минутное окно и разницу между browser и curl.
**Как работает с `externalTrafficPolicy: Cluster`:**
```
Browser → VIP → Node N1 (kube-vip leader) → iptables DNAT+SNAT → nginx pod
│
└── conntrack entry создаётся на N1:
(browser_ip:srcport → 185.247.187.151:443)
↔ (N1_ip:eph → nginx_pod:443)
```
Проблема возникает когда kube-vip меняет лидера:
```
kube-vip: N1 → N2 (истёк lease или GARP от N2)
Коммутатор: обновляет ARP-кэш, VIP теперь на N2
Browser (старое HTTP keep-alive соединение):
→ PSH/ACK 63KB → попадает на N2
N2: нет conntrack записи → TCP RST
curl (новое соединение):
→ SYN → попадает на N2
N2: создаёт новую conntrack → всё ОК
```
**Почему 5-10 минут:** время жизни ARP-записи на коммутаторе или интервал переизбрания kube-vip лидера. "ARP на все 4 ноды" — периодические GARP-анонсы могут уходить с разных нод.
**Почему GET-to-Flask чинит последующий POST (тесты #2 vs #4):**
- GET → Flask: создаётся *новое* TCP-соединение (новый SYN) → правильный conntrack на текущей ноде. POST идёт по *тому же* соединению → conntrack жив → OK.
- GET → 405: nginx отвечает сам и закрывает соединение с `Connection: close`. POST создаёт *новое* соединение, но VIP уже на другой ноде → RST.
### Механизм 2: `keepalive_timeout: 10s` — слишком агрессивно
`keep-alive: "10"` в ConfigMap = `keepalive_timeout 10s` в nginx.
```
T=0: Browser загружает страницу (GET /)
T=10: nginx закрывает idle соединение (отправляет FIN)
T=?: Пользователь заполняет форму (>10 сек → почти всегда)
T=X: Browser отправляет POST по "живому" с его точки зрения соединению
→ nginx уже закрыл socket → RST
```
Браузер не всегда замечает FIN немедленно (TCP буферизация ОС). curl — каждый раз новое соединение.
### Почему curl всегда работает
| | Browser | curl |
|---|---|---|
| Соединение | HTTP keep-alive, долгоживущее | Новое для каждого запроса |
| conntrack | Создано при SYN, может устареть | Новый SYN на каждый вызов |
| keepalive | Держит idle >10s | Сразу POST → не ждёт |
| VIP flap | Пакет идёт на новую ноду без conntrack | SYN на новой ноде создаёт conntrack |
### Ключевой инсайт
> Разница не в TCP-опциях (MSS, window size). Разница в **типе пакета**: curl всегда начинает с SYN → conntrack создаётся на правильной ноде. Browser держит соединение между запросами → когда VIP мигрирует, браузерные пакеты (ACK/PSH) приходят на новую ноду без предшествующего SYN → нет conntrack-записи → RST.
### Итоговая таблица причин
| Причина | Механизм | Почему не curl |
|---|---|---|
| kube-vip ARP lease renewal | Смена ноды → conntrack orphan | Новый SYN всегда |
| `keepalive_timeout: 10s` | nginx закрывает idle conn → browser не замечает | Нет idle |
Оба фактора вместе: рестарт сбрасывает состояние, 5-10 минут → kube-vip переизбирается / браузер держит соединение дольше 10s → RST.
### Рекомендации
1. **`keep-alive: "75"`** — исключить механизм #2 (самое простое, безопасное)
2. **`externalTrafficPolicy: Local`** — исключить механизм #1 (риск: потеря source IP для других сервисов)
3. **JS: Connection warmer** — GET на Flask при загрузке страницы (уже доказано тестом #2)
4. **JS: пересоздавать соединение** — использовать `fetch()` без keep-alive (credentials: 'omit' или отдельный subdomain)
+158
View File
@@ -0,0 +1,158 @@
# Cluster Dump — 2026-07-15 17:00 MSK
## 1. НОДЫ (4 шт)
| Нода | Роль | Taints |
|------|------|--------|
| iot-naeel-control-plane-xb699 | control-plane | `NoSchedule:control-plane` |
| iot-naeel-workers-vqphm-6f74n | workers | нет |
| iot-naeel-workers-vqphm-bhbvs | workers | нет |
| iot-naeel-workers-vqphm-v8zq4 | workers | нет |
## 2. INGRESS (shturval-ingress-controller)
### Поды (2 реплики)
| Под | Нода | Возраст |
|-----|------|---------|
| ...86jsp | control-plane-xb699 | ~6ч |
| ...jm4kl | workers-vqphm-v8zq4 | ~6ч |
### Сервис
- Тип: LoadBalancer
- External IP: 185.247.187.151
- `externalTrafficPolicy: Cluster`
- NodePorts: 80:30739, 443:32391
### Ingress ConfigMap
| Параметр | Значение |
|----------|----------|
| keep-alive | **10** |
| client-body-timeout | **10** |
| client-header-timeout | **10** |
| proxy-body-size | **8m** |
| upstream-keepalive-connections | **0** |
| use-http2 | false |
| worker-processes | 4 |
| error-log-level | info |
### Helm
- Chart: shturval-ingress-controller-2.12.1
- Ревизия 9 (сегодня 11:53) — `replicaCount: 2`, hostPort enabled
- Ревизия 8 (сегодня 02:00) — то же самое
- Более старых ревизий нет (почищены)
### Affinity (ingress deploy)
```yaml
preferredDuringScheduling:
- weight: 10 → избегать control-plane
- weight: 80 → предпочитать node-role: ingress
- weight: 50 → предпочитать node-role: infra
```
### События (каждые 51 сек!)
```
Warning SyncLoadBalancerFailed failed to ensure load balancer: no address pools could be found
Normal EnsuringLoadBalancer Ensuring load balancer
```
## 3. KUBE-VIP
### DaemonSet: shturval-vip (4 пода, на ВСЕХ 4 нодах)
- Image: r.shturval.tech/kube-vip:v1.0.0
- ARP mode: `vip_arp: true`
- Election: `svc_election: true`
- Leaderelection: `vip_leaderelection: false`
- Lease: `sht-uservip-cp-lock`
### Cloud Provider: shturval-vip-provider (1 под, на bhbvs)
- Image: r.shturval.tech/kubevip/kube-vip-cloud-provider:v0.0.12
- Ищет ConfigMap: `kubevip` в неймспейсе `kube-system`
### ❌ ConfigMap `kubevip` в `kube-system` — **ПУСТОЙ!**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: kubevip
namespace: kube-system
data: {} # <-- НЕТ ДАННЫХ
```
**Это причина ошибки «no address pools could be found»!**
## 4. DRHIDER (pythonk8s)
### Деплой
- 1 реплика
- Ревизия: 49
- Инит-контейнер: клонирует из Git, потом pip install + python app.py
- Ресурсы: 500m CPU, 1Gi RAM
- Проба: TCP :5000
### ❌ PodAffinity (добавлен сегодня 16:46, непрошеный)
```yaml
requiredDuringScheduling:
podAffinity → ingress pod на той же ноде
```
### Сервис
- ClusterIP: 10.102.171.63
- Port 80 → targetPort 5000
- `internalTrafficPolicy: Cluster`
### Ingress (drhider)
- Host: drhider.pythonk8s.dev.nubes.ru
- Аннотации: proxy-body-size=1024m, connect=120s, read/send=600s
- TLS: letsencrypt-prod
### Поды
| Под | Нода | Возраст |
|-----|------|---------|
| pythonk8s-6698c6c78-dkwsq | workers-vqphm-v8zq4 | ~15 мин |
Drhider на одной ноде с ingress-подом jm4kl.
## 5. ВСЕ INGRESS-РЕСУРСЫ
| Хост | Неймспейс | Возраст |
|------|-----------|---------|
| drhider.pythonk8s.dev.nubes.ru | 20a75175-... | 4д |
| loadtest.pythonk8s.dev.nubes.ru | 9039a501-... | 5д |
| contractor.pythonk8s.dev.nubes.ru | b4523aba-... | 7ч |
| capire.kube5s.ru | default | 54д |
| grafana.kube5s.ru | grafana | 66д |
| keycloak.k8c.ru | keycloak | 96д |
| qu.kube5s.ru | shared-sqs | 93д |
| iot.kube5s.ru | sless | 94д |
| terra.k8c.ru | terra | 92д |
## 6. CILIUM
4 пода (по одному на ноду), все Running. MTU был изменён на 1400 (по STATE).
## 7. КЛЮЧЕВЫЕ НАХОДКИ
### 🔴 ConfigMap `kubevip` пустой
kube-vip-cloud-provider не может найти address pools → каждые 51 сек ошибка SyncLoadBalancerFailed. При этом `185.247.187.151` работает через ARP-mode kube-vip DaemonSet (не через cloud provider). Вероятно, это не fatal, но указывает на недонастроенную интеграцию.
### 🔴 Ingress ConfigMap отличается от ожидаемого
- keep-alive: 10 (должен быть 75)
- client-body-timeout: 10 (должен быть 60)
- client-header-timeout: 10 (должен быть 30)
- proxy-body-size: 8m (должен быть 1024m)
Но для drhider это переопределено в аннотациях ingress-ресурса.
### 🟡 Ingress 2 реплики на 4 нодах с kube-vip
kube-vip анонсирует 185.247.187.151 на ВСЕХ 4 нодах, но ingress-поды только на 2. При `externalTrafficPolicy: Cluster` трафик на ноды без ingress: SNAT → кросс-нода → риск conntrack RST.
### 🟡 Helm: 2 апгрейда сегодня
Ревизии 8 (02:00) и 9 (11:53) сегодня. Кто-то обновлял ingress. `replicaCount: 2` в обеих.
### 🟢 Аннотации drhider ingress — хорошие
proxy-body-size=1024m, connect=120s, read/send=600s. Переопределяют дефолты ConfigMap.
### 🟢 Cilium — стабильный
4 пода, все Running, MTU 1400.
+204
View File
@@ -0,0 +1,204 @@
# DrHider — итоговая диагностика ERR_CONNECTION_RESET
**Дата:** 2026-07-15 (финал)
**Версия:** v0.0.29
---
## Резюме (моё мнение)
**`keep-alive: 10` — единственная причина ERR_CONNECTION_RESET.** Всё остальное (conntrack, hostPort, ARP flapping, svc_election) — шум, который мы искали 2 дня.
### Почему это так
VIP `185.247.187.151` жёстко привязан к control-plane-xb699 через аннотацию `kube-vip.io/vipHost`. Никакого flapping нет — трафик всегда приходит на одну ноду. Второй ingress-под на v8zq4 — избыточен, но не мешает.
Сценарий:
1. Браузер загружает страницу — opens TCP
2. Пользователь выбирает файлы — проходит >10 сек
3. Nginx (keep-alive=10) закрывает idle соединение
4. Браузер не замечает FIN (буферизация ОС)
5. POST 63KB летит в мёртвый сокет → RST
### Почему предыдущие гипотезы неверны
| Гипотеза | Опровержение |
|----------|-------------|
| kube-vip ARP flapping | `vipHost` фиксирует VIP на одной ноде. Lease мёртв — плевать |
| hostPort на 2 из 4 нод | Трафик не на все 4 ноды, а только на control-plane |
| upstream keepalive stale | `keepalive=0` не помог |
| MTU | Решено ещё 14-го, не при чём |
| conntrack | ETPolicy Cluster не при чём — трафик на одну ноду |
### Что подтверждает keep-alive гипотезу
| Тест | Объяснение |
|------|-----------|
| Сразу после рестарта ingress → OK | Все соединения свежие |
| Через 5-10 мин → RST | Соединения постарели >10с |
| GET/health → POST → OK | Новое живое соединение |
| curl с ВМ → всегда OK | curl открывает новый SYN каждый раз |
| `keep-alive: 75` + JS warmup → OK на 10+ мин | Увеличили окно, warmup греет |
## Хронология
| Дата | Событие |
|------|---------|
| 2026-07-11 | Исходный деплой |
| 2026-07-14 (день) | MTU fix (Cilium 1400) — решило 51с задержку |
| 2026-07-14 (вечер) | RST диагностика: keep-alive 75 + JS warmup — частично |
| 2026-07-15 (11:53) | Helm ревизия 9 — сброс ConfigMap на дефолты (keep-alive:10) |
| 2026-07-15 | podAffinity `required` на drhider (ручной kubectl edit) |
| 2026-07-15 (16:00) | Полная диагностика: hostPort vs cloud LB |
---
## Две независимые проблемы
### Проблема #1: MTU (решена 2026-07-14)
**Симптом:** 51с задержка при POST любого размера через внешний VIP.
**Причина:** Geneve +50, underlay 1450, дефолт 1500 → фрагментация/дроп.
**Решение:** Cilium `mtu: 1400`.
### Проблема #2: ERR_CONNECTION_RESET (хост порт, решена 2026-07-15)
**Симптом:** Браузер получает TCP RST при POST 63KB через 5-10 мин после рестарта ingress.
**Ранее считалось:** keep-alive 10с или kube-vip conntrack.
**Реальная причина:** Штурвал балансирует трафик на **все 4 ноды**, но ingress стоит `replicas: 2` с **hostPort** — порт 80/443 открыт только на 2 нодах.
```
Browser → cloud LB → любая из 4 нод
├── нода с ingress-подом → hostPort 80 → nginx → ✅
└── нода БЕЗ ingress-пода → порт 80 не слушается → RST ❌
```
**50% запросов попадает на пустую ноду → RST.**
---
## Подтверждающие данные
### Helm ревизии (дамп)
| Ревизия | Время | replicaCount | Другие изменения |
|---------|-------|-------------|------------------|
| 1 (деплой) | 2026-07-11 | 2 | — |
| 8 | 2026-07-15 02:00 | **2** | — |
| 9 | 2026-07-15 11:53 | **2** | — |
Helm **не менял** replicaCount. Всегда 2. Но ConfigMap сбрасывал на дефолты (keep-alive: 10, proxy-body-size: 8m и т.д.).
### Ingress ConfigMap (последствия Helm upgrade)
```yaml
# Было (ручные правки 2026-07-14) → Стало (после ревизии 9)
keep-alive: "75" → "10" # вернулось на дефолт Штурвала
proxy-body-size: "1024m" → "8m" # тоже сброшено
error-log-level: debug → info # сброшено
upstream-keepalive-connections: "0" → сохранилось
```
### kube-vip svc_election — мёртв
```yaml
# Lease ingress/kubevip-shturval-ingress-controller-controller
holderIdentity: "" # пусто — никто не держит
leaseDurationSeconds: 1 # аномально короткий (норма 15-30)
renewTime: 2026-07-11T14:55:28Z # 4 дня назад не обновлялся
leaseTransitions: 17 # но было 17 переходов
```
svc_election никогда не работал. VIP назначен через `ipMode: VIP` в статусе сервиса, работает на уровне облака (BGP/маршрутизация).
### hostPort
```yaml
# В шаблоне пода ingress (Helm template)
hostPort: 80
hostPort: 443
```
Включён в ревизиях 8 и 9. hostPort = порт слушается ТОЛЬКО на нодах где стоит под.
### podAffinity на drhider — временный костыль
```json
{
"podAffinity": {
"requiredDuringSchedulingIgnoredDuringExecution": [{
"labelSelector": {"matchLabels": {"app.kubernetes.io/name": "shturval-ingress-controller"}},
"namespaceSelector": {"matchLabels": {"name": "ingress"}},
"topologyKey": "kubernetes.io/hostname"
}]
}
}
```
Добавлен через `kubectl edit`, НЕ через Helm. Привязывает drhider к ноде с ingress-подом. Это НЕ лечит RST (RST на уровне VIP→нода, не на уровне нода→drhider). При следующем `helm upgrade` — исчезнет.
### Kube-vip DaemonSet — не участвует
```yaml
vip_arp: true # ARP включён
vip_leaderelection: false # нет CP election
svc_election: true # должен быть — но lease мёртв
```
Даемоны не логгируют VIP `185.247.187.151`. Трафик распределяет облако, не kube-vip.
---
## Решение
**Единственное полное решение:** ingress-под на каждой ноде куда приходит трафик.
### Вариант 1 (рекомендуемый): увеличить replicas
```yaml
# Helm values
shturval-ingress-controller:
replicaCount: 4
```
Или выяснить у DevOps сколько нод в LB-пуле Штурвала и поставить `replicaCount = число нод`.
### Вариант 2 (если балансировка на все worker'ы): DaemonSet
```yaml
# Helm values
shturval-ingress-controller:
kind: DaemonSet
```
---
## Что делать сейчас (ручной воркараунд)
### 1. Починить ConfigMap (срочно)
```bash
kubectl patch configmap -n ingress shturval-ingress-controller-controller --type merge \
-p '{"data":{"keep-alive":"75","proxy-body-size":"1024m","client-body-timeout":"60","client-header-timeout":"30"}}'
```
### 2. Масштабировать ingress (временный фикс)
```bash
kubectl scale deploy -n ingress shturval-ingress-controller-controller --replicas=4
```
Поды раскидаются по всем 4 нодам → hostPort на всех → 0% RST.
⚠️ **Предупреждение:** после следующего `helm upgrade`:
- ConfigMap сбросится (нужно править Helm values)
- replicas вернётся на 2
- podAffinity на drhider исчезнет
---
## Вопросы к DevOps
1. **Сколько нод в LB-пуле Штурвала?** (на какие ноды облако направляет трафик ingress?)
2. **Можно ли увеличить `replicaCount` до числа нод?**
3. **Как правильно изменить Helm values для ingress?** (чтобы ConfigMap не сбрасывался при upgrade)
@@ -0,0 +1,37 @@
# v0.0.46 — удалён process_names + git push HTTP/2 проблема — 2026-08-19
**Дата:** 2026-08-19
**Версия:** 0.0.45 → 0.0.46
---
## Удалён тестовый API process_names
- `process_names` (обфускация файлов по именам из TEST_INPUT_DIR) **удалён**.
- Причина: сервер не может читать локальные файлы клиента (нет доступа к его
машине). Файлы в поде — неудобно. API бесполезен.
- Удалены: endpoint, `TEST_INPUT_DIR`/`_TEST_API_ENABLED`, `import os`, History-файл.
- Версия 0.0.46.
## Находка: git push к gitea зависал (HTTP/2)
**Симптом:** `git push origin master` висел бесконечно (таймауты), хотя
`git ls-remote` и `curl` к gitea работали (200, быстро).
**Причина:** git использовал **HTTP/2** для отправки pack; шлюз
`gitea.services.ngcloud.ru` блокировал передачу данных по HTTP/2
(та же проблема, что была в drhider с HTTP/2 — ERR_HTTP2_PROTOCOL_ERROR).
**Решение:** push через HTTP/1.1 мгновенно прошёл:
```bash
git -c http.version=HTTP/1.1 push origin master
# и закреплено в конфиге:
git config http.version HTTP/1.1
```
**Проверка:** `8d7a7df..d2f2bd3 master -> master`, синхронизировано.
## Полезно
- На этой машине/шлюзе для git (и возможно других HTTP/2 клиентов) использовать
HTTP/1.1.
- git config `http.version HTTP/1.1` уже установлен.
@@ -0,0 +1,124 @@
# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider
_Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._
_Статус: РЕАЛИЗОВАНО 2026-08-21 (v0.0.58), см. `2026-08-21-vm-upload-implemented.md`._
---
## Цель
drhider (Managed Flask, Штурвал, `drhider.pythonk8s.dev.nubes.ru`) получает файлы через
`POST /api/upload` (multipart). Входной шлюз managed-кластера обрывает тела >~64КБ
(HTTP 000 ~10с). Решение — загружать файл на ВМ напрямую, Flask тянет сам (egress).
## Директива (обязательно)
**ВСЕ файловые операции загрузки в Flask (из браузера или вообще снаружи) — ТОЛЬКО через ВМ.**
Никаких прямых `POST /api/upload` с телом файла во Flask.
## Среда (уточнено 2026-08-21)
- `iot-naeel` — СОБСТВЕННЫЙ кластер, параметры/аннотации правились через kubectl. Пока
тестируем/смотрим поведение на нём (лимит 64КБ там может не воспроизводиться).
- В дальнейшем деплой — на ОБЫЧНЫЙ managed-кластер (лимит 64КБ будет реальным).
- ВМ — ОДНА (`5.172.178.213`, `contracts.kube5s.ru`), та же, что для loadtest. Location
`/drhider-upload/` добавляется в существующий nginx рядом с `/lt-serve/`.
- Вывод: VM-путь строим СЕЙЧАС, независимо от поведения iot-naeel — прод всё равно на обычном.
## Текущий поток
`uploadFiles()` (index.html ~413): по одному файлу в цикле → `FormData` → `POST /api/upload`.
Бэк: `MAX_CONTENT_LENGTH=200МБ`, сессия 500МБ. HTTP-клиент — **httpx** (не requests).
## Целевой поток
```
Браузер ──(PUT, файл целиком)──▶ ВМ nginx (без лимита 64КБ)
│ └─ получил URL + токен
│
└──▶ Flask POST /api/upload_refs (JSON: [{name,size,url}], <64КБ)
│
▼
Flask egress GET с ВМ ──▶ читает файл ──▶ add_file() ──▶ дальше как сейчас
```
## Изменения (4 места)
### 1. ВМ `5.172.178.213`, `nginx-contracts.conf` — приём загрузки
Новый location (рядом с `/lt-serve/`), приём через PUT + статическая отдача:
```nginx
location /drhider-upload/ {
alias /var/www/drhider-upload/;
dav_methods PUT;
create_full_put_path on;
client_max_body_size 1024m;
# CORS: браузер с drhider.pythonk8s.dev.nubes.ru шлёт PUT (не simple → preflight)
add_header Access-Control-Allow-Origin https://drhider.pythonk8s.dev.nubes.ru always;
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, DELETE' always;
add_header Access-Control-Allow-Headers 'Content-Type' always;
if ($request_method = OPTIONS) { return 204; }
}
```
- Каталог `/var/www/drhider-upload/` (владелец `www-data`), НЕ `/home/naeel` (403).
- Порядок: backup → правка → `nginx -t` → `systemctl reload nginx`.
### 2. Flask — новый endpoint pull (`site/routes/api_bp.py`)
```python
@api_bp.route("/upload_refs", methods=["POST"])
def upload_refs():
data = request.get_json(silent=True) or {}
sid = data.get("session") or create_session()
refs = data.get("files") or []
if not refs:
return jsonify({"ok": False, "error": "No files"}), 400
added = 0
with httpx.Client(timeout=120, follow_redirects=True) as client:
for ref in refs:
name, url = ref.get("name"), ref.get("url")
if not name or not url:
continue
content = b"".join(client.stream("GET", url).iter_bytes()) # egress, по частям
if not add_file(sid, name, content):
return jsonify({"ok": False, "error": "Session not found"}), 404
added += 1
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
```
- Импорт `httpx` (уже в requirements.txt) и `file_count` из session.
- `stream(...).iter_bytes()` — НЕ `content` целиком (OOM при 100МБ+).
- Обработка ошибок egress (таймаут/403/404) → вернуть 502 с именем файла.
### 3. Фронт (`site/templates/index.html`, `uploadFiles()`)
Разбить «Фазу 1» на два шага:
- **Шаг 1a:** каждый файл → `fetch(<ВМ>/drhider-upload/<token>/<name>, {method:'PUT', body:file})`,
собирать `[{name, size, url}]`. Прогресс — `xhr.upload.onprogress` заменить на `fetch` +
`ReadableStream` (или оставить XHR, но на PUT к ВМ).
- **Шаг 1b:** один `POST /api/upload_refs` с JSON `{session, files:[...]}` (<64КБ).
- Токен: `crypto.randomUUID()`. URL ВМ — константа/`data-атрибут`.
- Показывать прогресс «загрузка на ВМ» отдельно от «загрузка в Flask».
### 4. `requirements.txt` — без изменений (httpx уже есть).
## Безопасность и жизненный цикл
- Токен в пути URL — только `crypto.randomUUID()`, не переиспользуется.
- Flask после `add_file` делает `DELETE` на URL ВМ (или ВМ чистит по TTL — cron/systemd-timer
`find /var/www/drhider-upload -mmin +30 -delete`).
- ВМ отдаёт файл только по валидному URL с токеном (нет открытого листинга: `autoindex off`).
## Грабли (из pattern-дока + drhider)
- CORS: PUT — не simple-метод → браузер шлёт OPTIONS preflight; nginx обязан отвечать 204.
- `site/` конфликтует со stdlib `site.py` (для loadtest; у drhider — Штурвал, без gunicorn).
- egress через `httpx` с `stream`, timeout 120; большие ОТВЕТЫ проходят (проверено 50МБ).
- OOM: не читать файл в память целиком; сессия 500МБ уже ограничивает.
- Локальные curl на Krupski идут через прокси `172.17.192.1:10808` → для теста ВМ `--noproxy '*'`.
## Порядок работ + проверка
1. ВМ nginx (location + каталог + CORS) → проверить `curl --noproxy '*' -X PUT`.
2. Flask `/api/upload_refs` → проверить `python3 -c "import py_compile; py_compile.compile(...)"`.
3. Фронт `uploadFiles()` → `node -c` нет (это EJS/HTML+JS внутри шаблона) — ручная проверка.
4. Сквозной тест: файл 10МБ → ВМ → Flask → обфускация → ZIP.
5. Commit + push (правило: после правки + bump версии `VERSION` в `site/app.py`).
## Решения (закрыты)
- Кластер: сейчас `iot-naeel` (тест), прод — обычный managed. VM-путь обязателен.
- ВМ: одна, `contracts.kube5s.ru` (`5.172.178.213`), общая с loadtest — добавляем location.
@@ -0,0 +1,75 @@
# ПЛАН: универсальный файловый сервис на ВМ (API)
_Дата: 2026-08-21. Статус: РЕАЛИЗОВАНО И ЗАДЕПЛОЕНО (v1.0.0), см. `README.md` сервиса `/home/naeel/nubes/vmfiles/` и `2026-08-21-vm-upload-implemented.md`._
## Цель
Один внутренний (НЕ публичный) сервис на ВМ `5.172.178.213` (`contracts.kube5s.ru`)
для приёма/временного хранения/выдачи файлов. Потребители: **drhider**, **contracts**,
**SQS IoT**. Заменяет сырой nginx-dav `/drhider-upload/` на задокументированный,
версионированный API, чтобы любому сервису было ясно, как им пользоваться.
## Потребители и режимы
- **drhider** — браузер (PUT файла) + сервер (pull). Нужен CORS для браузера.
- **contracts** — сервер-к-серверу, много файлов из локали.
- **SQS IoT** — сервер-к-серверу.
## Технология
**FastAPI + uvicorn** (Python уже есть на ВМ).
Почему FastAPI: автогенерация OpenAPI/Swagger (`/docs`) → «как юзать» видно из коробки;
нативный streaming для больших тел.
## API v1 (контракт)
| Метод | Путь | Вход | Ответ |
|---|---|---|---|
| `POST` | `/api/v1/files` | тело=файл (raw или multipart `file`), header `X-App-Key` | `201 {"id","url","size","expires_at"}` |
| `GET` | `/api/v1/files/{id}` | header `X-App-Key` | байты файла |
| `DELETE` | `/api/v1/files/{id}` | header `X-App-Key` | `204` |
| `GET` | `/api/v1/health` | — | `{"ok":true,"version":...}` |
| `GET` | `/api/v1/docs` | — | Swagger UI (авто) |
Единые ошибки: `{"error": "...", "code": "unauthorized|not_found|too_large|expired|quota_exceeded"}`.
## Авторизация
Заголовок `X-App-Key`, по одному секрету на приложение. Ключи — в конфиге на ВМ
(файл `apps.yml`/`.env`): `drhider`, `contracts`, `sqs-iot`. Без валидного ключа — `401`.
## Хранилище и метаданные
- Файлы: `/var/lib/vmfiles/` (или `/var/www/vmfiles/`, владелец — пользователь сервиса).
- Метаданные: SQLite (`id, app, name, size, created_at, expires_at`).
- id — серверный UUID (или клиентский токен — см. «открытые вопросы»).
## Лимиты и TTL
- `max_file_size` (по умолчанию 1024m) и `quota` на приложение.
- TTL по умолчанию 30 мин (на приложение можно переопределить).
- Чистка — встроенная фоновая задача сервиса (вместо текущего cron-`find`).
## Деплой на ВМ
1. Код сервиса — в отдельной папке на ВМ (или отдельный репозиторий).
2. systemd-юнит `vmfiles.service` → `uvicorn app:app --host 127.0.0.1 --port 8769`.
3. nginx: новый `location /api/v1/ { proxy_pass http://127.0.0.1:8769; ... }`
+ `proxy_request_buffering off` (стримить большие тела) + `client_max_body_size 1024m`.
CORS — отдать сервису (FastAPI), не nginx.
4. Проверка: `nginx -t` → reload.
## Миграция drhider
- Пока сервис поднимается — `/drhider-upload/` оставить как есть (не ломать текущее).
- После готовности — перевести `uploadFiles()` и `/api/upload_refs` на `/api/v1/files`,
убрать dav-location и cron-`find`.
## Порядок работ
1. Скелет FastAPI + health + `/docs`.
2. `POST/GET/DELETE /files` + SQLite-метаданные + TTL-чистка.
3. `X-App-Key` авторизация + квоты.
4. systemd + nginx `proxy_pass`.
5. Тесты: curl (raw + multipart), большие файлы, expiry, quota.
6. Перевести drhider на новый API, выпилить старый dav.
7. Документ-спека (`README`/`API.md`) + примеры для contracts/SQS.
## Открытые вопросы (до «делай»)
1. Язык: FastAPI (рекомендую) или Flask?
2. id файла: сервер генерирует (POST) или клиент приносит токен (PUT)? Влияет на браузерный поток drhider.
3. Загрузка: только raw-тело или поддерживать multipart тоже?
4. Где живёт код сервиса — отдельный репозиторий на gitea или папка на ВМ?
5. Ключи приложений — придумать сейчас или сервис сгенерирует при первом старте?
@@ -0,0 +1,43 @@
# ВМ-загрузка в drhider — реализовано (v0.0.58, 2026-08-21)
_Реализация паттерна «ВМ-буфер + pull» по плану `2026-08-21-pattern-vm-buffer-pull-plan.md`._
_Снимок до изменений: ветка `save-pre-vm-upload-2026-08-21`._
## Что сделано
1. **Flask** (`site/routes/api_bp.py`) — новый `POST /api/upload_refs`:
принимает JSON `{session, files:[{name,size,url}]}`, тянет каждый файл с ВМ
ИСХОДЯЩИМ GET (httpx, stream по частям), кладёт в сессию `add_file()`,
после успешного pull делает DELETE с ВМ. Ошибки egress → 502.
2. **Фронт** (`site/templates/index.html`, `uploadFiles()`) — Фаза 1 переделана:
- 1a: каждый файл → `PUT` на ВМ `https://contracts.kube5s.ru/drhider-upload/<токен>_<k>`
(XHR, прогресс по-прежнему в строке), собирает refs.
- 1b: один маленький `POST /api/upload_refs` (<64КБ).
- Токен — `crypto.randomUUID()`. Константа `VM_UPLOAD_URL`.
3. **ВМ** (`5.172.178.213`) — nginx `nginx-contracts.conf`:
- `location /drhider-upload/` → `alias /var/www/drhider-upload/`,
`dav_methods PUT DELETE`, `create_full_put_path on`, `client_max_body_size 1024m`,
CORS-заголовки для `drhider.pythonk8s.dev.nubes.ru`, OPTIONS → 204.
- Каталог `/var/www/drhider-upload/` (www-data).
- Backup: `/tmp/nginx-contracts.conf.bak.20260821`.
- TTL-чистка в root cron: `*/5 * * * * find /var/www/drhider-upload -type f -mmin +30 -delete`.
4. **Версия** — `site/app.py`: 0.0.57 → 0.0.58.
## Отклонение от плана
- URL файла на ВМ — БЕЗ имени: `.../drhider-upload/<токен>_<k>` (имя — только в метаданных).
Иначе кириллица/пробелы в имени ломали бы URL.
## Тесты (2026-08-21)
- ВМ: PUT 10МБ → 201 (1.0с); GET → 200, 10485760B; OPTIONS preflight → 204 с CORS;
DELETE → 204; GET после DELETE → 404.
- Сквозной: PUT на ВМ → `POST /api/upload_refs` → 200, файл в сессии (10485760B),
файл с ВМ удалён (404).
- `py_compile` api_bp/app — OK; `node --check` JS из index.html — OK.
## Замечания
- ГРАБЛИ: на Krupski `dd` алиасится на `docker compose down` — для генерации файлов
использовать `head -c N /dev/urandom > file`.
- Локальные curl/httpx к contracts.kube5s.ru — через прокси (`172.17.192.1:10808`):
для тестов `--noproxy '*'` / `NO_PROXY=contracts.kube5s.ru`.
- Прод-безопасность (позже): токены без TTL-времени в самом URL, авторизация на ВМ
(сейчас ключ — случайный UUID, файл живёт ≤30 мин по cron).