[PonsWarp 개념 교실 8] 청크 크기 16~192KiB — 어떻게 정해졌나
쉽게 말하면, 청크 크기는 "너무 작으면 오버헤드가, 너무 크면 SCTP와 메모리가" 걸린다. PonsWarp는 연결 경로(host/srflx/relay)에 따라 64192KiB를 고르고, 16KiB192KiB로 클램프한다.

"파일을 크게 자를수록 빨라지지 않나?"라는 직관과 다른 답을 이 글에서 풀어본다.
이게 왜 필요한가 — 청크 크기가 만지는 세 가지
청크 크기는 전송의 세 가지 비용을 동시에 건드린다.
1. 오버헤드 비용. 조각 하나마다 22바이트 헤더가 붙는다. 16KiB 조각이면 0.13%, 192KiB 조각이면 0.01%다. 작을수록 오버헤드가 커진다.
2. 메모리 비용. 각 조각은 암호화 워커 버퍼와 전송 큐에 실린다. 조각이 클수록 in-flight(동시에 떠 있는) 바이트가 커진다. (in-flight 제어는 "흐름 제어" 편에서 다룬다.)
3. 재전송 비용. SCTP는 손실된 메시지 단위로 재전송한다. 조각 하나가 손실되면 그 조각 전체를 다시 보낸다. 조각이 클수록 한 번의 손실 비용이 크다.
이 세 가지가 서로 반대 방향으로 당기므로, "가장 큰 것"이 아니라 "균형점"이 답이다.
상한을 정하는 것 — SCTP 메시지 제한
청크를 아무리 키워도 SCTP가 허용하는 메시지 크기에 부딪힌다. 코드 주석이 이 한계를 직접 언급한다.
typescript// PonsWarp/src/utils/constants.ts export const CHUNK_SIZE_MAX = 192 * 1024; // 192KB ceiling (safe under 256KB)
192KiB 상한은 SCTP 메시지 크기(~256KB) 아래로 안전하게 잡은 값이다. 암호화된 패킷은 헤더(38B) + 태그(16B)가 추가되므로, 평문 청크는 상한 아래로 여유를 둬야 한다. 그래서 "192KiB 기본값 + 256KB 안전선"이 된다.
흥미로운 점은 빠른 길(host)일수록 큰 청크를 쓰는 것이다. host(같은 LAN)는 지연이 낮고 손실이 적어서 큰 조각이 이득이고, relay(TURN 경유)는 지연이 높아서 작은 조각으로 메시지 수를 늘려 파이프를 채운다.
경로별 프로파일 — 코드가 정해 둔 표
PonsWarp는 연결 경로마다 다른 청크 크기를 갖는 튜닝 프로파일을 코드에 상수로 정의해 두었다.
typescript// PonsWarp/src/utils/transferFlowControl.ts DIRECT_HOST_TRANSFER_TUNING_PROFILE // pathKind: 'host', chunkSize: 192KiB HOST_ELEVATED_RTT_TRANSFER_TUNING_PROFILE // 고RTT host: 64KiB DIRECT_SRFLX_TRANSFER_TUNING_PROFILE // pathKind: 'srflx', chunkSize: 128KiB RELAY_TRANSFER_TUNING_PROFILE // pathKind: 'relay', chunkSize: 64KiB UNKNOWN_TRANSFER_TUNING_PROFILE // pathKind: 'unknown', chunkSize: 128KiB
| 경로 | 의미 | 청크 | 이유 (주석) |
|---|---|---|---|
| host | 같은 LAN, 낮은 지연 | 192KiB | 큰 메시지 + 두꺼운 SCTP 큐 |
| host elevated-RTT | VPN/CGNAT, 높은 RTT | 64KiB | 작은 조각 → in-flight 메시지 수 증가로 BDP 충전 |
| srflx | NAT 통과, 공인 IP 경유 | 128KiB | host와 relay의 중간 |
| relay | TURN 경유 | 64KiB | 고RTT 릴레이에서 앱 큐 상한 유지 |
| unknown | 분류 불가 | 128KiB | 안전한 중간값 |
선택 로직은 ICE 후보 타입(host/srflx/relay)을 보고 프로파일을 고른다. 같은 파일도 연결 경로에 따라 조각 크기가 달라진다.
경계값이 만들어진 과정 — 벤치마크의 흔적
프로파일 주석에는 튜닝의 실제 흔적이 남아 있다.
// 20MiB re-broke DCs; 8–12MiB completed with decrypt worker at ~3.8 MB/s peak.
// Climb only after multi-run stability at this band.
maxInFlightBytes: 12 * MIB,
20MiB의 in-flight가 DataChannel을 끊었고, 8~12MiB에서 안정적으로 완료됐다는 기록이다. 청크 크기와 in-flight는 별개의 값이지만, "어떤 값이 망가졌는지"를 측정으로 확인하고 상한을 낮췄다는 실험적 접근이 드러난다. 이 수치는 나중에 "성능 체인" 편에서 다시 나온다.
클램프 — 안전망
청크 크기는 코드에서 16KiB~192KiB로 클램프된다. 어떤 프로파일이든 이 범위를 벗어나지 않게 한다.
typescript// PonsWarp/src/utils/constants.ts export const CHUNK_SIZE_MIN = 16 * 1024; // 16KB export const CHUNK_SIZE_MAX = 192 * 1024; // 192KB ceiling
하한 16KiB는 "너무 잘게 쪼개면 헤더 오버헤드가 커지는" 지점, 상한 192KiB는 "SCTP 안전선"이다. 이 안에서 경로별 프로파일이 움직인다.
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| 하한 | 16KiB (오버헤드 방지) | constants.ts:14 |
| 기본(host) | 192KiB (SCTP 안전선 아래) | constants.ts:15-16 |
| relay 청크 | 64KiB | constants.ts:30 |
| srflx 청크 | 128KiB | transferFlowControl.ts:213-217 |
| 고RTT host | 64KiB | transferFlowControl.ts:205-210 |
| 경로 분류 | ICE 후보 타입 → 프로파일 선택 | transferFlowControl.ts:236-243 |
| 튜닝 근거 | 20MiB가 DC를 끊음, 8~12MiB 안정 | transferFlowControl.ts:194 |
지금 돌아보는 판단
세 가지를 정리한다.
첫째, 청크 크기는 "커서 좋다"가 아니라 "경로에 맞춘다"다. 같은 파일이라도 LAN(192KiB)과 TURN 릴레이(64KiB)에서 조각 크기가 다르다. 고RTT 경로는 작은 조각으로 메시지 수를 늘려 대역폭·지연 곱(BDP)을 채운다. 직관과 반대인데, 실제 네트워크에서는 맞는 선택이다.
둘째, 상한은 브라우저 API가 아니라 SCTP가 정한다. 192KiB 상한의 근거는 "256KB 미만으로 안전하게"다. 프로토콜의 물리적 한계를 코드 주석에 남겨 둔 것은 재구축할 때 같은 실수를 반복하지 않게 하는 좋은 관행이다.
셋째, 수치는 실험이 만든다. "20MiB가 끊었다"는 주석은 단순 튜닝이 아니라 측정 → 실패 → 하향의 과정을 보여준다. 이런 흔적이 없으면 다음 개발자가 다시 20MiB를 시도할 것이다.
한 문장 정리
청크 크기는 헤더 오버헤드·메모리·재전송 비용의 균형점이고, PonsWarp는 SCTP 안전선(192KiB) 아래에서 ICE 경로에 따라 64192KiB를 고르며, 16192KiB로 클램프한다.
관련 개념: [개념 7 — 22바이트 패킷 헤더] (오버헤드의 원인) · [개념 16 — in-flight 윈도우] (청크가 실리는 큐)
다음 글: [개념 9 — 재정렬 버퍼: Arena·free-list·BTreeMap·LRU]