7.8 KiB
Проблема загрузки файлов: диагностика и решение
Дата: 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.
Причина
Два механизма одновременно:
-
keep-alive: 10— nginx закрывал idle-соединения через 10с. Браузер не замечал FIN (буферизация TCP в ОС), отправлял POST в уже закрытый сокет → RST. -
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
| Параметр | По умолчанию | Текущее | Причина |
|---|---|---|---|
keep-alive |
75 | 75 | Было 10 — рвало idle → RST |
use-http2 |
true | "false" |
HTTP/2 давал PROTOCOL_ERROR |
proxy-body-size |
8m | 1024m | Загрузка docx/pdf до 1 ГБ |
client-body-timeout |
60 | 10 ⚠️ | Надо вернуть 60 |
client-header-timeout |
60 | 10 ⚠️ | Надо вернуть 30 |
upstream-keepalive-connections |
0 | 0 | Был 50, эксперимент |
error-log-level |
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 — что должно быть в Штурвале
# Ingress ConfigMap
keep-alive: "75" # ⚠️ было 10 — СРОЧНО исправить
client-body-timeout: "60" # ⚠️ было 10 — СРОЧНО исправить
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