Files
drhider/PROBLEM-AND-SOLUTION.md
T
2026-07-13 21:15:48 +04:00

13 KiB
Raw Blame History

Проблема загрузки файлов: диагностика и решение

Дата: 2026-07-13


В чём проблема

При передаче данных через HTTP POST из внешней сети на managed-сервисы, развёрнутые в кластере (Flask, любые другие), периодически возникает пауза длительностью 51 секунда. Проблема затрагивает любой сервис, чей трафик проходит через ingress-контроллер Штурвала при cross-node-форвардинге. Размер передаваемых данных не имеет значения — задержка константна, проявляется даже на 100 KB.

Внутри кластера (pod → ingress pod напрямую) передача происходит мгновенно. Проблема проявляется только при проходе трафика извне через VIP (kube-vip), который распределяет запросы на все ноды кластера.

Причина (технически)

  • Cilium соединяет ноды через Geneve-туннель
  • Geneve добавляет к каждому пакету +50 байт заголовков (Ethernet 14 + IP 20 + UDP 8 + Geneve 8)
  • Cilium ожидает что underlay (физический канал между нодами) = 1500 байт
  • 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: 1с → 3с → 7с → 15с → 25с = 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

Замер выполнен 2026-07-13:

ping -M do -s 1472  →  Message too large (1500 не проходит)
ping -M do -s 1422  →  ok (1450 проходит)

Underlay MTU = 1450 → Geneve-туннель = 1400 → поды = 1400.

Почему это безопасно

  • Понижение 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_geneve device.
  • Установленный вручную 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 убирает cross-node SNAT на обратном пути целиком — двойной инкапсуляции return-трафика (главная причина stall) просто не возникает. Но это меняет всю сетевую конфигурацию, для быстрого фикса — избыточно.

Итоговый вердикт

Путь правильный (фикс корня, а не симптома). Конечное состояние 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:

  1. Замерить underlay MTU между нодами через ping -M do
  2. Передать значение провайдеру/девопсу для включения в конфигурацию кластера
  3. Применить при плановом обслуживании или при создании новых кластеров
  4. Для существующих кластеров — согласовать окно и порядок отката

Для новых кластеров: включить mtu: <измеренное значение> в конфиг Cilium на этапе развёртывания, до деплоя приложений.