diff --git a/History/2026-07-10-final-answer.md b/History/2026-07-10-final-answer.md new file mode 100644 index 0000000..12adf78 --- /dev/null +++ b/History/2026-07-10-final-answer.md @@ -0,0 +1,23 @@ +# 2026-07-10 — Финальный ответ: Werkzeug Connection: close + +## Корень +Werkzeug dev-сервер ВСЕГДА закрывает TCP-соединение после ответа (`Connection: close`). +nginx должен создавать НОВЫЙ TCP на каждый запрос → Cilium conntrack cycling → intermittent failure. + +nginx:alpine держит `Connection: keep-alive` → 1 TCP на все запросы → 10/10. + +## Почему все тесты такие +| Тест | Результат | Причина | +|------|-----------|---------| +| nginx:alpine | 10/10 | keep-alive, 1 TCP | +| Flask | 2-5/10 | close, N TCP | +| Gunicorn sync | 1/10 | тоже close + ошибка конфига | +| upstream-keepalive=0 | 0/10 | даже keepalive запрещён | +| внутри кластера | 10/10 | другой code path Cilium | +| свежий деплой 10/10 | чистый conntrack | + +## Решение +Gunicorn с gevent worker (поддерживает keepalive): +``` +gunicorn -w 2 -k gevent --keep-alive 75 --timeout 120 --bind 0.0.0.0:5000 app:app +``` diff --git a/History/2026-07-10-sonnet-answer-contradiction.md b/History/2026-07-10-sonnet-answer-contradiction.md new file mode 100644 index 0000000..563b7fd --- /dev/null +++ b/History/2026-07-10-sonnet-answer-contradiction.md @@ -0,0 +1,19 @@ +# 2026-07-10 — Ответ Соннета: stale upstream keepalive + +## Диагноз +nginx держит пул keepalive-соединений к Flask. После редеплоя — старые соединения ведут на мёртвый pod IP (Cilium conntrack). Часть запросов попадает на живые, часть на stale → случайность. + +## Почему nginx:alpine всегда 10/10 +Он тоже в поде, но nginx→nginx keepalive работает корректно. nginx→Werkzeug — нет (Werkzeug шлёт Connection: close, nginx может игнорировать). + +## Почему Gunicorn 1/10 +Был запущен с 1 sync worker. H2 multiplexing от nginx создаёт много конкурентных запросов → один worker не справляется → 9/10 в очереди → таймаут. + +## План проверок + +| # | Действие | Что проверяет | +|---|----------|--------------| +| 1 | ingress annotation: `upstream-keepalive-connections: "0"` | Stale keepalive — если 10/10, причина найдена | +| 2 | Gunicorn `-w 4 -k gevent --keep-alive 0` | Concurrency fix | +| 3 | Ingress: `use-http2: "false"` | H2 multiplexing | +| 4 | `kubectl rollout restart` + сразу тест | Cilium conntrack | diff --git a/History/sonnet-question-contradiction.md b/History/sonnet-question-contradiction.md new file mode 100644 index 0000000..68e6962 --- /dev/null +++ b/History/sonnet-question-contradiction.md @@ -0,0 +1,38 @@ +# Sonnet: противоречие — nginx 10/10, Flask raw то 10/10 то 2/10 + +## Данные без интерпретаций + +### Тест 1: nginx:alpine через тот же VIP/ingress/порт +100KB raw binary POST, 10 запросов, таймаут 2с: **стабильно 10/10** + +### Тест 2: Flask (тот же ingress/VIP) — raw binary +100KB raw binary POST, те же параметры: +- Первый прогон: 10/10 +- Второй прогон (после редеплоя): 2/10 +- Третий прогон: 2/10 + +### Тест 3: Flask — URL-encoded form (base64) +100KB base64 → 136KB POST: стабильно 2-5/10 + +### Тест 4: Flask изнутри кластера +pod → VIP: 10/10 всегда +pod → ingress service: 10/10 всегда + +### Что пробовали +- debug=False: 5/10 +- Gunicorn: 1/10 (хуже) +- proxy_request_buffering on/off: 3-5/10 +- kube-vip nodeSelector/taint: не влияет +- MSS clamping: не влияет +- raw binary вместо form: то 10/10, то 2/10 + +## Инфра +- Shturval 2.12.1, Cilium CNI +- Flask 3.1.3, Werkzeug 3.1.8 +- nginx ingress, http2 on, proxy_request_buffering off +- kube-vip ARP mode, 4 ноды, ingress hostPort на 2 + +## Вопрос +Почему nginx:alpine через тот же ingress стабильно 10/10, а Flask raw binary — то 10/10, то 2/10? +Что именно в цепочке ingress→Flask даёт такую нестабильность, если тело одно и то же (raw 100KB)? +Может ли Werkzeug dev server иметь скрытый баг с keepalive/connection reuse, который проявляется случайно? diff --git a/History/sonnet-question-final.md b/History/sonnet-question-final.md new file mode 100644 index 0000000..4c2e5ea --- /dev/null +++ b/History/sonnet-question-final.md @@ -0,0 +1,26 @@ +# Sonnet: финальный вопрос + +## Ситуация +Nginx:alpine через managed Flask ingress = 100KB POST 10/10 всегда. +Тот же Flask через тот же ingress = 2-5/10, случайно. + +## ВСЁ что пробовали (без эффекта) +- debug=False +- Gunicorn (хуже — 1/10) +- proxy_request_buffering on/off +- upstream-keepalive=0 (хуже — 0/10) +- use-http2=false +- raw binary вместо URL-encoded form +- kube-vip: nodeSelector, taint, leader election +- MSS clamping +- свежий редеплой +- chunky 100KB +- ретраи + +## Инфра +Flask 3.1.3, Werkzeug 3.1.8, Shturval 2.12.1, Cilium CNI, kube-vip ARP. +Flask и nginx на одной ноде. Ingress на двух нодах. +Внутри кластера всё 10/10 всегда. + +## Вопрос +Какого хуя nginx:alpine 10/10, а Flask 2-5/10 через АБСОЛЮТНО тот же ingress/сеть/ноду? diff --git a/requirements.txt b/requirements.txt index e4a286c..f51d321 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ flask gunicorn +gevent diff --git a/site/app.py b/site/app.py index 598ccbf..3b63b24 100644 --- a/site/app.py +++ b/site/app.py @@ -4,7 +4,7 @@ from flask import Flask, render_template, request app = Flask(__name__) -VERSION = '1.0.8' +VERSION = '1.0.9' @app.route('/') @@ -51,4 +51,25 @@ def upload(): if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) + import gunicorn.app.base + + class StandaloneApp(gunicorn.app.base.BaseApplication): + def __init__(self, app, options=None): + self.application = app + self.options = options or {} + super().__init__() + + def load_config(self): + for k, v in self.options.items(): + self.cfg.set(k, v) + + def load(self): + return self.application + + StandaloneApp(app, { + 'bind': '0.0.0.0:5000', + 'workers': 2, + 'worker_class': 'gevent', + 'timeout': 120, + 'keepalive': 75, + }).run()