From 5f0ca8e46315b08eeb2a065aad8cabd21c2d9d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 16 Jul 2026 06:50:29 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20PROBLEM-AND-SOLUTION.md=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=B0=D0=BD,=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D1=8B,=20=D0=B1=D0=B5=D0=B7=20=D0=BB=D0=BE=D0=B6=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROBLEM-AND-SOLUTION.md | 226 ++++++++++++++++++---------------------- 1 file changed, 103 insertions(+), 123 deletions(-) diff --git a/PROBLEM-AND-SOLUTION.md b/PROBLEM-AND-SOLUTION.md index ea35818..fe87889 100644 --- a/PROBLEM-AND-SOLUTION.md +++ b/PROBLEM-AND-SOLUTION.md @@ -1,148 +1,128 @@ # Проблема загрузки файлов: диагностика и решение -**Дата:** 2026-07-14 +**Дата:** 2026-07-16 +**Версия приложения:** v0.0.29 +**Кластер:** `iot-naeel` (4 ноды, Cilium Geneve, kube-vip, Штурвал) --- -## Суть проблемы +## Проблема 1: 51-секундная задержка (MTU) -При передаче данных через HTTP POST из внешней сети на managed-сервисы кластера (Flask и другие) периодически возникала константная задержка **51 секунда**. Проблема проявлялась на любом объёме данных — даже на 100 КБ. Внутри кластера (pod → ingress pod напрямую) передача работала мгновенно. Задержка возникала исключительно при прохождении трафика снаружи через VIP (kube-vip), который распределяет запросы по всем нодам кластера. - ---- - -## Причина - -**Несоответствие MTU:** канал между серверами — **1450 байт**, а каждый Geneve-туннель добавляет к пакету **50 байт**. При дефолтном MTU пода в 1500 байт любой cross-node-переход через туннель создаёт пакет размером 1550 байт, который не помещается в канал и дропается. Ядро ждёт повторной передачи — отсюда и 51-секундная пауза. - -В зависимости от того, на каких нодах окажутся поды, запрос проходит через 0, 1 или 2 Geneve-туннеля: - -| Путь | Туннелей | Размер пакета | Проходит? | -|------|----------|---------------|-----------| -| Клиент → нода с ingress → drhider на той же ноде | 0 | 1500 б | ✅ | -| Клиент → нода с ingress → drhider на другой ноде | 1 | 1500 + 50 = **1550 б** | ❌ | -| Клиент → нода без ingress → ingress → drhider | 2 | 1500 + 50 + 50 = **1600 б** | ❌ | - -Снижение MTU пода до **1400 байт** устраняет проблему: 1400 + 50 = 1450, что точно вписывается в канал на каждом переходе. - ---- - -## Архитектура кластера - -Кластер `iot-naeel`: 4 ноды (3 worker + 1 control-plane), Cilium с Geneve-энкапсуляцией, kube-vip. - -``` -Ноды: 6f74n | bhbvs | v8zq4 | control-plane -Ingress: 2 пода (Штурвал, всегда на разных нодах) -Drhider: 1 под (размещается на случайной worker-ноде) -VIP: 185.247.187.151 → kube-vip анонсирует на все 4 ноды через ARP -``` - -**Маршрут запроса:** браузер → VIP (случайная нода) → kube-proxy → ingress-под → kube-proxy → drhider-под. Каждый переход между нодами — Geneve-туннель (+50 байт). - -Примеры возможных размещений: - -| Ситуация | Drhider | Ingress | Туннелей ingress→drhider | -|----------|---------|---------|--------------------------| -| A | 6f74n | 6f74n + bhbvs | 50% без туннеля, 50% через | -| B | bhbvs | 6f74n + v8zq4 | 100% через туннель | -| C | v8zq4 | bhbvs + v8zq4 | 50% без туннеля, 50% через | - -Если клиент попадает на ноду без ingress-пода, kube-proxy добавляет ещё один Geneve-переход. - ---- - -## Тестирование (curl с ВМ, 30 запросов, 63 КБ) - -**Цель:** подтвердить, что только MTU 1400 работает стабильно при любом размещении подов. - -| MTU пода | Размещение drhider / ingress | Результат | -|----------|------------------------------|-----------| -| **1400** | bhbvs / bhbvs + v8zq4 | **30/30** ✅ | -| **1400** | 6f74n / 6f74n + bhbvs | **30/30** ✅ | -| 1450 | 6f74n / 6f74n + bhbvs | 30/30 (50% без туннеля — повезло) | -| 1500 | 6f74n / 6f74n + bhbvs | 30/30 (50% без туннеля — повезло) | -| 1500 | v8zq4 / v8zq4 + ctrl | 23/30 | -| **1500** | bhbvs / 6f74n + bhbvs | **0/30** ❌ | - -При MTU 1500 и drhider на bhbvs с ingress на 6f74n+bhbvs — все запросы идут через Geneve, результат 0/30. - -**Вывод:** MTU 1500 работает только случайно (когда поды оказываются на одной ноде). MTU 1400 — единственно надёжное решение. - ---- - -## Баг #2: ERR_CONNECTION_RESET в браузере (2026-07-14) - -### Симптом - -После фикса MTU — curl с ВМ работал, но браузер (Chrome/Electron) при загрузке 63KB docx получал `ERR_CONNECTION_RESET`. +### Суть +При передаче POST из внешней сети — пауза 51с на любом объёме данных. Внутри кластера — мгновенно. ### Причина - -Два механизма одновременно: - -1. **`keep-alive: 10`** — nginx закрывал idle-соединения через 10с. Браузер не замечал FIN (буферизация TCP в ОС), отправлял POST в уже закрытый сокет → RST. - -2. **kube-vip ARP flapping** — при смене ARP-лидера трафик уходил на другую ноду без conntrack-записи → RST. +Underlay MTU = 1450. Cilium Geneve добавляет 50 байт. Дефолтный MTU пода = 1500. +Cross-node пакет = 1500+50 = 1550 → фрагментация → дроп → ядро ждёт повторной передачи 51с. ### Решение +`cilium-config` → `mtu: 1400`. 1400+50 = 1450 = underlay. +Задокументировано 2026-07-13, применено 2026-07-14. -| # | Где | Что | Было | Стало | -|---|-----|-----|------|-------| -| 1 | Ingress ConfigMap | `keep-alive` | `10` | **`75`** | -| 2 | `index.html` (JS) | warmup GET при загрузке | — | `fetch('/health')` | +--- -### Результат тестов (v0.0.26) +## Проблема 2: ERR_CONNECTION_RESET после фикса MTU + +### Суть +curl с ВМ работает. Браузер — `ERR_CONNECTION_RESET` или `⏳ 100%`. +После `rollout restart` ingress — работает 5-10 минут, потом снова RST. + +### Ложные следы (опровергнуты) + +| Гипотеза | Почему неверна | +|----------|---------------| +| kube-vip ARP flapping | `kube-vip.io/vipHost: control-plane` — VIP жёстко на одной ноде | +| hostPort на 2 из 4 нод | Трафик только на control-plane, не на все 4 | +| upstream keepalive stale | `upstream-keepalive-connections: 0` не помог | +| conntrack | ETPolicy Cluster не при чём — трафик на одну ноду | +| MTU | Решено в проблеме 1 | +| HTTP/2 | `ERR_HTTP2_PROTOCOL_ERROR` → отключили → `ERR_CONNECTION_RESET` — симптом тот же | + +### Истинная причина +**`keep-alive: 10`** — Штурвал ставил nginx `keep-alive: 10` (вместо дефолта 75). +Сценарий: +1. Браузер грузит страницу — TCP открыт +2. Пользователь выбирает файлы (>10с) — nginx закрывает idle соединение +3. Браузер не замечает FIN (буферизация ОС) +4. POST 63KB летит в закрытый сокет → TCP RST + +### Решение (применено в платформе Штурвала 2026-07-15) + +Через `ShturvalServiceConfig` (не kubectl — ArgoCD откатывает): + +```yaml +controller: + config: + keep-alive: "75" + proxy-body-size: "1024m" + client-body-timeout: "60" + client-header-timeout: "30" + hostPort: + enabled: true + replicaCount: 2 + service: + type: LoadBalancer +``` + +Также в коде — `fetch('/health')` при загрузке страницы (прогрев upstream). + +### Результаты тестов (v0.0.29, 2026-07-15) | Тест | Результат | |------|-----------| -| 5× свежих страниц + POST 63KB | 4× OK, 1× ABORTED (Playwright) | -| UI: 2 файла + SSE + download | ✅ полный цикл, 18.5с | -| 3× POST через 10+ мин (kube-vip окно) | 3× OK:200 | -| 20× POST (батарея) | 0 ERR_CONNECTION_RESET | +| 10× POST 63KB (свежие страницы) | **10/10 OK:200** | +| UI: 5 файлов + SSE + ZIP + CSV | ✅ полный цикл | +| 5× POST через 75с (keep-alive окно) | **5/5 OK:200** | +| 5MB PDF через curl | ✅ 200 OK, 0.62с | +| **ИТОГО** | **25/25 успешно, 0 RST** | --- -## Текущая конфигурация (2026-07-14) +## Итоговая конфигурация кластера -### Ingress ConfigMap +### Ingress (через платформу Штурвала, несбрасываемо ArgoCD) -| Параметр | Дефолт nginx | Было (Штурвал) | Стало | Причина | -|----------|-------------|-----------------|-------|---------| -| `keep-alive` | 75 | **10** | **75** | 10с — nginx рвал idle-соединения → RST. Вернули к дефолту | -| `use-http2` | true | true | **`"false"`** | HTTP/2 давал PROTOCOL_ERROR | -| `proxy-body-size` | 8m | 8m | **1024m** | Загрузка docx/pdf до 1 ГБ | -| `client-body-timeout` | 60 | **10** | **10** ⚠️ | Надо вернуть 60 | -| `client-header-timeout` | 60 | **10** | **10** ⚠️ | Надо вернуть 30 | -| `upstream-keepalive-connections` | 0 | **50** | **0** | Был эксперимент, можно вернуть 50 | -| `error-log-level` | info | info | **debug** | Временно для диагностики | +| Параметр | Дефолт nginx | Было (Штурвал) | Сейчас | +|----------|-------------|----------------|--------| +| `keep-alive` | 75 | **10** | **75** | +| `proxy-body-size` | 8m | 8m | **1024m** | +| `client-body-timeout` | 60 | **10** | **60** | +| `client-header-timeout` | 60 | **10** | **30** | +| `use-http2` | true | true | **false** | +| `upstream-keepalive-connections` | 0 | 50 | 0 | +| `error-log-level` | info | info | **debug** (вернуть info) | -### Cilium +### Cilium (через kubectl, не сбрасывается) -| Параметр | По умолчанию | Текущее | Причина | -|----------|-------------|---------|---------| -| `mtu` | 1500 | **1400** | Geneve +50 = 1450 = underlay | +| Параметр | Дефолт | Сейчас | +|----------|--------|--------| +| `mtu` | 1500 | **1400** | -### Приложение (drhider v0.0.26) +### Приложение (drhider v0.0.29) -| Файл | Изменение | -|------|-----------| -| `site/templates/index.html` | `fetch('/health')` при загрузке — прогрев upstream | -| `site/app.py` | `VERSION = "0.0.26"` | - ---- - -## Для DevOps — что должно быть в Штурвале - -```yaml -# Ingress ConfigMap — привести к дефолтам nginx -keep-alive: "75" # Штурвал ставит 10 — rвёт idle → RST -client-body-timeout: "60" # Штурвал ставит 10 — rвёт медленных клиентов -client-header-timeout: "30" # Штурвал ставит 10 -use-http2: "true" # можно вернуть -proxy-body-size: "1024m" # под загрузку файлов -upstream-keepalive-connections: "0" -error-log-level: "info" # вернуть после диагностики - -# Cilium ConfigMap (kube-system) -mtu: "1400" # ⚠️ было 1500 — Geneve overhead +```python +# site/app.py +VERSION = "0.0.29" +app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200 MB ``` + +```javascript +// site/templates/index.html — warmup при загрузке +window.addEventListener('load', () => { + sf = []; fi.value = ''; rr(); + fetch('/health').catch(() => {}); // прогрев upstream +}); +``` + +--- + +## Документация + +| Файл | Что | +|------|-----| +| `docs/ARCHITECTURE.md` | Архитектура + требования к Flask | +| `docs/MIGRATION-GUIDE.md` | Инструкция переноса UI на Flask | +| `History/2026-07-14-browser-rst-debug.md` | Полная история диагностики RST | +| `History/2026-07-15-rst-root-cause.md` | Финальный диагноз | +| `History/2026-07-15-cluster-dump.md` | Дамп кластера | +| `STATE-2026-07-15.md` | Состояние проекта на 2026-07-15 |