16 KiB
Проблема загрузки файлов: диагностика и решение
Дата: 2026-07-13
В чём проблема
При передаче данных через HTTP POST из внешней сети на managed-сервисы, развёрнутые в кластере (Flask, любые другие), периодически возникает пауза длительностью 51 секунда. Проблема затрагивает любой сервис, чей трафик проходит через ingress-контроллер Штурвала при cross-node-форвардинге. Размер передаваемых данных не имеет значения — задержка константна, проявляется даже на 100 KB.
Внутри кластера (pod → ingress pod напрямую) передача происходит мгновенно. Проблема проявляется только при проходе трафика извне через VIP (kube-vip), который распределяет запросы на все ноды кластера.
Причина (технически)
Cilium — сетевая подсистема Kubernetes, отвечает за связь между подами на разных нодах. Geneve — протокол туннелирования, который Cilium использует чтобы «обернуть» обычный сетевой пакет для передачи через физическую сеть между серверами.
flowchart LR
A["Geneve 50 B"]:::orange --- B["Данные 1450 B"]:::green
B -.-> C["Итого 1500 B"]:::gray
C --> D["Underlay-канал 1450 B"]:::red
D --> E["TCP stall 51с"]:::dark
classDef orange fill:#ff9800,color:#fff,stroke:#e65100
classDef green fill:#4caf50,color:#fff,stroke:#2e7d32
classDef gray fill:#9e9e9e,color:#fff,stroke:#616161
classDef red fill:#f44336,color:#fff,stroke:#c62828
classDef dark fill:#b71c1c,color:#fff
1450 (данные) + 50 (Geneve) = 1500 → underlay 1450 → не проходит → TCP stall 51 секунда.
- Cilium соединяет ноды через Geneve-туннель
- Geneve добавляет к каждому пакету +50 байт заголовков (Ethernet 14 + IP 20 + UDP 8 + Geneve 8)
- Cilium ожидает что underlay (физический канал между нодами) = 1500 байт
Стандартный Ethernet MTU — 1500. Все сетевые интерфейсы, драйверы и протоколы по умолчанию исходят из этого. Cilium опрашивает интерфейс ноды, видит 1500 и принимает за истину. Почему реальный underlay оказался 1450 — может быть связано с инфраструктурой хостинг-провайдера, промежуточным сетевым оборудованием или инкапсуляцией на уровне виртуализации. Без диагностики на стороне провайдера точная причина неизвестна. Факт установлен замером: 1500 не проходит, 1450 проходит.
- Cilium считает: 1500 − 50 = 1450 — такой MTU назначает туннелю и подам
- Реальный underlay (измерен
ping -M do): 1450 байт, а не 1500 - Пакет из пода = 1450 байт
- Пакет + Geneve-заголовок = 1450 + 50 = 1500 байт
- Реальный канал = 1450 байт → пакет 1500 не влазит
- Роутер пытается отправить ICMP «fragmentation needed», но ICMP заблокирован
- Отправитель не знает что пакет не дошёл, ждёт подтверждения
- TCP включает exponential backoff от начального RTO 200ms: 0.2с → 0.4с → 0.8с → 1.6с → 3.2с → 6.4с → 12.8с → 25.6с = 51 секунда
- Через 51 секунду соединение разрывается →
ERR_TIMED_OUT
Почему не всегда
Проблема только при cross-node-форвардинге (kube-vip направляет трафик на ingress-под на другой ноде). При попадании на ту же ноду — всё мгновенно.
Что сделано сейчас (временный обход)
Для обхода проблемы были применены два изменения:
1. externalTrafficPolicy: Local — запрещает cross-node-форвардинг. Трафик обслуживается только на той ноде куда пришёл, без туннеля → stall отсутствует.
2. 4 реплики ingress вместо 2 — у Штурвала по умолчанию 2 пода, а нод 4. С Local трафик на ноду без пода дропается. Выставлено 4 — по одной на каждую ноду.
Проблема: Штурвал управляет ingress через Helm с replicaCount: 2 и при каждом реконсиле (~каждый час) возвращает 4 обратно на 2. Половина запросов начинает падать с ERR_TIMED_OUT. Требуется постоянный ручной контроль.
Корневое решение
Выставить корректный MTU в Cilium:
kubectl edit configmap -n kube-system cilium-config
# mtu: 1450
kubectl rollout restart ds/cilium -n kube-system
kubectl rollout restart deploy -n ingress shturval-ingress-controller-controller
# Вернуть Cluster
kubectl patch svc -n ingress shturval-ingress-controller-controller \
-p '{"spec":{"externalTrafficPolicy":"Cluster"}}'
После этого:
- Cross-node-форвардинг работает без stall
- Ingress остаётся на 2 репликах (стоковое значение Штурвала)
Localне нужен- Ручной scale не нужен
Почему 1450
Физический интерфейс ноды (eth0) показывает MTU 1500 — стандартное значение, выставленное провайдером. Однако между физическими серверами провайдер добавляет собственную инкапсуляцию (VXLAN/GRE, ~50 байт overhead), поэтому реальный path MTU = 1450.
Ваша VM Сеть провайдера Другая VM
eth0: 1500 ──── [VXLAN/GRE +50 overhead] ──── eth0: 1500
реальный path MTU = 1450
| Параметр | Значение | Почему |
|---|---|---|
eth0 (видит VM) |
1500 | Провайдер выставил интерфейс |
| Реальный path MTU | 1450 | Провайдер добавляет инкапсуляцию |
cilium-config mtu: |
1450 | Сообщить Cilium реальный path |
| Pod/tunnel MTU | 1400 | Cilium вычитает 50 на Geneve |
Подтверждено: фикс устранил stall, загрузка файлов идёт без задержки.
Почему это безопасно
- Понижение MTU консервативно: меньше пакет → меньше проблем
- Влияет на весь кластер одинаково (не ломает отдельные сервисы)
- Стандартная практика для туннельных CNI (Geneve, VXLAN, GRE)
- Не трогает логику Штурвала
Полный анализ Опуса 4.8
Главное уточнение
mtu в cilium-config — это MTU underlay-сети (физической сети между нодами), а НЕ туннеля. Cilium сам вычитает 50 байт на Geneve:
mtu: 1450 → tunnel/pod MTU = 1450 − 50 = 1400
По умолчанию Cilium автодетектит underlay 1500 → туннель 1450. Если stall всё равно происходит — значит реальный underlay < 1500. Поэтому нужно измерять, не гадать.
Подводные камни
- ConfigMap сам не применяется. Нужен
kubectl rollout restart ds/cilium -n kube-system— без этого агенты продолжат работать со старым MTU. - Существующие TCP-соединения не поменяют MSS — эффект только на новые соединения после рестарта.
- Нужен рестарт подов приложения (ingress, drhider) — veth-интерфейс пода получает MTU при создании. Для транзитного (ingress) трафика обновления маршрутов ноды обычно достаточно на новых соединениях, но для чистоты — пересоздать.
- Пересоздавать туннели вручную не нужно — рестарт агента переинициализирует
cilium_genevedevice. - Установленный вручную
externalTrafficPolicy: Localнадо вернуть наClusterчерезkubectl patch(ставился не через Helm, сам не откатится).
Риски для других сервисов
mtu — параметр всего кластера, влияет на ВСЕ поды и ВСЕ ноды. Понижение MTU безопасно (меньше = консервативнее). Максимум — небольшой рост числа пакетов / микроскопическая потеря throughput. Функционально ничего не ломает.
Риск в момент раскатки: rollout restart ds/cilium перезапускает агенты по нодам. eBPF-датаплейн переживает рестарт (трафик идёт), но возможны короткие блипы. Лучше делать в окно.
Настоящий риск — только если поставить слишком высокий MTU (проблема останется) или слишком низкий без нужды (лишний оверхед). Поэтому критически важен замер.
iptables как альтернатива
Не равноценен и с Cilium может не сработать. Cilium с eBPF-датаплейном / kube-proxy replacement обходит netfilter FORWARD для трафика подов. Правило в mangle/FORWARD может просто не видеть нужные пакеты.
--clamp-mss-to-pmtu берёт MSS из PMTU исходящего маршрута. Если MTU маршрута неверен — клампинг будет неправильным. Надёжнее явный --set-mss 1400.
На какую ноду: kube-vip раздаёт VIP на все 4 ноды → правило нужно на всех (DaemonSet/node-level).
Вывод: iptables — костыль-fallback, который с Cilium может оказаться нерабочим. Правильный путь — cilium-config, потому что Cilium ставит advmss на маршрутах (тот же MSS clamping, но нативно в eBPF-датаплейне).
Верификация после изменения
# Проверить что Cilium применил MTU
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -i mtu
ip link show | grep -E 'cilium|geneve' # MTU tunnel-девайса
ip route show | grep -E 'advmss|mtu' # advmss на cilium-маршрутах
# Проверить MSS в SYN-пакетах
tcpdump -ni any 'tcp[tcpflags] & tcp-syn != 0' -vv | grep mss
Функционально: файл >5 MB через drhider — без паузы 51с. F5 без ERR_TIMED_OUT.
Более грамотная альтернатива на будущее
Cilium DSR + kubeProxyReplacement позволяет поду отвечать клиенту напрямую, минуя обратный путь через ingress и Geneve-туннель. Это устраняет ответную часть задержки, но не решает корневую проблему (stall возникает на входящем пути, клиент → pod). Для быстрого фикса — избыточно.
Итоговый вердикт
Путь правильный (фикс корня, а не симптома). Конечное состояние Cluster + корректный MTU + replicaCount: 2 — чистое и согласованное: убирает разом и ручной scale, и Local-костыль.
Результаты (тестовый кластер iot-naeel, 2026-07-13)
Изменение применено на личном кластере где отсутствуют активные production-сервисы.
До: externalTrafficPolicy: Local, 4 реплики ingress, ERR_TIMED_OUT при сбросе реплик, TCP-stall 51с.
После: externalTrafficPolicy: Cluster, 2 реплики ingress, MTU 1450.
| Метрика | До | После |
|---|---|---|
| F5 (5 запросов) | 50% ERR_TIMED_OUT | 5/5 OK, <250ms |
| Upload 63 KB | пауза 51с | 0.2-0.3s |
| Upload 6×63 KB | нестабильно | все <0.25s |
| Process 6 файлов | ~90s (со stall) | 32.8s |
| Download ZIP | медленно | 0.23s |
| Download CSV | медленно | 0.12s |
Все костыли (Local, ручной scale) убраны. Кластер работает на заводских настройках Штурвала + одна цифра в конфиге Cilium.
Рекомендации для production-кластеров
На действующих кластерах с активными сервисами НЕ рекомендуется применять kubectl edit configmap + rollout restart ds/cilium вручную. Причины:
rollout restart ds/ciliumперезапускает сетевой слой на всех нодах одновременно (rolling update, но всё же)- eBPF-датаплейн переживает рестарт, но возможны кратковременные блипы
- Ошибка в значении MTU затронет все сервисы кластера
- На production требуется окно обслуживания и мониторинг
Рекомендуемый порядок для production:
- Замерить underlay MTU между нодами через
ping -M do - Передать значение провайдеру/девопсу для включения в конфигурацию кластера
- Применить при плановом обслуживании или при создании новых кластеров
- Для существующих кластеров — согласовать окно и порядок отката
Для новых кластеров: включить mtu: <измеренное значение> в конфиг Cilium на этапе развёртывания, до деплоя приложений.