feat: Gunicorn + gevent (keepalive), bump 1.0.8→1.0.9
Deploy loadtest / validate (push) Waiting to run
Deploy loadtest / validate (push) Waiting to run
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -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, который проявляется случайно?
|
||||
@@ -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/сеть/ноду?
|
||||
@@ -1,2 +1,3 @@
|
||||
flask
|
||||
gunicorn
|
||||
gevent
|
||||
|
||||
+23
-2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user