Files
drhider/PROBLEM-AND-SOLUTION.md
T
2026-07-14 22:02:25 +04:00

149 lines
8.1 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-14
---
## Суть проблемы
При передаче данных через HTTP POST из внешней сети на managed-сервисы кластера (Flask и другие) периодически возникала константная задержка **51 секунда**. Проблема проявлялась на любом объёме данных — даже на 100 КБ. Внутри кластера (pod → ingress pod напрямую) передача работала мгновенно. Задержка возникала исключительно при прохождении трафика снаружи через VIP (kube-vip), который распределяет запросы по всем нодам кластера.
---
## Причина
**Несоответствие MTU:** канал между серверами — **1450 байт**, а каждый Geneve-туннель добавляет к пакету **50 байт**. При дефолтном MTU пода в 1500 байт любой cross-node-переход через туннель создаёт пакет размером 1550 байт, который не помещается в канал и дропается. Ядро ждёт повторной передачи — отсюда и 51-секундная пауза.
В зависимости от того, на каких нодах окажутся поды, запрос проходит через 0, 1 или 2 Geneve-туннеля:
| Путь | Туннелей | Размер пакета | Проходит? |
|------|----------|---------------|-----------|
| Клиент → нода с ingress → drhider на той же ноде | 0 | 1500 б | ✅ |
| Клиент → нода с ingress → drhider на другой ноде | 1 | 1500 + 50 = **1550 б** | ❌ |
| Клиент → нода без ingress → ingress → drhider | 2 | 1500 + 50 + 50 = **1600 б** | ❌ |
Снижение MTU пода до **1400 байт** устраняет проблему: 1400 + 50 = 1450, что точно вписывается в канал на каждом переходе.
---
## Архитектура кластера
Кластер `iot-naeel`: 4 ноды (3 worker + 1 control-plane), Cilium с Geneve-энкапсуляцией, kube-vip.
```
Ноды: 6f74n | bhbvs | v8zq4 | control-plane
Ingress: 2 пода (Штурвал, всегда на разных нодах)
Drhider: 1 под (размещается на случайной worker-ноде)
VIP: 185.247.187.151 → kube-vip анонсирует на все 4 ноды через ARP
```
**Маршрут запроса:** браузер → VIP (случайная нода) → kube-proxy → ingress-под → kube-proxy → drhider-под. Каждый переход между нодами — Geneve-туннель (+50 байт).
Примеры возможных размещений:
| Ситуация | Drhider | Ingress | Туннелей ingress→drhider |
|----------|---------|---------|--------------------------|
| A | 6f74n | 6f74n + bhbvs | 50% без туннеля, 50% через |
| B | bhbvs | 6f74n + v8zq4 | 100% через туннель |
| C | v8zq4 | bhbvs + v8zq4 | 50% без туннеля, 50% через |
Если клиент попадает на ноду без ingress-пода, kube-proxy добавляет ещё один Geneve-переход.
---
## Тестирование (curl с ВМ, 30 запросов, 63 КБ)
**Цель:** подтвердить, что только MTU 1400 работает стабильно при любом размещении подов.
| MTU пода | Размещение drhider / ingress | Результат |
|----------|------------------------------|-----------|
| **1400** | bhbvs / bhbvs + v8zq4 | **30/30** ✅ |
| **1400** | 6f74n / 6f74n + bhbvs | **30/30** ✅ |
| 1450 | 6f74n / 6f74n + bhbvs | 30/30 (50% без туннеля — повезло) |
| 1500 | 6f74n / 6f74n + bhbvs | 30/30 (50% без туннеля — повезло) |
| 1500 | v8zq4 / v8zq4 + ctrl | 23/30 |
| **1500** | bhbvs / 6f74n + bhbvs | **0/30** ❌ |
При MTU 1500 и drhider на bhbvs с ingress на 6f74n+bhbvs — все запросы идут через Geneve, результат 0/30.
**Вывод:** MTU 1500 работает только случайно (когда поды оказываются на одной ноде). MTU 1400 — единственно надёжное решение.
---
## Баг #2: ERR_CONNECTION_RESET в браузере (2026-07-14)
### Симптом
После фикса MTU — curl с ВМ работал, но браузер (Chrome/Electron) при загрузке 63KB docx получал `ERR_CONNECTION_RESET`.
### Причина
Два механизма одновременно:
1. **`keep-alive: 10`** — nginx закрывал idle-соединения через 10с. Браузер не замечал FIN (буферизация TCP в ОС), отправлял POST в уже закрытый сокет → RST.
2. **kube-vip ARP flapping** — при смене ARP-лидера трафик уходил на другую ноду без conntrack-записи → RST.
### Решение
| # | Где | Что | Было | Стало |
|---|-----|-----|------|-------|
| 1 | Ingress ConfigMap | `keep-alive` | `10` | **`75`** |
| 2 | `index.html` (JS) | warmup GET при загрузке | — | `fetch('/health')` |
### Результат тестов (v0.0.26)
| Тест | Результат |
|------|-----------|
| 5× свежих страниц + POST 63KB | 4× OK, 1× ABORTED (Playwright) |
| UI: 2 файла + SSE + download | ✅ полный цикл, 18.5с |
| 3× POST через 10+ мин (kube-vip окно) | 3× OK:200 |
| 20× POST (батарея) | 0 ERR_CONNECTION_RESET |
---
## Текущая конфигурация (2026-07-14)
### Ingress ConfigMap
| Параметр | Дефолт nginx | Было (Штурвал) | Стало | Причина |
|----------|-------------|-----------------|-------|---------|
| `keep-alive` | 75 | **10** | **75** | 10с — nginx рвал idle-соединения → RST. Вернули к дефолту |
| `use-http2` | true | true | **`"false"`** | HTTP/2 давал PROTOCOL_ERROR |
| `proxy-body-size` | 8m | 8m | **1024m** | Загрузка docx/pdf до 1 ГБ |
| `client-body-timeout` | 60 | **10** | **10** ⚠️ | Надо вернуть 60 |
| `client-header-timeout` | 60 | **10** | **10** ⚠️ | Надо вернуть 30 |
| `upstream-keepalive-connections` | 0 | **50** | **0** | Был эксперимент, можно вернуть 50 |
| `error-log-level` | info | info | **debug** | Временно для диагностики |
### Cilium
| Параметр | По умолчанию | Текущее | Причина |
|----------|-------------|---------|---------|
| `mtu` | 1500 | **1400** | Geneve +50 = 1450 = underlay |
### Приложение (drhider v0.0.26)
| Файл | Изменение |
|------|-----------|
| `site/templates/index.html` | `fetch('/health')` при загрузке — прогрев upstream |
| `site/app.py` | `VERSION = "0.0.26"` |
---
## Для DevOps — что должно быть в Штурвале
```yaml
# Ingress ConfigMap — привести к дефолтам nginx
keep-alive: "75" # Штурвал ставит 10 — rвёт idle → RST
client-body-timeout: "60" # Штурвал ставит 10 — rвёт медленных клиентов
client-header-timeout: "30" # Штурвал ставит 10
use-http2: "true" # можно вернуть
proxy-body-size: "1024m" # под загрузку файлов
upstream-keepalive-connections: "0"
error-log-level: "info" # вернуть после диагностики
# Cilium ConfigMap (kube-system)
mtu: "1400" # ⚠️ было 1500 — Geneve overhead
```