From ae7203e59601a4b197df18dfbfbee5a66040e6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 10 Jul 2026 23:08:50 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20Gunicorn=20instead=20of=20Werkzeug=20de?= =?UTF-8?q?v=20server,=20bump=201.0.7=E2=86=921.0.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- History/2026-07-10-sonnet-answer-flask.md | 36 +++++++++++++++++ History/sonnet-question-flask.md | 49 +++++++++++++++++++++++ site/app.py | 28 ++++++++++++- 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 History/2026-07-10-sonnet-answer-flask.md create mode 100644 History/sonnet-question-flask.md diff --git a/History/2026-07-10-sonnet-answer-flask.md b/History/2026-07-10-sonnet-answer-flask.md new file mode 100644 index 0000000..912f0f8 --- /dev/null +++ b/History/2026-07-10-sonnet-answer-flask.md @@ -0,0 +1,36 @@ +# 2026-07-10 — Ответ Соннета: Werkzeug dev server + chunked TE = гонка + +## Диагноз +Werkzeug dev server не умеет корректно работать с chunked transfer encoding от nginx. +При `proxy_request_buffering: off` nginx шлёт тело чанками. +Flask отвечает `Connection: close` → nginx ещё шлёт чанк → RST → таймаут. + +nginx:alpine держит keep-alive → проблемы нет. + +## Решение +Gunicorn вместо Werkzeug dev server. +Правильно де-чанкует тело, держит persistent connections. + +## Способ +Shturval запускает `python app.py`. Вместо `app.run()` — Gunicorn через Python API: +```python +if __name__ == '__main__': + 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, + 'timeout': 300, + }).run() +``` + +requirements.txt уже содержит gunicorn. Dockerfile CMD можно убрать (Shturval всё равно переопределяет). diff --git a/History/sonnet-question-flask.md b/History/sonnet-question-flask.md new file mode 100644 index 0000000..51de5f6 --- /dev/null +++ b/History/sonnet-question-flask.md @@ -0,0 +1,49 @@ +# Sonnet: Flask за ingress'ом дропает большие POST — nginx echo нет + +## Новый решающий тест +Nginx:alpine через тот же VIP/ingress — 100KB POST = 10/10 успех. +Flask через тот же VIP/ingress — 100KB POST = 2-5/10. + +VIP, ingress, kube-vip, сеть исключены. Проблема в связке «ingress → Flask». + +## Что уже проверили на Flask +- debug=True → debug=False: 5/10 (незначительно) +- request.get_data() перед request.form: 2/10 (ухудшило, откачено) +- proxy_request_buffering off: 5/10 (лучше чем on = 3/10) +- proxy_request_buffering on: 3/10 +- Внутри кластера (pod→pod, pod→ingress): 10/10 всегда + +## Инфра +- Flask 3.1.3, Werkzeug 3.1.8, Python 3.12 +- Запуск: app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) +- Ingress: nginx, proxy_http_version 1.1, proxy_request_buffering off +- Flask отвечает Connection: close (dev server не держит keepalive) +- Content-Type: application/x-www-form-urlencoded, тело ~136KB (base64) +- Код: request.form.get('data', '') → парсинг формы + +## Код /upload +```python +@app.route('/upload', methods=['POST']) +def upload(): + t0 = time.time() + data = request.form.get('data', '') + if data: + if ',' in data: + data = data.split(',', 1)[1] + data = data.replace(' ', '+') + missing = len(data) % 4 + if missing: + data += '=' * (4 - missing) + raw = base64.b64decode(data) + elapsed = round((time.time() - t0) * 1000) + return {'ok': True, 'bytes': len(raw), 'elapsed_ms': elapsed} + for f in request.files.values(): + ... + return {'ok': True, 'bytes': total, 'elapsed_ms': elapsed} +``` + +## Вопрос +Почему Flask за nginx ingress'ом дропает connection на большом POST, а nginx:alpine — нет? +Может ли Werkzeug dev server иметь проблему с chunked transfer encoding от ingress'а? +Или проблема в парсинге URL-encoded формы большого размера? +Что попробовать В КОДЕ (не в инфраструктуре)? diff --git a/site/app.py b/site/app.py index ae580de..ad803a0 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.7' +VERSION = '1.0.8' @app.route('/') @@ -48,4 +48,28 @@ 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, + 'timeout': 300, + 'worker_class': 'sync', + 'keepalive': 5, + 'limit_request_field_size': 0, + 'limit_request_line': 0, + 'limit_request_fields': 0, + }).run()