190 lines
13 KiB
Markdown
190 lines
13 KiB
Markdown
# Проблема загрузки файлов: диагностика и решение
|
||
|
||
**Дата:** 2026-07-13
|
||
|
||
---
|
||
|
||
## В чём проблема
|
||
|
||
При передаче данных через HTTP POST из внешней сети на managed-сервисы, развёрнутые в кластере (Flask, любые другие), периодически возникает пауза длительностью 51 секунда. Проблема затрагивает любой сервис, чей трафик проходит через ingress-контроллер Штурвала при cross-node-форвардинге. Размер передаваемых данных не имеет значения — задержка константна, проявляется даже на 100 KB.
|
||
|
||
Внутри кластера (pod → ingress pod напрямую) передача происходит мгновенно. Проблема проявляется только при проходе трафика извне через VIP (kube-vip), который распределяет запросы на все ноды кластера.
|
||
|
||
### Причина (технически)
|
||
|
||
*Cilium — сетевая подсистема Kubernetes, отвечает за связь между подами на разных нодах. Geneve — протокол туннелирования, который Cilium использует чтобы «обернуть» обычный сетевой пакет для передачи через физическую сеть между серверами.*
|
||
|
||
- 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:
|
||
|
||
```bash
|
||
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-датаплейне).
|
||
|
||
### Верификация после изменения
|
||
|
||
```bash
|
||
# Проверить что 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 на этапе развёртывания, до деплоя приложений.
|