docs: PROBLEM-AND-SOLUTION.md — переписан, только факты, без ложных следов
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
+103
-123
@@ -1,148 +1,128 @@
|
|||||||
# Проблема загрузки файлов: диагностика и решение
|
# Проблема загрузки файлов: диагностика и решение
|
||||||
|
|
||||||
**Дата:** 2026-07-14
|
**Дата:** 2026-07-16
|
||||||
|
**Версия приложения:** v0.0.29
|
||||||
|
**Кластер:** `iot-naeel` (4 ноды, Cilium Geneve, kube-vip, Штурвал)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Суть проблемы
|
## Проблема 1: 51-секундная задержка (MTU)
|
||||||
|
|
||||||
При передаче данных через HTTP POST из внешней сети на managed-сервисы кластера (Flask и другие) периодически возникала константная задержка **51 секунда**. Проблема проявлялась на любом объёме данных — даже на 100 КБ. Внутри кластера (pod → ingress pod напрямую) передача работала мгновенно. Задержка возникала исключительно при прохождении трафика снаружи через VIP (kube-vip), который распределяет запросы по всем нодам кластера.
|
### Суть
|
||||||
|
При передаче POST из внешней сети — пауза 51с на любом объёме данных. Внутри кластера — мгновенно.
|
||||||
---
|
|
||||||
|
|
||||||
## Причина
|
|
||||||
|
|
||||||
**Несоответствие 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`.
|
|
||||||
|
|
||||||
### Причина
|
### Причина
|
||||||
|
Underlay MTU = 1450. Cilium Geneve добавляет 50 байт. Дефолтный MTU пода = 1500.
|
||||||
Два механизма одновременно:
|
Cross-node пакет = 1500+50 = 1550 → фрагментация → дроп → ядро ждёт повторной передачи 51с.
|
||||||
|
|
||||||
1. **`keep-alive: 10`** — nginx закрывал idle-соединения через 10с. Браузер не замечал FIN (буферизация TCP в ОС), отправлял POST в уже закрытый сокет → RST.
|
|
||||||
|
|
||||||
2. **kube-vip ARP flapping** — при смене ARP-лидера трафик уходил на другую ноду без conntrack-записи → RST.
|
|
||||||
|
|
||||||
### Решение
|
### Решение
|
||||||
|
`cilium-config` → `mtu: 1400`. 1400+50 = 1450 = underlay.
|
||||||
|
Задокументировано 2026-07-13, применено 2026-07-14.
|
||||||
|
|
||||||
| # | Где | Что | Было | Стало |
|
---
|
||||||
|---|-----|-----|------|-------|
|
|
||||||
| 1 | Ingress ConfigMap | `keep-alive` | `10` | **`75`** |
|
|
||||||
| 2 | `index.html` (JS) | warmup GET при загрузке | — | `fetch('/health')` |
|
|
||||||
|
|
||||||
### Результат тестов (v0.0.26)
|
## Проблема 2: ERR_CONNECTION_RESET после фикса MTU
|
||||||
|
|
||||||
|
### Суть
|
||||||
|
curl с ВМ работает. Браузер — `ERR_CONNECTION_RESET` или `⏳ 100%`.
|
||||||
|
После `rollout restart` ingress — работает 5-10 минут, потом снова RST.
|
||||||
|
|
||||||
|
### Ложные следы (опровергнуты)
|
||||||
|
|
||||||
|
| Гипотеза | Почему неверна |
|
||||||
|
|----------|---------------|
|
||||||
|
| kube-vip ARP flapping | `kube-vip.io/vipHost: control-plane` — VIP жёстко на одной ноде |
|
||||||
|
| hostPort на 2 из 4 нод | Трафик только на control-plane, не на все 4 |
|
||||||
|
| upstream keepalive stale | `upstream-keepalive-connections: 0` не помог |
|
||||||
|
| conntrack | ETPolicy Cluster не при чём — трафик на одну ноду |
|
||||||
|
| MTU | Решено в проблеме 1 |
|
||||||
|
| HTTP/2 | `ERR_HTTP2_PROTOCOL_ERROR` → отключили → `ERR_CONNECTION_RESET` — симптом тот же |
|
||||||
|
|
||||||
|
### Истинная причина
|
||||||
|
**`keep-alive: 10`** — Штурвал ставил nginx `keep-alive: 10` (вместо дефолта 75).
|
||||||
|
Сценарий:
|
||||||
|
1. Браузер грузит страницу — TCP открыт
|
||||||
|
2. Пользователь выбирает файлы (>10с) — nginx закрывает idle соединение
|
||||||
|
3. Браузер не замечает FIN (буферизация ОС)
|
||||||
|
4. POST 63KB летит в закрытый сокет → TCP RST
|
||||||
|
|
||||||
|
### Решение (применено в платформе Штурвала 2026-07-15)
|
||||||
|
|
||||||
|
Через `ShturvalServiceConfig` (не kubectl — ArgoCD откатывает):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
controller:
|
||||||
|
config:
|
||||||
|
keep-alive: "75"
|
||||||
|
proxy-body-size: "1024m"
|
||||||
|
client-body-timeout: "60"
|
||||||
|
client-header-timeout: "30"
|
||||||
|
hostPort:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
```
|
||||||
|
|
||||||
|
Также в коде — `fetch('/health')` при загрузке страницы (прогрев upstream).
|
||||||
|
|
||||||
|
### Результаты тестов (v0.0.29, 2026-07-15)
|
||||||
|
|
||||||
| Тест | Результат |
|
| Тест | Результат |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| 5× свежих страниц + POST 63KB | 4× OK, 1× ABORTED (Playwright) |
|
| 10× POST 63KB (свежие страницы) | **10/10 OK:200** |
|
||||||
| UI: 2 файла + SSE + download | ✅ полный цикл, 18.5с |
|
| UI: 5 файлов + SSE + ZIP + CSV | ✅ полный цикл |
|
||||||
| 3× POST через 10+ мин (kube-vip окно) | 3× OK:200 |
|
| 5× POST через 75с (keep-alive окно) | **5/5 OK:200** |
|
||||||
| 20× POST (батарея) | 0 ERR_CONNECTION_RESET |
|
| 5MB PDF через curl | ✅ 200 OK, 0.62с |
|
||||||
|
| **ИТОГО** | **25/25 успешно, 0 RST** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Текущая конфигурация (2026-07-14)
|
## Итоговая конфигурация кластера
|
||||||
|
|
||||||
### Ingress ConfigMap
|
### Ingress (через платформу Штурвала, несбрасываемо ArgoCD)
|
||||||
|
|
||||||
| Параметр | Дефолт nginx | Было (Штурвал) | Стало | Причина |
|
| Параметр | Дефолт nginx | Было (Штурвал) | Сейчас |
|
||||||
|----------|-------------|-----------------|-------|---------|
|
|----------|-------------|----------------|--------|
|
||||||
| `keep-alive` | 75 | **10** | **75** | 10с — nginx рвал idle-соединения → RST. Вернули к дефолту |
|
| `keep-alive` | 75 | **10** | **75** |
|
||||||
| `use-http2` | true | true | **`"false"`** | HTTP/2 давал PROTOCOL_ERROR |
|
| `proxy-body-size` | 8m | 8m | **1024m** |
|
||||||
| `proxy-body-size` | 8m | 8m | **1024m** | Загрузка docx/pdf до 1 ГБ |
|
| `client-body-timeout` | 60 | **10** | **60** |
|
||||||
| `client-body-timeout` | 60 | **10** | **10** ⚠️ | Надо вернуть 60 |
|
| `client-header-timeout` | 60 | **10** | **30** |
|
||||||
| `client-header-timeout` | 60 | **10** | **10** ⚠️ | Надо вернуть 30 |
|
| `use-http2` | true | true | **false** |
|
||||||
| `upstream-keepalive-connections` | 0 | **50** | **0** | Был эксперимент, можно вернуть 50 |
|
| `upstream-keepalive-connections` | 0 | 50 | 0 |
|
||||||
| `error-log-level` | info | info | **debug** | Временно для диагностики |
|
| `error-log-level` | info | info | **debug** (вернуть info) |
|
||||||
|
|
||||||
### Cilium
|
### Cilium (через kubectl, не сбрасывается)
|
||||||
|
|
||||||
| Параметр | По умолчанию | Текущее | Причина |
|
| Параметр | Дефолт | Сейчас |
|
||||||
|----------|-------------|---------|---------|
|
|----------|--------|--------|
|
||||||
| `mtu` | 1500 | **1400** | Geneve +50 = 1450 = underlay |
|
| `mtu` | 1500 | **1400** |
|
||||||
|
|
||||||
### Приложение (drhider v0.0.26)
|
### Приложение (drhider v0.0.29)
|
||||||
|
|
||||||
| Файл | Изменение |
|
```python
|
||||||
|------|-----------|
|
# site/app.py
|
||||||
| `site/templates/index.html` | `fetch('/health')` при загрузке — прогрев upstream |
|
VERSION = "0.0.29"
|
||||||
| `site/app.py` | `VERSION = "0.0.26"` |
|
app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200 MB
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Для 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// site/templates/index.html — warmup при загрузке
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
sf = []; fi.value = ''; rr();
|
||||||
|
fetch('/health').catch(() => {}); // прогрев upstream
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
| Файл | Что |
|
||||||
|
|------|-----|
|
||||||
|
| `docs/ARCHITECTURE.md` | Архитектура + требования к Flask |
|
||||||
|
| `docs/MIGRATION-GUIDE.md` | Инструкция переноса UI на Flask |
|
||||||
|
| `History/2026-07-14-browser-rst-debug.md` | Полная история диагностики RST |
|
||||||
|
| `History/2026-07-15-rst-root-cause.md` | Финальный диагноз |
|
||||||
|
| `History/2026-07-15-cluster-dump.md` | Дамп кластера |
|
||||||
|
| `STATE-2026-07-15.md` | Состояние проекта на 2026-07-15 |
|
||||||
|
|||||||
Reference in New Issue
Block a user