aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 11:49:32 +0100
committereunhhu <[email protected]>2026-09-29 11:49:32 +0100
commit8140a59205911170e151c95ed5596db7aa0d5ba5 (patch)
tree4ac10d2390ad4486f278c5b37e4a5e5966b93657 /docs
parent34fc79fe568784294a274851e3040c1171af1fe2 (diff)
downloadpure-8140a59205911170e151c95ed5596db7aa0d5ba5.tar.gz
pure-8140a59205911170e151c95ed5596db7aa0d5ba5.zip
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.
Diffstat (limited to 'docs')
-rw-r--r--docs/design/011-pure-transport.md7
-rw-r--r--docs/research/096-per-delivery-cpu.md2
-rw-r--r--docs/research/097-lossy-paths.md210
3 files changed, 218 insertions, 1 deletions
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의 코드로 확인하지는 않았다.