From b3298b415e3948748772dcccb3fe97fb7cbb7281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 14 Jul 2026 19:19:12 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20PROBLEM-AND-SOLUTION,=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0,=20=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=8F=20Connection:close,=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODERULES.md | 1 + .github/copilot-instructions.md | 4 +- .gitignore | 11 +- History/2026-07-14-hop-by-hop-connection.md | 23 +++ PROBLEM-AND-SOLUTION.md | 152 +++++++------------- 5 files changed, 88 insertions(+), 103 deletions(-) create mode 100644 History/2026-07-14-hop-by-hop-connection.md diff --git a/.github/CODERULES.md b/.github/CODERULES.md index cccdc77..9ed4540 100644 --- a/.github/CODERULES.md +++ b/.github/CODERULES.md @@ -1,5 +1,6 @@ # ⛔ ЖЁСТКИЕ ПРАВИЛА - НЕЛЬЗЯ менять код без «делай» — показать план → ждать +- НЕЛЬЗЯ менять ЧТО-ЛИБО при вопросе (?, почему, как, где, что) — только ответ словами - НЕЛЬЗЯ спешить — обдумать, проверить, потом отвечать - НЕЛЬЗЯ лезть в /home/naeel/nubes/contracts/ (кроме loadtest) - НЕЛЬЗЯ предполагать — сомневаешься = спроси diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9e02de5..4760ecf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,2 +1,4 @@ -# ⛔ STOP: без «делай» ничего не делать. Не спешить. Не лезть в contracts/. Не гадать — спросить. +# ⛔⛔⛔ STOP: без «делай» ничего не делать +# ⛔⛔⛔ ВОПРОС (?, почему, как, где, что) — ТОЛЬКО ОТВЕТ. Читать/узнавать МОЖНО, менять НЕЛЬЗЯ. +# ⛔ Не спешить. Не лезть в contracts/. Не гадать — спросить. # 📋 Правила: .github/CODERULES.md diff --git a/.gitignore b/.gitignore index 95241e5..c4c4007 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ __pycache__/ *.pyc .env -31.08.25 4 курс ЛМКК.md -TMP/ TMP/ +about1500.md +*.har +chrome-net-export-log.json +docs/about1500.md +*.har +chrome-net-export-log.json +docs/about1500.md +*.har +chrome-net-export-log.json diff --git a/History/2026-07-14-hop-by-hop-connection.md b/History/2026-07-14-hop-by-hop-connection.md new file mode 100644 index 0000000..90c3edd --- /dev/null +++ b/History/2026-07-14-hop-by-hop-connection.md @@ -0,0 +1,23 @@ +# Waitress + Connection: close — hop-by-hop заголовок + +**Дата:** 2026-07-14 +**Версия:** 0.0.21 → 0.0.22 + +--- + +## Ошибка + +`Connection` — hop-by-hop заголовок. Waitress (WSGI) запрещает его использовать в приложении (PEP 3333). + +```python +# ❌ НЕЛЬЗЯ +response.headers["Connection"] = "close" + +# Ошибка: +AssertionError: Connection is a "hop-by-hop" header; it cannot be used by a WSGI application (see PEP 3333) +``` + +## Правило + +**Никогда не добавлять `Connection` заголовок в Flask/Waitress-приложении.** +Нужно закрывать соединения — только через nginx (upstream keepalive) или uWSGI/gunicorn. diff --git a/PROBLEM-AND-SOLUTION.md b/PROBLEM-AND-SOLUTION.md index 21d6eed..3aff86e 100644 --- a/PROBLEM-AND-SOLUTION.md +++ b/PROBLEM-AND-SOLUTION.md @@ -1,131 +1,83 @@ # Проблема загрузки файлов: диагностика и решение -**Дата:** 2026-07-13 +**Дата:** 2026-07-14 --- -## В чём проблема +## Суть проблемы -При передаче данных через HTTP POST из внешней сети на managed-сервисы, развёрнутые в кластере (Flask, любые другие), периодически возникает пауза длительностью 51 секунда. Проблема затрагивает любой сервис, чей трафик проходит через ingress-контроллер Штурвала при cross-node-форвардинге. Размер передаваемых данных не имеет значения — задержка константна, проявляется даже на 100 KB. - -Внутри кластера (pod → ingress pod напрямую) передача происходит мгновенно. Проблема проявляется только при проходе трафика извне через VIP (kube-vip), который распределяет запросы на все ноды кластера. - -### Причина (технически) - -*Cilium — сетевая подсистема Kubernetes, отвечает за связь между подами на разных нодах. Geneve — протокол туннелирования, который Cilium использует чтобы «обернуть» обычный сетевой пакет для передачи через физическую сеть между серверами.* - -```mermaid -flowchart LR - A["Geneve 50 B"]:::orange --- B["Данные 1450 B"]:::green - B -.-> C["Итого 1500 B"]:::gray - C --> D["Underlay-канал 1450 B"]:::red - D --> E["TCP stall 51с"]:::dark - - classDef orange fill:#ff9800,color:#fff,stroke:#e65100 - classDef green fill:#4caf50,color:#fff,stroke:#2e7d32 - classDef gray fill:#9e9e9e,color:#fff,stroke:#616161 - classDef red fill:#f44336,color:#fff,stroke:#c62828 - classDef dark fill:#b71c1c,color:#fff -``` - -> **1450** (данные) + **50** (Geneve) = **1500** → underlay **1450** → не проходит → TCP stall **51 секунда**. - -- Cilium соединяет ноды через **Geneve-туннель** -- Geneve добавляет к каждому пакету **+50 байт** заголовков (внешний IP 20 + UDP 8 + Geneve 8 + инкапсулируемый L2-кадр 14) -- Cilium ожидает что underlay (физический канал между нодами) = **1500 байт** - > *Стандартный Ethernet MTU — 1500. Все сетевые интерфейсы, драйверы и протоколы по умолчанию исходят из этого. Cilium опрашивает интерфейс ноды, видит 1500 и принимает за истину. Почему реальный underlay оказался 1450 — может быть связано с инфраструктурой хостинг-провайдера, промежуточным сетевым оборудованием или инкапсуляцией на уровне виртуализации. Без диагностики на стороне провайдера точная причина неизвестна. Факт установлен замером: 1500 не проходит, 1450 проходит.* -- Cilium считает: **1500 − 50 = 1450** — такой MTU назначает туннелю и подам -- Реальный underlay (измерен `ping -M do`): **1450 байт**, а не 1500 -- Пакет из пода = **1450 байт** -- Пакет + Geneve-заголовок = **1450 + 50 = 1500 байт** -- Реальный канал = **1450 байт** → пакет **1500 не влазит** -- Роутер пытается отправить ICMP «fragmentation needed», но ICMP заблокирован -- Отправитель не знает что пакет не дошёл, ждёт подтверждения -- TCP включает exponential backoff от начального RTO 200ms: **0.2с → 0.4с → 0.8с → 1.6с → 3.2с → 6.4с → 12.8с → 25.6с = 51 секунда** -- Через 51 секунду прикладной таймаут (браузер/curl) разрывает ожидание → `ERR_TIMED_OUT` - -### Почему не всегда - -Проблема только при **cross-node**-форвардинге (kube-vip направляет трафик на ingress-под на другой ноде). При попадании на ту же ноду — всё мгновенно. +При передаче данных через HTTP POST из внешней сети на managed-сервисы кластера (Flask и другие) периодически возникала константная задержка **51 секунда**. Проблема проявлялась на любом объёме данных — даже на 100 КБ. Внутри кластера (pod → ingress pod напрямую) передача работала мгновенно. Задержка возникала исключительно при прохождении трафика снаружи через VIP (kube-vip), который распределяет запросы по всем нодам кластера. --- -## Что сделано сейчас (временный обход) +## Причина -Для обхода проблемы были применены два изменения: +**Несоответствие MTU:** канал между серверами — **1450 байт**, а каждый Geneve-туннель добавляет к пакету **50 байт**. При дефолтном MTU пода в 1500 байт любой cross-node-переход через туннель создаёт пакет размером 1550 байт, который не помещается в канал и дропается. Ядро ждёт повторной передачи — отсюда и 51-секундная пауза. -**1. `externalTrafficPolicy: Local`** — запрещает cross-node-форвардинг. Трафик обслуживается только на той ноде куда пришёл, без туннеля → stall отсутствует. +В зависимости от того, на каких нодах окажутся поды, запрос проходит через 0, 1 или 2 Geneve-туннеля: -**2. 4 реплики ingress вместо 2** — у Штурвала по умолчанию 2 пода, а нод 4. С `Local` трафик на ноду без пода дропается. Выставлено 4 — по одной на каждую ноду. +| Путь | Туннелей | Размер пакета | Проходит? | +|------|----------|---------------|-----------| +| Клиент → нода с ingress → drhider на той же ноде | 0 | 1500 б | ✅ | +| Клиент → нода с ingress → drhider на другой ноде | 1 | 1500 + 50 = **1550 б** | ❌ | +| Клиент → нода без ingress → ingress → drhider | 2 | 1500 + 50 + 50 = **1600 б** | ❌ | -**Проблема:** Штурвал управляет ingress через Helm с `replicaCount: 2`. При обнаружении расхождения (фактическое состояние не соответствует Helm-значениям) реконсилер возвращает реплики с 4 обратно на 2. Интервал не фиксирован — срабатывает по событию (деплой, изменение конфигурации). Половина запросов начинает падать с `ERR_TIMED_OUT`. +Снижение MTU пода до **1400 байт** устраняет проблему: 1400 + 50 = 1450, что точно вписывается в канал на каждом переходе. --- -## Корневое решение +## Архитектура кластера -Выставить корректный MTU в Cilium: - -```bash -kubectl patch configmap -n kube-system cilium-config --type merge -p '{"data":{"mtu":"1400"}}' - -kubectl rollout restart ds/cilium -n kube-system -kubectl delete pods -n ingress --all --force --grace-period=0 -kubectl delete pods -n --all --force --grace-period=0 -``` - -После этого: -- Cross-node-форвардинг работает без stall -- Ingress остаётся на 2 репликах (стоковое значение Штурвала) -- `Local` не нужен -- Ручной scale не нужен - -### Почему 1400 - -Underlay между нодами — **1450** (измерен `ping -M do`). -В этой версии Cilium (`routing-mode: tunnel`, `tunnel-protocol: geneve`) значение `mtu` задаёт pod MTU напрямую. Geneve-заголовок (+50 байт) добавляется поверх. +Кластер `iot-naeel`: 4 ноды (3 worker + 1 control-plane), Cilium с Geneve-энкапсуляцией, kube-vip. ``` -Pod MTU 1400 → пакет 1400 + Geneve 50 = 1450 = underlay 1450 → проходит -Pod MTU 1450 → пакет 1450 + Geneve 50 = 1500 > underlay 1450 → дроп +Ноды: 6f74n | bhbvs | v8zq4 | control-plane +Ingress: 2 пода (Штурвал, всегда на разных нодах) +Drhider: 1 под (размещается на случайной worker-ноде) +VIP: 185.247.187.151 → kube-vip анонсирует на все 4 ноды через ARP ``` -| Параметр | Значение | -|---------|----------| -| Underlay между нодами | 1450 | -| `cilium-config mtu:` | **1400** | -| Pod MTU | 1400 | -| Пакет через Geneve | 1400 + 50 = 1450 | +**Маршрут запроса:** браузер → VIP (случайная нода) → kube-proxy → ingress-под → kube-proxy → drhider-под. Каждый переход между нодами — Geneve-туннель (+50 байт). -> В некоторых версиях Cilium `mtu` означает underlay (Cilium сам вычитает 50). В нашей — задаёт pod MTU напрямую. Проверено 2026-07-14: `mtu: 1450` → pod 1450 (не работает через Geneve), `mtu: 1400` → pod 1400 (работает). +Примеры возможных размещений: + +| Ситуация | 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-переход. --- -## Результаты (кластер `iot-naeel`, 2026-07-14) +## Тестирование (curl с ВМ, 30 запросов, 63 КБ) -| Метрика | До (MTU 1450) | После (MTU 1400 + Connection: close) | -|---------|---------------|--------------------------------------| -| Upload из браузера | ERR_CONNECTION_RESET | <0.2s | -| 5 файлов подряд | первый сбой | все <0.1s | -| F5 при обработке | мёртвые соединения | чисто | -| Edge 7× подряд | на 5-й сбой | стабильно | +**Цель:** подтвердить, что только 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 — единственно надёжное решение. --- -## Рекомендации для production-кластеров +## Текущая конфигурация (2026-07-14) -На действующих кластерах с активными сервисами **НЕ рекомендуется** применять `kubectl edit configmap` + `rollout restart ds/cilium` вручную. Причины: +| Параметр | По умолчанию | Текущее | Причина изменения | +|----------|-------------|---------|-------------------| +| Cilium `mtu` | 1500 | **1400** | 1400 + 50 (Geneve) = 1450 = underlay | +| `proxy-body-size` (ingress) | 8m | **1024m** | Поддержка загрузки файлов до 1 ГБ | +| HTTP/2 | включён | включён | — | +| `externalTrafficPolicy` | Cluster | Cluster | — | +| Реплик ingress | 2 | 2 | — | -- `rollout restart ds/cilium` перезапускает сетевой слой на **всех** нодах одновременно (rolling update, но всё же) -- eBPF-датаплейн переживает рестарт, но возможны кратковременные блипы -- Ошибка в значении MTU затронет **все** сервисы кластера -- На production требуется окно обслуживания и мониторинг - -**Рекомендуемый порядок для production:** - -1. Замерить underlay MTU между нодами через `ping -M do` -2. Передать значение провайдеру/девопсу для включения в конфигурацию кластера -3. Применить при плановом обслуживании или при создании новых кластеров -4. Для существующих кластеров — согласовать окно и порядок отката - -**Для новых кластеров:** включить `mtu: <измеренное значение>` в конфиг Cilium на этапе развёртывания, до деплоя приложений. +Код v0.0.22: XHR-таймаут 30 с, cache-busting, обработка SSE `GeneratorExit`.