[PonsWarp 개념 교실 19] 백프레셔 — high/low watermark로 전송을 보호하는 법
쉽게 말하면, 백프레셔는 받는 쪽이 감당할 수 없을 때 보내는 쪽에 잠깐 멈추라고 말하는 흐름 제어다.

파일 전송에서는 네트워크보다 디스크 쓰기가 느릴 수 있다. 이때 송신자가 계속 보내면 수신 버퍼가 커지고, 결국 브라우저 메모리와 전송 안정성이 함께 무너진다.
high watermark — 48MiB에서 멈춘다
PonsWarp의 writer는 수신 후 아직 디스크에 flush하지 못한 바이트를 세고, 48MiB에 도달하면 control 채널로 PAUSE를 보낸다.
typescriptexport const WRITE_BUFFER_HIGH_MARK = 48 * 1024 * 1024; if (!isPaused && pendingBytesInBuffer >= highMark) { return 'PAUSE'; }
PAUSE는 실패가 아니다. 생산자와 소비자의 속도 차이를 인정하고, 소비자가 따라올 시간을 만드는 정상적인 상태 전환이다.
low watermark — 16MiB까지 내려오면 다시 시작한다
버퍼가 48MiB 아래로 내려갔다고 바로 재개하면 PAUSE/RESUME가 빠르게 반복될 수 있다. 그래서 high와 low를 다르게 둔다.
typescriptexport const WRITE_BUFFER_LOW_MARK = 16 * 1024 * 1024; if (isPaused && pendingBytesInBuffer <= lowMark) { return 'RESUME'; }
48MiB에서 멈추고 16MiB까지 비운 뒤 재개하는 차이를 히스테리시스라고 볼 수 있다. 경계 하나만 두는 것보다 상태가 덜 흔들린다.
0 ─────── 16MiB ───────── 48MiB ───────→
RESUME PAUSE
모바일에서는 여러 번 멈출 수 있다
화면이 꺼지거나 브라우저가 백그라운드로 가면 한 번의 전송에서도 채널이 여러 번 멈출 수 있다. PonsWarp는 resume 시도 횟수를 12회로 제한한다.
typescriptexport const MAX_RESUME_ATTEMPTS = 12;
무한 재시도는 실패를 숨기고 사용자를 붙잡는다. 시도 예산을 두면 복구 가능한 장애와 종료해야 할 장애를 구분할 수 있다.
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| PAUSE high mark | 48MiB | writerFlowControl.ts:7 |
| RESUME low mark | 16MiB | writerFlowControl.ts:9 |
| 상태 결정 | resolveWriterBackpressureAction | writerFlowControl.ts:16-34 |
| resume 최대 시도 | 12회 | writerFlowControl.ts:11,40-54 |
| control 명령 | PAUSE/RESUME | webRTCService.ts, swarmManager.ts |
high와 low를 같은 값으로 두면 안 되는 이유
writer 버퍼가 48MiB가 되는 순간 멈추고, 16MiB까지 내려오면 재개하는 이유는 두 임계값 사이에 히스테리시스(hysteresis) 를 만들기 위해서다. 하나의 값만 사용하면 버퍼가 경계 근처에서 조금 늘고 줄 때마다 PAUSE와 RESUME가 반복된다. control 메시지가 흔들리고, 송신자는 실제 데이터보다 상태 전환을 더 많이 처리한다.
textbuffered bytes 48MiB ───────── HIGH: PAUSE │ 멈춘 동안 writer flush 16MiB ───────── LOW : RESUME │ └──── 다시 전송
입력은 수신 writer의 미flush 바이트, 상태는 RUNNING 또는 PAUSED, 출력은 송신자에게 보내는 control 메시지다. PAUSE는 이미 채널에 들어간 패킷을 되돌리지 못하므로 “앞으로 더 넣지 말라”는 경계다. RESUME도 버퍼가 완전히 0이 된 뒤가 아니라 low watermark까지 내려왔을 때 보내야 회선을 다시 채울 수 있다.
그냥 송신자에서만 막으면 안 되나?
송신자만 bufferedAmount를 보면 수신 디스크가 느린 상황을 알 수 없다. 반대로 수신자가 매 패킷마다 “더 보내도 된다”고 답하면 control 트래픽과 상태 관리가 늘어난다. 수신 writer가 자신의 자원 상태를 high/low watermark로 요약해 보내는 편이 경계가 명확하다.
PAUSE는 현재 전송 run과 연결되어야 한다. 오래된 run의 RESUME가 새 전송을 깨우면 수신 버퍼가 아직 비어 있지 않은데 송신이 재개될 수 있다. 따라서 메시지에 run 식별자와 sequence/frontier를 함께 포함하고, 취소 시 pending control을 무효화해야 한다.
| 상태 | 조건 | 허용 행동 |
|---|---|---|
RUNNING | buffer < high | 패킷 수신 |
PAUSED | buffer >= high | writer flush만 진행 |
READY | buffer <= low | RESUME 전송 |
CANCELLED | run 종료 | 모든 대기·제어 무효화 |
경계값과 재구축 판단
48MiB와 16MiB는 모든 장치의 정답이 아니다. 큰 모바일 메모리에서는 너무 보수적일 수 있고, 작은 탭에서는 이미 늦을 수 있다. 단위가 payload인지 실제 writer buffer인지 명확히 하고, flush 실패·QuotaError·탭 종료도 PAUSE와 다른 terminal error로 구분해야 한다. 네트워크가 끊겼는데 버퍼가 줄지 않는 경우에는 RESUME를 반복해서 보내지 않는다.
백프레셔를 재구축할 때는 worker credit, DataChannel window, writer watermark를 층별로 로그에 남긴다. 어느 층이 멈췄는지 보이지 않으면 모든 정지를 “네트워크가 느리다”로 오진하게 된다.
한 문장 정리
PonsWarp는 수신 writer 버퍼가 48MiB에 차면 PAUSE하고 16MiB까지 비워지면 RESUME하는 high/low watermark로 송신 속도와 저장 속도를 맞춘다.
관련 개념: [개념 14 — DataChannel] · [개념 15 — prepare-ahead 크레딧] · [개념 16 — in-flight 윈도우]
high와 low를 같은 값으로 두면 안 되는 이유
하나의 기준만 두고 “48MiB 이상이면 pause, 48MiB 아래면 resume”라고 하면 버퍼가 경계 주변에서 계속 상태를 바꾼다. 네트워크 drain이 1바이트만 일어나도 재개하고, 다음 패킷에서 다시 멈추는 현상이 생긴다.
PonsWarp는 48MiB에서 멈추고 16MiB까지 내려온 뒤 재개한다. 이 차이가 hysteresis다. 저장 장치가 잠깐 느려진 정도는 버티고, 충분히 회복된 뒤에만 다시 속도를 올린다.
writer와 sender의 상태 전이
textRECEIVING + buffer < 48MiB → 계속 수신 RECEIVING + buffer ≥ 48MiB → PAUSE PAUSED + buffer > 16MiB → 계속 대기 PAUSED + buffer ≤ 16MiB → RESUME
resolveWriterBackpressureAction은 side effect 없는 순수 함수다. 따라서 실제 writer를 만들지 않고 high/low 경계와 상태 전이를 테스트할 수 있다.
resume 예산
화면 꺼짐이나 모바일 background에서는 resume이 여러 번 필요할 수 있다. 하지만 무한 재시도는 사용자가 영원히 기다리게 만든다. MAX_RESUME_ATTEMPTS=12는 자동 복구의 한계선이다.
재구축할 때는 resume 횟수뿐 아니라 마지막 실패 이유와 마지막 persisted offset도 함께 기록해야 한다.