feat: bufferedAmount flow control in WS upload, bump 1.0.16
Deploy loadtest / validate (push) Waiting to run
Deploy loadtest / validate (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
# 2026-07-11 — WS stall: root cause (Sonnet) + план flow control
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
WebSocket-загрузка файлов через ingress (Flask + flask-sock + Werkzeug dev server).
|
||||||
|
Файл 3.7 MB, чанки 64 KB. Первые ~90% загружаются мгновенно, затем пауза ~51 сек.
|
||||||
|
|
||||||
|
## Диагноз (Sonnet)
|
||||||
|
|
||||||
|
**Первопричина: TCP receive buffer saturation на поде.**
|
||||||
|
|
||||||
|
### Механика
|
||||||
|
|
||||||
|
```
|
||||||
|
Браузер: ws.send() × 58 без пауз, без проверки bufferedAmount
|
||||||
|
↓
|
||||||
|
nginx: пробрасывает всё в под (proxy-request-buffering: off)
|
||||||
|
↓
|
||||||
|
Ядро пода: TCP recv buffer автотюнится до ~6 MB
|
||||||
|
↓
|
||||||
|
При ~3.3 MB (90%) буфер ЗАПОЛНЕН → TCP zero window
|
||||||
|
↓
|
||||||
|
nginx: send() → EAGAIN → перестаёт читать от браузера
|
||||||
|
↓
|
||||||
|
Werkzeug: читает 1 фрейм (64 KB) → окно приоткрывается
|
||||||
|
→ nginx пихает 1-2 фрейма → окно схлопывается
|
||||||
|
→ ЦИКЛ: stop-and-go oscillation
|
||||||
|
↓
|
||||||
|
Оставшиеся ~400 KB (6 чанков) дрейнуются ~8 KB/s = 51 сек
|
||||||
|
```
|
||||||
|
|
||||||
|
### Почему HTTP POST не виснет?
|
||||||
|
|
||||||
|
HTTP POST тестировался на nginx:alpine (C-сервер) — вычитывает со скоростью линии,
|
||||||
|
буфер не заполняется. На Flask/Werkzeug HTTP POST падает в 30-50% случаев —
|
||||||
|
та же проблема, но проявляется через Connection: close → новый TCP → conntrack timeout.
|
||||||
|
|
||||||
|
### Почему внутри кластера (прямой WS на pod:5000) быстро?
|
||||||
|
|
||||||
|
Нет nginx-прослойки. Браузер → напрямую под, TCP buffer тот же, но:
|
||||||
|
- Меньше промежуточных буферов
|
||||||
|
- Нет дополнительной задержки на nginx event loop
|
||||||
|
|
||||||
|
## План: `bufferedAmount` flow control
|
||||||
|
|
||||||
|
**Меняется:** `app.js`, функция `uploadFileWS`
|
||||||
|
|
||||||
|
### Суть
|
||||||
|
|
||||||
|
Вместо прямого `ws.send(data)` — проверка `ws.bufferedAmount`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function sendOrWait(data) {
|
||||||
|
if (ws.bufferedAmount > 256 * 1024) {
|
||||||
|
setTimeout(function () { sendOrWait(data); }, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(data);
|
||||||
|
offset += CHUNK;
|
||||||
|
if (offset < f.size) {
|
||||||
|
readNext();
|
||||||
|
} else {
|
||||||
|
ws.send('DONE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`reader.onload` становится однострочным: `sendOrWait(e.target.result)`.
|
||||||
|
|
||||||
|
Порог 256 KB выбран так, чтобы:
|
||||||
|
- Не заполнять TCP buffer пода (>6 MB max)
|
||||||
|
- Оставлять запас для nginx внутренних буферов
|
||||||
|
- Не создавать излишних задержек
|
||||||
|
|
||||||
|
### Версия
|
||||||
|
|
||||||
|
`app.py`: `VERSION = '1.0.15'` → `'1.0.16'`
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
- Только client-side (app.js)
|
||||||
|
- app.py — без изменений в логике
|
||||||
|
- CHUNK (64 KB), порог прогресса (10%), таймаут (300 сек) — без изменений
|
||||||
|
|
||||||
|
### Ожидаемый результат
|
||||||
|
|
||||||
|
- Буфер не переполняется → нет stop-and-go oscillation
|
||||||
|
- Прогресс-бар обновляется плавно на всём протяжении загрузки
|
||||||
|
- Файл 3.7 MB загружается без 51-секундной паузы
|
||||||
|
- Скорость определяется скоростью чтения сервером (Werkzeug), а не TCP-буфером
|
||||||
+1
-1
@@ -7,7 +7,7 @@ from flask_sock import Sock
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
sock = Sock(app)
|
sock = Sock(app)
|
||||||
|
|
||||||
VERSION = '1.0.15'
|
VERSION = '1.0.16'
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|||||||
+11
-2
@@ -210,8 +210,13 @@ function uploadFileWS(url, f, idx) {
|
|||||||
|
|
||||||
ws.onopen = function () {
|
ws.onopen = function () {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
|
||||||
ws.send(e.target.result);
|
function sendOrWait(data) {
|
||||||
|
if (ws.bufferedAmount > 256 * 1024) {
|
||||||
|
setTimeout(function () { sendOrWait(data); }, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(data);
|
||||||
offset += CHUNK;
|
offset += CHUNK;
|
||||||
if (offset < f.size) {
|
if (offset < f.size) {
|
||||||
var pct = Math.round(offset / f.size * 100);
|
var pct = Math.round(offset / f.size * 100);
|
||||||
@@ -224,6 +229,10 @@ function uploadFileWS(url, f, idx) {
|
|||||||
} else {
|
} else {
|
||||||
ws.send('DONE');
|
ws.send('DONE');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
sendOrWait(e.target.result);
|
||||||
};
|
};
|
||||||
function readNext() {
|
function readNext() {
|
||||||
var end = Math.min(offset + CHUNK, f.size);
|
var end = Math.min(offset + CHUNK, f.size);
|
||||||
|
|||||||
Reference in New Issue
Block a user