# Проблема загрузки файлов: диагностика и решение **Дата:** 2026-07-14 --- ## Суть проблемы При передаче данных через 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`. ### Причина Два механизма одновременно: 1. **`keep-alive: 10`** — nginx закрывал idle-соединения через 10с. Браузер не замечал FIN (буферизация TCP в ОС), отправлял POST в уже закрытый сокет → RST. 2. **kube-vip ARP flapping** — при смене ARP-лидера трафик уходил на другую ноду без conntrack-записи → RST. ### Решение | # | Где | Что | Было | Стало | |---|-----|-----|------|-------| | 1 | Ingress ConfigMap | `keep-alive` | `10` | **`75`** | | 2 | `index.html` (JS) | warmup GET при загрузке | — | `fetch('/health')` | ### Результат тестов (v0.0.26) | Тест | Результат | |------|-----------| | 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 | --- ## Текущая конфигурация (2026-07-14) ### Ingress ConfigMap | Параметр | Дефолт 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** | Временно для диагностики | ### Cilium | Параметр | По умолчанию | Текущее | Причина | |----------|-------------|---------|---------| | `mtu` | 1500 | **1400** | Geneve +50 = 1450 = underlay | ### Приложение (drhider v0.0.26) | Файл | Изменение | |------|-----------| | `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 ```