Files
loadtest/History/2026-07-11-ws-stall-flow-control.md
2026-07-11 06:38:38 +04:00

3.4 KiB

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:

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-буфером