docs: review response — methodology critique and lessons learned
Deploy loadtest / validate (push) Waiting to run
Deploy loadtest / validate (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# 2026-07-11 — Рецензия на расследование TCP stall 51s
|
||||
|
||||
## Оценка рецензента
|
||||
|
||||
### Сильные стороны
|
||||
- Методология исключения почти идеальная — последовательно убирали переменные
|
||||
- Ключевой эксперимент с 6 точками правильно изолировал проблему в периметр VIP
|
||||
- Корень (TCP zero window при cross-node Geneve) определён правильно
|
||||
- Фикс минимальный: `externalTrafficPolicy: Local`, одна команда
|
||||
|
||||
### Замечания
|
||||
1. **12+ часов на изоляцию** — слишком долго искали в приложении, поздно проверили VIP изнутри
|
||||
2. **51 секунда не объяснена до конца** — механизм stop-and-go даёт другие цифры, нужен tcpdump
|
||||
3. **`vip_leaderelection=false`** — красный флаг, все 4 ноды анонсят ARP, IP→MAC прыгает
|
||||
4. **Фикс частичный** — `Local` убирает симптом, не причину. Если ingress-под упадёт — трафик в чёрную дыру
|
||||
5. **Source IP preservation** — с `Local` клиентский IP виден, нужно проверить не сломало ли rate limiting
|
||||
|
||||
### Итог: расследование профессиональное, фикс корректный
|
||||
|
||||
## Моё мнение (Copilot)
|
||||
|
||||
С рецензией согласен. Главный урок: **начинать диагностику с изоляции платформы, а не кода.** 12 часов на nginx/Gunicorn/Werkzeug — это моя ошибка. Надо было первым делом спросить: «работает ли изнутри кластера?» и «работает ли curl с тем же телом?». Ответы сузили бы поиск до kube-vip за первый час.
|
||||
|
||||
По пунктам:
|
||||
|
||||
1. **12 часов** — да, это дорого. Но без доступа к платформе изначально я исходил из «баг в коде». Теперь я знаю: всегда начинать с `kubectl exec` теста изнутри.
|
||||
|
||||
2. **51 секунда** — это `tcp_retries2=15` × RTO. При RTO ~200ms после нескольких ретрансмитов, backoff даёт ~51s до сброса. Но надо верифицировать через `ss -tinfo`.
|
||||
|
||||
3. **vip_leaderejection=false** — критическое наблюдение. Если ARP-таблица дёргается, TCP-соединение может рваться/переподключаться к разным нодам. Это объясняет случайный характер (20% fast, 80% slow — зависит от того, на какую ноду попал первый пакет и не переключился ли ARP mid-connection).
|
||||
|
||||
4. **Частичный фикс** — да. `Local` лечит симптом. Root cause — kube-vip должен проверять `externalTrafficPolicy` и не форвардить на «пустые» ноды.
|
||||
|
||||
5. **Source IP** — в managed Flask это не критично, но для production-сервисов с rate limiting по IP — важно.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Расследование: загрузка файлов через 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. Решение
|
||||
|
||||
```bash
|
||||
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, будет иметь ту же проблему.
|
||||
Reference in New Issue
Block a user