5.8 KiB
Расследование: загрузка файлов через Managed Flask на Shturval
1. Исходная проблема
Приложение: Flask 3.1.3 + flask-sock, загрузка файлов через WebSocket (64KB чанки). Платформа: Shturval 2.12.1 (managed Kubernetes), kube-vip ARP mode, Cilium Geneve.
Симптом: файл 3.7 MB — первые 90% загружаются мгновенно, затем пауза ровно 51 секунда. В ~80% соединений. Оставшиеся ~20% проходят мгновенно.
2. Что проверили (и исключили)
Код приложения
- Пробовали flow control на клиенте (bufferedAmount) — не помогло
- Пробовали server-side ACK (чанк → подтверждение → следующий) — сломалось полностью
- Создали минимальный Flask (13 строк кода, без логики) — та же пауза
- HTTP POST вместо WebSocket — та же пауза для тел >50KB
- Gunicorn с gevent вместо Werkzeug — не влияет
Платформа
- nodeSelector (под на одной ноде с ingress) — не влияет
- Scale ingress до 4 реплик (на всех нодах) — не влияет
- hostNetwork на python-поде (исключает Cilium Geneve) — не влияет
- keepalive=0 к upstream (исключает nginx connection pool) — не влияет
- vip_leaderelection=true на kube-vip (пробовали ранее) — не влияет
- MSS clamping (пробовали ранее) — не влияет
nginx ingress
- proxy_request_buffering on/off — не влияет
- proxy_buffering on/off — не влияет
- ModSecurity в DetectionOnly — не блокирует
- Таймауты: proxy_send_timeout 600s, proxy_read_timeout 600s
3. Ключевой эксперимент: изоляция точки сбоя
Тестировали WebSocket 2MB с разных точек:
| Источник | Цель | Результат |
|---|---|---|
| Pod внутри кластера | ingress pod IP (та же нода, 172.16.1.51) | 5/5 FAST |
| Pod внутри кластера | ingress pod IP (другая нода, 172.16.0.9) | 5/5 FAST |
| Pod внутри кластера | ingress ClusterIP (10.105.13.158) | 5/5 FAST |
| Pod внутри кластера | свой же ClusterIP (без ingress) | 3/3 FAST |
| SSH-хост (5.172.178.213) | VIP (185.247.187.151) | 2/5 FAST, 3/5 SLOW |
| Внешняя машина | VIP | ~15% FAST, ~85% SLOW |
Вывод: внутри кластера всё мгновенно всегда. Пауза только при проходе через VIP извне.
4. Корень проблемы
Kube-vip в ARP-режиме с vip_leaderelection=false (отдельно от svc_election=true):
- Все 4 ноды анонсируют VIP через ARP
- Маршрутизатор выбирает ноду (вероятно, ECMP или последний ARP-ответ)
- Kube-vip проводит leader election для сервиса и назначает VIP на интерфейс ноды-лидера
- Лидером стала нода
iot-naeel-workers-vqphm-bhbvs(10.10.102.4) - На этой ноде не было ingress-пода (ingress был только на 2 из 4 нод)
- Трафик шёл: клиент → VIP → bhbvs → cross-node forward → ingress-нода → python-под
Cross-node форвардинг (через Geneve или IPIP) вызывает TCP stall:
- Первые ~1MB данных проходят в TCP-буфер
- Буфер заполняется → TCP объявляет zero window
- Начинается stop-and-go oscillation: сервер читает порцию → окно приоткрывается → пара пакетов проходит → окно закрывается
- Эффективная скорость падает до ~8 KB/s
- Оставшиеся данные дрейнуются за ~51 секунду
Сервис имел externalTrafficPolicy: Cluster (дефолт), что разрешает kube-vip форвардить трафик на любую ноду, даже без локальных endpoints.
5. Решение
kubectl patch svc -n ingress shturval-ingress-controller-controller \
-p '{"spec":{"externalTrafficPolicy":"Local"}}'
Плюс kubectl scale deploy -n ingress shturval-ingress-controller-controller --replicas=4 — чтобы на каждой ноде был локальный ingress-под.
С Local трафик идёт только на ноды с локальными ingress-подами. Cross-node форвардинг исключён.
Результат: 5/5 FAST (2MB WebSocket через VIP с внешней машины).
6. Что должно быть сделано на стороне платформы
Kube-vip с externalTrafficPolicy: Cluster должен форвардить трафик только на ноды с живыми endpoints сервиса, а не на все ноды подряд. Либо Shturval должен по умолчанию ставить externalTrafficPolicy: Local для LoadBalancer-сервисов, либо kube-vip должен корректно обрабатывать Cluster-режим без TCP stall.
7. Вердикт
Баг платформы. Код приложения корректен. Любое приложение на этом кластере, принимающее >50KB данных через VIP, будет иметь ту же проблему.