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
|
flask
|
||||||
gunicorn
|
gunicorn
|
||||||
|
gevent
|
||||||
|
|||||||
+23
-2
@@ -4,7 +4,7 @@ from flask import Flask, render_template, request
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
VERSION = '1.0.8'
|
VERSION = '1.0.9'
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -51,4 +51,25 @@ def upload():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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