From 8140a59205911170e151c95ed5596db7aa0d5ba5 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Tue, 29 Sep 2026 11:49:32 +0100 Subject: Measure a link on paths of loss, reordering and a bottleneck, against quinn (097) M6 of design 011: with a 50 Mbit/s bottleneck, a link carries 21-41 times quinn's goodput when packets reorder, the same under random loss, and 1.3-3.5 times with both; its p99 at 50 records a second is the same or shorter; beside TCP CUBIC it takes 0.48-0.55 of the path, inside the guardrail. Only on a clean path is it 3-5% below quinn. --- docs/design/011-pure-transport.md | 7 +- docs/research/096-per-delivery-cpu.md | 2 + docs/research/097-lossy-paths.md | 210 ++++++++++++++++++++++++++++++++++ 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 docs/research/097-lossy-paths.md (limited to 'docs') diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md index d50b593..be25a86 100644 --- a/docs/design/011-pure-transport.md +++ b/docs/design/011-pure-transport.md @@ -1,6 +1,6 @@ # 011 — 연결 없는 전송: 전송의 상태를 앱의 데이터로 -작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 1단계를 구현했고 M1, M2, M4, M5를 쟀다. +작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 1단계를 구현했고 M1, M2, M4, M5, M6를 쟀다. [외부 피드백 002](../outside-feedback/feedback-002.md)의 "QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다. 1단계(링크 시제품과 kill -9 검사)는 [093](../research/093-connectionless-link.md)에 있다. 어느 쪽을 죽여도 핸드셰이크 없이 @@ -22,6 +22,11 @@ M5는 [096](../research/096-per-delivery-cpu.md)에 있다. 처음에는 작은 25 % 적고, 61 B에서 1.06–1.12배다. 설계가 적은 송수신 경로(GSO, GRO, 묶어 받기)를 이때 넣었고, 작은 기록을 담는 RECORDS 프레임을 더했다. +M6는 [097](../research/097-lossy-paths.md)에 있다. 처음에는 패킷 순서만 바뀌어도 링크가 1.5 Mbit/s로 주저앉았다. 늦게 온 패킷을 +알아보고 혼잡 사건을 되돌리게 하고, CUBIC, 패킷 경계에서 자르는 부분, 경로 MTU, 페이싱을 넣었다. 50 Mbit/s 병목에서 재정렬 +아래 quinn의 21–41배, 무작위 손실 아래 같은 처리량, 같거나 짧은 꼬리 지연이다. TCP CUBIC 옆에서 대역의 0.48–0.55를 쓴다. +가드레일(±25 %) 안이다. + 2026-09-29에 링크를 `link/`로 뗐다([009](009-layout.md)). 프로토콜은 이제 소켓, 스레드, 시계가 없는 상태이고 (`link/link-protocol.h`), UDP와 스레드는 그 위의 층이다. 그래서 한 스레드가 링크 여럿을 들 수 있다. M2를 이 보내는 쪽으로 다시 돌렸더니 결과가 같았다. 다만 재시작 뒤 가장 늦은 저장이 1초였다. 받았다고 했지만 저장 소식이 없는 기록을 1초 기다리기 diff --git a/docs/research/096-per-delivery-cpu.md b/docs/research/096-per-delivery-cpu.md index df2718c..fe45954 100644 --- a/docs/research/096-per-delivery-cpu.md +++ b/docs/research/096-per-delivery-cpu.md @@ -122,6 +122,8 @@ pure-serve 두 대를 잇고, 보내는 앱에 주문 2만 건을 쌓은 뒤 받 설계해야 한다. - H4("활성 링크의 전달당 CPU는 QUIC 기준선보다 나쁘지 않다")는 1 KiB 이상에서 선다. 작은 기록에서는 거의 선다. quinn의 값에는 기록마다의 확인과 정확히 한 번이 없다는 점을 함께 본다. +- 뒤에 M6([097](097-lossy-paths.md))을 하며 세 가지를 바꿨다: 패킷 경계에서 자르는 부분, 경로 MTU(1,452 B), 페이싱이다. 그 + 뒤 이 조건에서 기록 하나의 CPU가 1 KiB와 16 KiB에서 12 % 더 줄었다. 61 B는 같다(나누기 전 판과 번갈아 세 라운드). ## 한계와 다음 diff --git a/docs/research/097-lossy-paths.md b/docs/research/097-lossy-paths.md new file mode 100644 index 0000000..c30d9d8 --- /dev/null +++ b/docs/research/097-lossy-paths.md @@ -0,0 +1,210 @@ +# 097 — 손실과 재정렬, 병목: 재정렬에 무너지지 않고, 손실에서 QUIC만큼 + +2026-09-29. [설계 011](../design/011-pure-transport.md)의 M6다. [096](096-per-delivery-cpu.md)이 깨끗한 루프백에서 기록 하나의 +CPU를 쟀다면, 여기서는 지연, 손실, 재정렬, 병목이 있는 경로에서 링크 하나가 얼마나 나르는지, 기록 하나가 얼마나 늦는지, 옆의 +TCP와 대역을 어떻게 나누는지를 quinn과 댄다. + +착수 기준: + +- **바꿀 결정.** 011의 가드레일이다. TCP CUBIC과 나눈 몫이 ±25 %를 벗어나면(0.375–0.625 밖), 고친 뒤에야 채택한다. 처리량과 + 꼬리 지연은 quinn과 댄다. +- **최소 확인.** RTT 20·100 ms, 손실 0·1·5 %, 재정렬, 50 Mbit/s 병목에서 세 가지를 본다. 쌓인 1 KiB 기록을 푸는 처리량, 초당 + 50건의 꼬리 지연, TCP CUBIC 흐름 옆의 몫이다. +- **중단 기준.** 위의 ±25 %다. + +## 쉬운 말로 + +- 알아낸 것: + - 처음에는 패킷 순서만 바뀌어도 링크가 주저앉았다. 잃은 패킷이 없는데 50 Mbit/s 경로에서 1.5 Mbit/s였다. RFC 9002의 고정 + 문턱 때문이고, quinn도 같은 규칙을 쓴다. + - 늦게 온 패킷을 알아보고 창을 되돌리게 하니, 순서가 흔들려도 흔들리지 않을 때와 같은 45 Mbit/s를 낸다. 같은 경로에서 + quinn은 1–2 Mbit/s다(21–41배). + - 무작위 손실 아래 처리량은 quinn과 같다(0.97–1.07배). 손실과 재정렬이 함께 있으면 quinn의 1.3–3.5배다. + - 초당 50건의 꼬리 지연(p99)은 quinn과 같거나 짧다. RTT 100 ms에 손실 5 %에서 418 대 547 ms다. + - TCP CUBIC 흐름 옆에서 대역의 0.48–0.55를 쓴다. 가드레일(0.375–0.625) 안이다. 같은 경로에서 quinn은 지연이 1 ms만 흔들려도 + 0.02–0.07로 밀려난다. + - 깨끗한 경로의 처리량만 quinn보다 3–5 % 낮다. 패킷마다 붙는 머리 때문이다. +- 아직 모르는 것: 실제 무선·셀룰러 경로, 흐름이 많거나 RTT가 서로 다를 때, ngtcp2와의 비교, 저장소 옮기기(M3), 보안 검사. +- 사용 경험의 차이: 와이파이나 셀룰러처럼 패킷 순서가 흔들리는 망에서도 링크가 느려지지 않는다. 이 에뮬레이션에서 quinn은 + 1–3 ms의 흔들림만으로 한 자릿수 Mbit/s로 떨어졌다. + +## 한 것 + +### 프로토콜을 시간 밖으로 꺼냈다 + +측정 전에 링크를 `link/`로 떼고 둘로 나눴다([009](../design/009-layout.md)). `link/link-protocol.h`는 소켓, 스레드, 시계가 없는 +상태다. 패킷과 시각이 들어가고 패킷이 나온다. 그래서 두 쪽을 가상 시간의 네트워크에 놓을 수 있다. + +- 시뮬레이터([sim.c](../../research/experiments/020-lossy-paths/sim.c))는 병목(속도, 큐, 드롭테일), 지연, 흔들림(재정렬), 무작위 + 손실을 흉내 낸다. 같은 씨앗이면 같은 결과가 나온다. 1 KiB 기록 3만 건이 1초 안팎에 끝난다. +- 처음 돌린 결과가 문제를 곧바로 보여 줬다(RTT 20 ms, 50 Mbit/s, 1 KiB). + +| 경로 | 처리량 | 실제로 잃은 패킷 | 손실로 판정 | 가장 작은 창 | +|---|---:|---:|---:|---:| +| 깨끗함 | 41 Mbit/s | 0(큐 225) | 225 | 12,000 B | +| 재정렬만(흔들림 5 ms) | 1.5 Mbit/s | 0 | 308 | 2,400 B | +| 손실 1 % | 5.5 Mbit/s | 130 | 49 | 4,244 B | + +재정렬만 있고 잃은 것이 없는데, 순서가 바뀐 패킷을 손실로 판정해 창이 최소로 주저앉았다. RFC 9002의 고정 문턱(나중 패킷 3개, +RTT의 9/8) 때문이다. quinn도 같은 규칙을 쓴다. + +### 늦게 온 패킷 + +손실로 판정한 비행을 링에 잠시(RTT 두 번) 남겨 둔다. 그 사이에 확인이 오면 늦게 온 것이다. + +- 그 패킷이 나른 부분은 받은 것으로 친다. 아직 다시 보내지 않았으면 다시 보내지 않는다. +- 문턱을 넓힌다. RACK처럼 두 가지다. 나중 패킷 문턱은 실제로 본 재정렬 깊이로, 시간 문턱은 최소 RTT의 4분의 1씩 늘린다. 재정렬 + 없는 혼잡 사건이 16번 이어지면 반으로 줄인다. +- 혼잡 사건을 되돌린다. 앞 사건의 손실이 정해지기 전에 이어진 사건들을 한 에피소드로 묶는다. 에피소드의 손실이 모두 늦게 온 + 것이면, 첫 사건 전의 창, 문턱, CUBIC 상태로 돌아간다. 하나라도 진짜 손실이면(링에서 늦은 확인 없이 떠나면) 되돌리지 않는다. + +시뮬레이터에서 재정렬만 있을 때의 처리량이 RTT 20 ms에서 1.5 → 43.6 Mbit/s, RTT 100 ms에서 10 → 40.1 Mbit/s가 됐다. 재정렬이 +없을 때와 같다. + +### CUBIC, 쓰지 않은 창 + +- 혼잡 회피를 NewReno에서 CUBIC(RFC 9438)으로 바꿨다. quinn의 기본값이고, TCP CUBIC과 나란히 설 때 같은 규칙이어야 한다. + Reno 친화 추정은 RTT마다 0.53 데이터그램이다. 배포된 CUBIC(Linux, quinn)이 그렇게 한다. RFC 9438처럼 이전 최대를 넘은 뒤 + 1로 올리지는 않는다. +- 창을 다 쓰지 않을 때는(보낼 것이 없거나 받는 쪽 크레딧에 막히면) 창을 키우지 않는다(RFC 9002 §7.8). 전에는 크레딧에 막힌 + 채로 창이 1만 패킷 넘게 자랐다. +- Reno 추정은 확인이 올 때만큼, 또는 시간이 가는 만큼(RFC 8312) 가운데 앞선 쪽으로 키운다. 앱이 초당 몇 건만 보내는 링크는 + 확인이 드물어, 손실 뒤 필요한 창으로 돌아가는 데 오래 걸렸다. + +### 기록을 패킷 경계에서 자른다 + +전에는 기록을 1,000 B 조각으로 나눴다. 1 KiB 기록은 패킷에 하나씩만 들어가 패킷의 13 %가 비었다. + +- 이제 패킷의 남은 자리에 64 B 이상 들어가면 기록을 그만큼 자른다. 자른 자리는 바뀌지 않는다. 그래서 한 패킷이 나른 것은 늘 + 온전한 부분들이다. +- 받는 쪽은 조각 표 대신 받은 바이트 범위를 합쳐 둔다. 순서대로 오는 부분은 할당 없이 줄 끝에서 바로 이어 붙인다. +- 루프백에서 패킷이 1 KiB 기록에 8 %, 16 KiB 기록에 10 % 줄었다. 기록 하나의 CPU는 61 B와 1 KiB에서 같고, 16 KiB에서 5 % + 줄었다. + +### 경로가 나르는 만큼 큰 패킷 + +1,200 B로 시작한다. RTT를 알면 1,452 B로 채운 PING을 혼자, 조각내지 말라는 표시(DF)를 달고 보낸다(DPLPMTUD, RFC 8899). 확인이 +오면 그 크기를 쓴다. 탐침을 잃는 것은 혼잡이 아니다. 1초 뒤 다시, 세 번까지, 그 뒤로는 10분마다 해 본다. + +- CUBIC은 데이터그램 단위로 창을 키운다. 그래서 같은 손실률에서 패킷이 큰 쪽이 바이트로 더 빨리 자란다. quinn과의 손실 아래 + 차이는 대부분 이것이었다. +- GSO를 소켓 옵션이 아니라 묶음마다 제어 메시지로 청한다. 소켓 옵션이 걸려 있으면, 혼자 보낸 1,452 B 탐침을 시스템이 + 1,200 B와 252 B로 잘라 보냈다. `make check`의 UDP 검사가 이것을 잡았다. +- 루프백에서 패킷이 19–27 % 줄고, 기록 하나의 CPU가 1 KiB에서 12 %, 16 KiB에서 12 % 줄었다([096](096-per-delivery-cpu.md)의 + 조건, 나누기 전과 번갈아 세 라운드). + +### 페이싱 + +창을 RTT에 걸쳐 1.25배(느린 시작에서 2배)의 빠르기로 보낸다. 한 번에는 2 ms어치(10–256 데이터그램)까지만 보낸다(RFC 9002 §7.7). +보내는 쪽 상태가 다음에 보낼 수 있는 때를 알려 준다. + +### 측정 경로 + +비특권 이름공간의 루프백 하나를 경로로 쓴다([run.py](../../research/experiments/020-lossy-paths/run.py)). + +- 방향마다 netem이 지연(RTT의 절반), 흔들림, 손실을 준다. 받는 쪽 포트(40000–40007)로 가는 쪽만 그 뒤에 tbf 병목(50 Mbit/s, 큐는 + RTT 한 번어치)을 지난다. 되돌아오는 답과 TCP의 확인은 병목 뒤에 줄 서지 않는다. +- Pure와 quinn은 GSO 없이 보내고, TCP의 세그먼트는 1,500 B를 넘지 않게 한다. 큐 규칙은 GSO 묶음과 TSO 조각을 패킷 하나로 + 다루기 때문이다. +- 몫을 잴 때는 흐름마다 따로 된 프로세스를 동시에 띄우고, 60초 동안 20초부터 센다. 이때 경로의 지연을 1 ms 흔든다. 흔들림이 + 없으면 드롭테일 큐가 흐름들의 위상만으로 한쪽 패킷을 여덟 배 더 버린다(Floyd와 Jacobson, 1992). + - 흔들림 없이 쟀을 때 Pure는 TCP에게서 0.63–0.77, quinn에게서 0.69를 가져갔다. + - 같은 조건에서 Pure끼리는 0.47, 큐를 fq_codel로 바꾸면 0.49였다. + - 1 ms 흔들림에서는 RTT 20 ms에서 0.53이다. +- 이 경로를 맞추는 데 측정 여섯 벌을 버렸다. 이유는 이렇다. + - tbf 밑에 netem을 두면 tbf의 큐 한도가 사라진다. + - 답이 데이터 뒤에 줄 선다. + - GSO 묶음이 패킷 하나로 다뤄진다. + - 먼저 띄운 흐름이 몫을 더 가져간다. + - 흔들림이 없으면 위상 효과가 생긴다. + - 하네스가 재정렬을 틀림으로 셌다. + +## 결과 + +라즈베리 파이 5, 비특권 이름공간의 루프백이다. 병목은 50 Mbit/s이고 큐는 RTT 한 번어치다. 번갈아 세 라운드의 중앙값이다. +Pure는 링크와 리스너만 잰다([transport.c](../../research/experiments/019-idle-links/transport.c), 앱과 저장 없음). quinn은 한 +연결의 한 스트림이다. 기록은 1,013 B(1 KiB 항목의 주문)다. 원시 결과는 `research/experiments/020-lossy-paths/`의 +`2026-09-29-paths-linux-aarch64.jsonl`에 있다. 앞 단계의 결과도 같은 곳에 있다(`-parts-`는 세 라운드, `-mtu-`는 한 라운드). + +**쌓인 기록 풀기(Mbit/s, 기록의 바이트).** 흔들림 3 ms면 패킷 순서가 바뀐다. + +| RTT | 손실 | 흔들림 | Pure | quinn | Pure/quinn | +|---:|---:|---:|---:|---:|---:| +| 20 ms | 0 | 0 | 45.4 | 46.8 | 0.97 | +| 20 ms | 0 | 3 ms | 45.4 | 2.18 | 20.8 | +| 20 ms | 1 % | 0 | 6.84 | 6.54 | 1.05 | +| 20 ms | 1 % | 3 ms | 7.30 | 2.07 | 3.53 | +| 20 ms | 5 % | 0 | 3.08 | 2.89 | 1.07 | +| 20 ms | 5 % | 3 ms | 3.27 | 1.62 | 2.02 | +| 100 ms | 0 | 0 | 44.9 | 47.3 | 0.95 | +| 100 ms | 0 | 3 ms | 45.2 | 1.11 | 40.8 | +| 100 ms | 1 % | 0 | 1.58 | 1.56 | 1.01 | +| 100 ms | 1 % | 3 ms | 1.43 | 0.82 | 1.74 | +| 100 ms | 5 % | 0 | 0.60 | 0.62 | 0.97 | +| 100 ms | 5 % | 3 ms | 0.65 | 0.49 | 1.33 | + +**단계마다, RTT 20 ms에서.** 무엇이 얼마나 바꿨는지다. + +| Pure | 깨끗함 | 재정렬 | 손실 1 % | 손실 5 % | +|---|---:|---:|---:|---:| +| 늦은 패킷, CUBIC, 부분까지(세 라운드) | 44.3 | 44.3 | 5.83 | 2.26 | +| + 경로 MTU(한 라운드) | 45.4 | 45.4 | 8.24 | 2.80 | +| + 페이싱, Reno 0.53(세 라운드, 최종) | 45.4 | 45.4 | 6.84 | 3.08 | +| quinn(최종과 같은 때) | 46.8 | 2.18 | 6.54 | 2.89 | + +경로 MTU를 쓴 판의 손실 1 % 값(8.24)은 RFC 9438처럼 Reno 추정을 이전 최대 뒤에 RTT마다 1 데이터그램으로 키운 판이다. 배포된 +CUBIC과 같게 0.53으로 두니 6.84가 됐다. + +**초당 50건의 꼬리 지연(ms, 넣은 때부터 넘겨받은 때까지).** Pure / quinn이다. + +| RTT | 손실 | p50 | p99 | 최대 | +|---:|---:|---|---|---| +| 20 ms | 0 | 10 / 10 | 10 / 10 | 10 / 10 | +| 20 ms | 1 % | 10 / 10 | 50 / 50 | 50 / 83 | +| 20 ms | 5 % | 10 / 10 | 70 / 96 | 90 / 137 | +| 100 ms | 0 | 50 / 50 | 50 / 50 | 50 / 50 | +| 100 ms | 1 % | 50 / 50 | 170 / 196 | 190 / 282 | +| 100 ms | 5 % | 70 / 110 | 418 / 547 | 517 / 686 | + +잃은 패킷은 다음 패킷의 확인이 오는 순간에 손실로 판정해 곧바로 다시 보낸다(RTT 100 ms, 20 ms 간격이면 보낸 뒤 약 120 ms). +p99가 170 ms인 까닭이다. 시뮬레이터도 같은 값을 준다. + +**TCP CUBIC 옆의 몫(60초 가운데 20초부터, 경로 지연 1 ms 흔들림).** + +| RTT | Pure | 라운드별 | TCP끼리 | quinn | +|---:|---:|---|---:|---:| +| 20 ms | 0.484 | 0.517, 0.484, 0.483 | 0.518 | 0.065 | +| 100 ms | 0.549 | 0.560, 0.545, 0.549 | 0.482 | 0.018 | + +- TCP끼리의 몫도 라운드마다 0.44–0.58 사이에서 움직인다. Pure는 그 범위 안이다. +- quinn은 1 ms 흔들림을 손실로 읽어 창을 줄인다. 같은 흔들림에서 quinn 혼자 몰아 보낼 때도 1–2 Mbit/s였다. +- 큐를 fq_codel로 바꾸면(흐름마다 공정하게 줄 세움, 한 라운드) Pure 0.49·0.48, quinn 0.50·0.50이다. + +## 판정 + +- **가드레일은 통과다.** TCP CUBIC 옆에서 Pure의 몫은 0.48(RTT 20 ms)과 0.55(RTT 100 ms)다. 라운드별로 0.48–0.56으로, + 0.375–0.625 안이고 TCP끼리의 흔들림과 같은 범위다. +- **M6의 나머지.** + - 재정렬에서 quinn의 21–41배다. + - 무작위 손실에서는 같다(0.97–1.07배). + - 손실과 재정렬이 함께 있으면 1.3–3.5배다. + - 꼬리 지연은 같거나 짧다. + - 깨끗한 경로에서만 3–5 % 낮다. +- **M5는 더 좋아졌다.** 부분, 경로 MTU, 페이싱을 넣은 뒤 기록 하나의 CPU가 1 KiB와 16 KiB에서 12 % 줄었다. 61 B는 같다 + (096의 조건). +- 011의 5단계("M5·M6 가드레일을 잰다")가 끝났다. 남은 것은 4단계다: 에폭 바꿈, 경로 바뀜, 저장소 옮기기(H3, M3), 보안 검사. + +## 한계와 다음 + +- **경로는 에뮬레이션이다.** + - 루프백 하나에 netem과 tbf를 얹었다. 실제 무선·셀룰러, ECMP의 재정렬, 여러 AQM이 아니다. + - 몫은 두 흐름, 같은 RTT만 봤다. +- **무작위 손실 아래는 손실을 혼잡으로 읽는 모든 제어와 같다.** 손실 5 %, RTT 100 ms에서 0.6 Mbit/s다. 넘으려면 모델 기반 제어 + (BBR 계열)나 여분 전송이 필요하다. 그것은 가드레일(TCP와의 몫)과 함께 따져야 한다. +- **꼬리 지연은 확인으로 알 수 있는 가장 이른 때에 다시 보낸다.** 더 줄이려면, 앱이 한가해 창이 빌 때 기록을 한 번 더 보내는 + 여분이 필요하다. +- **깨끗한 경로의 3–5 %.** 패킷마다 스트림 이름(약 25 B)을 싣는다. 096의 짧은 이름표 과제와 같다. +- **경로 MTU는 늘리기만 한다.** 경로가 도중에 좁아지면(블랙홀) 알아채지 못한다. RFC 8899의 블랙홀 탐지는 다음이다. +- **페이싱은 보내는 스레드의 poll(ms 단위)에 묶인다.** +- **QUIC 비교는 quinn 0.11 하나다.** GSO 없이 쟀다. ngtcp2는 아직이다. quinn이 흔들림에 무너진 것은 RFC 9002의 고정 문턱 때문으로 + 보지만, quinn의 코드로 확인하지는 않았다. -- cgit v1.2.3