# Проблема загрузки файлов: диагностика и решение

**Дата:** 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
```
