Files
drhider/PROBLEM-AND-SOLUTION.md
T

207 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Проблема загрузки файлов: диагностика и решение
**Дата:** 2026-07-13
---
## В чём проблема
При передаче данных через HTTP POST из внешней сети на managed-сервисы, развёрнутые в кластере (Flask, любые другие), периодически возникает пауза длительностью 51 секунда. Проблема затрагивает любой сервис, чей трафик проходит через ingress-контроллер Штурвала при cross-node-форвардинге. Размер передаваемых данных не имеет значения — задержка константна, проявляется даже на 100 KB.
Внутри кластера (pod → ingress pod напрямую) передача происходит мгновенно. Проблема проявляется только при проходе трафика извне через VIP (kube-vip), который распределяет запросы на все ноды кластера.
### Причина (технически)
*Cilium — сетевая подсистема Kubernetes, отвечает за связь между подами на разных нодах. Geneve — протокол туннелирования, который Cilium использует чтобы «обернуть» обычный сетевой пакет для передачи через физическую сеть между серверами.*
```mermaid
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: **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 на этапе развёртывания, до деплоя приложений.