diff options
| author | eunhhu <[email protected]> | 2026-09-28 16:57:14 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-28 16:57:14 +0100 |
| commit | 502f70dbd66debcf1cb0328427831ec14926504b (patch) | |
| tree | 87c96959c47fe4093e100cadeec992a6954e4604 /docs/design | |
| parent | 5e4cf1629482a2b36d229ffb0005ce31046eaa6c (diff) | |
| download | pure-502f70dbd66debcf1cb0328427831ec14926504b.tar.gz pure-502f70dbd66debcf1cb0328427831ec14926504b.zip | |
Redesign 011: no connections, the transport's state as the app's data
The first draft replaced a few of QUIC's layers with what Pure already
has, every technique known and the gains constant factors, and it kept
the connection. This one removes it. Nodes have keys, links are the app's
wiring, paths are a cache of where the last authenticated packet came
from, and a link's transport state (key epoch, stored-through number,
heard numbers, packet-number reservation) is a row committed atomically
with the app's state; memory holds only active links and per-path
congestion state. So a restart of either node is invisible to the other
(no handshake, nothing lost or doubled), an idle link costs a row on
disk rather than process memory, and moving the store moves its links.
Forward secrecy comes from key epochs rather than per-connection
handshakes; replay protection survives restarts because heard numbers
are stored with the events they applied (OSCORE, the closest prior art,
needs an Echo round trip there).
Hypotheses H1-H4 and a measurement plan against quinn and ngtcp2:
memory per idle link at 10k to 1M links, kill -9 under load with 10k
active links, moving the store, first contact, and CPU, loss and fairness
as guardrails; stop if an idle link is not at least a hundred times
cheaper than a QUIC connection, if restarts need handshakes, or if
anything is lost or doubled. Beyond it: speculative execution of
verified, deterministic modules, for interactions that do not wait.
Diffstat (limited to 'docs/design')
| -rw-r--r-- | docs/design/006-native-protocol.md | 5 | ||||
| -rw-r--r-- | docs/design/011-pure-transport.md | 318 |
2 files changed, 172 insertions, 151 deletions
diff --git a/docs/design/006-native-protocol.md b/docs/design/006-native-protocol.md index c4dfd3b..09c4075 100644 --- a/docs/design/006-native-protocol.md +++ b/docs/design/006-native-protocol.md @@ -8,8 +8,9 @@ TLS 1.3/TCP 위에 실은 피어 호스트를 검증했다. [026 — scalar 피� [030 — 문서 피어·출력 소비 선택](../research/030-authenticated-document-peer.md)을 본다. 이 설계의 일반 RPC·QUIC·새 전송 후보가 구현된 것은 아니다. -2026-09-28: 노드 사이 전달의 새 UDP 전송 후보(Pure 전송)는 설계와 quinn·ngtcp2 대비 측정 계획을 -[011](011-pure-transport.md)에 적었다. 아래 "새 UDP 전송은 … 별도 연구 후보로 연다"는 조건은 그 0단계(비용 분해)가 판정한다. +2026-09-28: 노드 사이 전달의 새 UDP 전송 후보는 [011 — 연결 없는 전송](011-pure-transport.md)이다. 링크의 전송 +상태를 앱 상태와 한 커밋에 두어 연결 개념을 없앤다. 아래 "새 UDP 전송은 … 별도 연구 후보로 연다"는 조건은 그 가설과 +중단 기준이 판정한다. ## 결정 diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md index 0d019cd..52bdd44 100644 --- a/docs/design/011-pure-transport.md +++ b/docs/design/011-pure-transport.md @@ -1,199 +1,219 @@ -# 011 — Pure 전송: 저장한 기록을 곧바로 보내는 UDP 전송 +# 011 — 연결 없는 전송: 전송의 상태를 앱의 데이터로 -작성일: 2026-09-28. 상태: 설계. 구현과 측정 전이다. [외부 피드백 002](../outside-feedback/feedback-002.md)의 -"QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다. +작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 구현과 측정 전이다. +[외부 피드백 002](../outside-feedback/feedback-002.md)의 "QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다. + +## 첫 판을 버린 까닭 + +첫 판(1953051)은 QUIC의 층 몇 개를 Pure가 이미 가진 장치로 대신하는 설계였다. 저장소를 재전송 버퍼로 쓰고, 봉인을 한 번만 +하고, 첫 패킷에 데이터를 싣는 것이다. 이것은 혁신이라 부를 수 없었다. + +- 기법이 모두 이미 있었다. 끝에서 끝 논증(1984), WireGuard, Aeron, Kafka다. +- 기대할 수 있는 것이 상수배였다. CPU를 1.5–3배 줄이고 첫 연결에서 왕복 하나를 아끼는 정도다. +- 무엇보다 '연결'이라는 개념을 그대로 두었다. QUIC을 조금 가볍게 한 것이었다. + +패킷을 깎아서 얻는 것에는 하한이 있다. 바이트, 왕복, 암호가 그 하한을 정한다. 큰 차이는 무엇을 누가 기억하느냐에서 난다. +Pure는 앱의 상태를 트랜잭션으로 저장하는 런타임이다. 그렇다면 전송의 상태도 거기에 있을 수 있다. 그러면 연결이 필요 없다. + +## 결정 + +Pure 노드 사이에는 연결이 없다. + +- 없는 것: connect·accept·close, 연결마다의 핸드셰이크, 재연결, 세션을 붙잡는 keepalive, 연결 풀, 앱이 받는 "connection + reset", 연결마다 잡아 두는 버퍼. +- 있는 것은 넷이다. + - 노드: 키를 가진다. + - 링크: 배선한 출력 하나에서 입력 하나로 간다. 이미 앱의 배선에 있다. + - 경로: 주소다. 마지막으로 인증된 패킷이 온 곳의 캐시일 뿐이다. + - 패킷. +- 링크의 전송 상태는 저장소의 행이다. 키 에폭, 번호, 확인, 허가가 모두 앱 상태와 같은 커밋에 원자적으로 들어간다. 메모리에 + 있는 것은 활성 링크의 캐시와 경로마다의 혼잡 상태뿐이다. ## 쉬운 말로 -- 알고 싶은 것: Pure 노드끼리 주고받는 전달(보내기·메신저 줄)을 QUIC보다 적은 CPU·메모리·왕복으로 옮길 수 있는가. -- 그럴 수 있다고 보는 까닭: Pure의 전달은 이미 저장되고, 번호가 붙고, 받는 쪽에서 한 번만 적용되고, 봉인된다. QUIC과 TCP+TLS는 - 같은 일(재전송 버퍼, 재전송 방지, 암호, 흐름 제어, 확인 응답)을 한 번 더 한다. 그 겹친 층을 걷어 낼 수 있는지를 본다. -- 사용 경험의 차이(가설): 메신저 줄이 상대 화면에 닿는 시간이 줄고, 처음 연결할 때 기다림이 없다. 와이파이에서 LTE로 바뀌어도 - 전달이 끊기지 않는다. 노드 사이에 HTTPS 인증서가 필요 없다. -- 아직 모르는 것: 전달 하나의 비용 가운데 전송이 차지하는 몫이 얼마인지. 몫이 작으면 새 프로토콜로 얻을 것도 작다. 그래서 그것부터 잰다. -- 근거: 지금은 문헌과 기존 측정([086](../research/086-sealed-sends.md), [088](../research/088-sealed-sessions.md))뿐이다. - QUIC 대비 수치는 아직 없다. +- 서버를 kill -9 하고 다시 띄워도, 붙어 있던 기기들은 알아채지 못한다. 핸드셰이크도 재연결 폭주도 없고, 잃거나 겹치는 메시지도 + 없다. +- 기기 100만 대가 붙은 노드가 쉬는 기기에는 메모리를 쓰지 않는다. 쉬는 기기는 저장소의 행 하나다. +- 기기가 잠에서 깨거나 와이파이에서 LTE로 바뀌면 첫 패킷으로 이어진다. +- 저장소를 다른 기계로 옮기면 링크가 그대로 따라간다. 장애 조치가 곧 저장소 옮기기다. +- 근거: 아직 설계뿐이다. 아래 가설을 측정으로 확인한다. ## 착수 기준 -- **바꿀 결정.** Pure 노드 사이의 전달을 지금의 HTTP/1.1(TLS) 위 HPKE 봉인([083](../research/083-delivery-over-http.md), - [088](../research/088-sealed-sessions.md))에서 무엇으로 옮길지. 후보는 셋이다. - - (가) 이 문서의 Pure 전송(UDP) - - (나) QUIC 스트림 위의 Pure 기록 형식(ngtcp2 또는 quinn) - - (다) 옮기지 않음 - HTTP 전달은 어느 경우든 제3자와 브라우저를 위한 경계로 남는다([006](006-native-protocol.md)). +- **바꿀 결정.** 노드 사이의 전달을 무엇으로 할지 정한다. HTTP 전달은 어느 경우든 제3자와 브라우저를 위한 경계로 남는다 + ([006](006-native-protocol.md)). + - (가) 연결 없는 전송 + - (나) QUIC 위의 Pure 형식 + - (다) 지금의 HTTP 전달([083](../research/083-delivery-over-http.md), [088](../research/088-sealed-sessions.md)) - **남은 질문.** - 1. 전달 하나의 CPU 가운데 전송(형식, 암호, 시스템 호출, 복사, 확인 응답, 전송 상태)의 몫은 얼마이고, 적용·저장의 몫은 얼마인가. - 2. 보내는 앱의 저장소를 재전송 버퍼로 쓸 때 복사나 지연이 늘지 않는가. - 3. 처음 만나는 연결의 첫 패킷에 기록을 실어도 안전한가. 증폭, CPU 소모 공격, 전방 비밀성이 걸린다. - 4. 표준 혼잡 제어를 그대로 쓰면서 공정성을 지킬 수 있는가. - 5. UDP가 막힌 곳에서는 무엇으로 이어 가는가. + 1. 전송 상태를 앱 커밋에 묶으면 패킷마다 동기화가 필요해지지 않는가. + 2. 쉬는 링크 하나의 비용이 정말 행 하나(약 100 B)인가. 깨울 때 첫 패킷에 드는 시간은 얼마인가. + 3. 재시작 뒤 재전송 공격을 막는 창을 잃지 않는가. 가장 가까운 선행 기술인 OSCORE는 여기서 한 왕복이 든다. + 4. 핸드셰이크가 없을 때 전방 비밀성, CPU 소모 공격, 증폭을 어떻게 다루는가. + 5. 활성 링크의 전달당 CPU가 QUIC 기준선보다 나쁘지 않은가. - **최소 확인.** - 1. 먼저 비용 분해다. 쌓인 보내기 풀기(W1)를 지금의 전달과 quinn 기준선에서 돌린다. 전달 하나의 CPU를 층별로 나눈다. - 2. 그다음 시제품이다. 한 세션·한 전달선만 둔다. 손실 탐지는 RFC 9002, 혼잡 제어는 NewReno로 하고, 재전송은 저장소에서 읽는다. - 이것을 W1과 손실·지연(W4)에서 가장 좋은 QUIC 기준선과 번갈아 잰다. -- **중단 기준.** 아래 "중단 기준" 절의 수치다. 요약하면 셋이다. - - 전송의 몫이 작으면 만들지 않는다. - - 시제품이 CPU당 전달에서 1.5배를 못 넘으면 멈추고 (나)를 쓴다. - - 한 번이라도 잃거나 겹치면 되돌린다. + 1. 링크 행을 앱 상태에 두는 시제품을 만든다. 한 노드 쌍에 링크 여럿을 둔다. 어느 쪽이든 kill -9로 죽였다 살린 뒤, 핸드셰이크 + 없이 다음 패킷으로 이어지고 잃거나 겹치는 기록이 없는지 본다. + 2. 쉬는 링크를 1만, 10만, 100만 개로 늘린다. 프로세스 메모리와 저장소 크기의 기울기를 quinn·ngtcp2 연결당 메모리와 댄다. +- **중단 기준.** 아래 "중단 기준" 절에 둔다. ## 의심하는 관례 | 관례 | 이 설계 | |---|---| -| 전송이 신뢰성을 가진다(재전송 버퍼, ACK). 그 위에서 앱은 또 outbox, 재시도, 중복 제거를 가진다 | 보내는 앱의 저장소(outbox)가 재전송 버퍼다. 전송은 잃은 것을 알아채고 저장소에서 그 번호를 다시 읽는다 | -| 전송의 확인 응답과 앱의 "저장했다"는 다른 메시지다 | 한 패킷에 둘 다 싣는다. 패킷 ACK(손실 탐지, RTT)와 전달선마다의 "N번까지 저장했다"다. 보내는 쪽은 뒤의 것으로 덜어 낸다 | -| 전송 암호(TLS, QUIC)와 본문 봉인(HPKE)을 따로 한다 | 직접 잇는 링크는 한 번만 봉인한다. 두 노드의 세션 키로 패킷을 봉인한다. 중계를 지날 때만 기록을 따로 봉인한다 | -| 0-RTT 데이터는 재전송 공격 때문에 위험하다 | 받는 쪽이 번호로 한 번만 적용하므로, 다시 보낸 기록은 적용되지 않는다. 받는 노드의 키는 배선할 때 알고 있으므로, 처음 만나는 연결도 첫 패킷부터 기록을 싣는다 | -| 흐름 제어는 바이트 창이다 | 받는 쪽이 다음 저장에 들일 수 있는 기록 수와 바이트를 알린다(지금 `X-Pure-Batch`가 하는 일을 전송이 한다) | -| 연결은 주소(5-tuple)로 식별한다 | 세션 식별자와 키로 식별한다. NAT 재바인딩, 망 이동에서 주소가 바뀌어도 세션이 이어진다 | -| 선 위의 형식과 저장 형식이 다르다(직렬화, 파싱) | 저장한 기록의 바이트와 필드 표가 곧 선 위의 바이트다. 받는 쪽은 JSON을 읽지 않고 `pure_app_deliver`에 넘긴다 | - -이것은 끝에서 끝 논증(Saltzer, Reed, Clark, "End-to-End Arguments in System Design", ACM TOCS 1984)을 그대로 쓴 것이다. -전달의 완전한 신뢰성은 끝점만 보장할 수 있다. 전송의 신뢰성은 성능을 위한 것일 뿐이다. Pure에는 끝점의 장치(저장, 번호, 한 번 -적용)가 이미 있으므로, 전송은 그것을 빌려 쓰고 제 것을 두지 않는다. +| 통신하려면 연결을 열고(핸드셰이크) 끝나면 닫는다 | 링크는 배선할 때 생긴다. 서로의 키를 알므로 첫 패킷부터 봉인된 기록이 간다 | +| 연결 상태는 프로세스 메모리에 있다 | 저장소의 행이다. 메모리는 그 캐시다 | +| 서버를 다시 시작하면 연결이 모두 끊기고 클라이언트가 다시 잇는다 | 재시작해도 행이 남는다. 다음 패킷이 그대로 이어진다 | +| 연결은 주소로 식별한다 | 키로 식별한다. 주소는 마지막으로 인증된 패킷이 온 곳이다 | +| 전송의 재전송 버퍼, 앱의 outbox와 재시도, 받는 쪽의 중복 제거가 따로 있다 | 보내는 앱의 저장소가 재전송 버퍼다. 받는 쪽이 저장해 둔 들은 번호가 곧 재전송 방지다 | +| 전송 암호와 본문 봉인을 따로 한다 | 직접 링크는 링크의 키 에폭으로 한 번 봉인한다. 중계를 지날 때만 기록을 따로 봉인한다 | +| 흐름 제어는 바이트 창이다 | 받는 쪽이 다음 저장에 들일 수 있는 만큼을 허가한다 | +| 전방 비밀성은 연결마다의 임시 키 교환으로 얻는다 | 연결이 없으므로 에폭 교체(래칫)로 얻는다. 기준은 핸드셰이크가 아니라 키의 나이다 | +| 선 위의 형식과 저장 형식이 다르다 | 저장한 기록의 바이트와 필드 표가 곧 선 위의 바이트다 | ## 구조(후보) -패킷의 바이트 배치는 아직 고정하지 않는다. 시제품과 측정으로 정한다. +패킷의 바이트 배치는 시제품과 측정으로 정한다. + +**링크의 행(저장).** 목록(B+트리, [090](../research/090-tables.md))의 한 항목이다. 대략 100 B다. -**개념.** +- 상대 노드: 키 표의 번호 +- 링크 이름: 출력, 입력 +- 키 에폭: 번호, 보내는 키와 받는 키(AEAD 키와 IV 기반) +- 저장 확인된 번호. 보낸 기록 자체는 이미 outbox에 있다. +- 들은 번호. `pure_app_deliver`가 이미 적용과 함께 저장한다. +- 패킷 번호 예약 상한. 번호를 K개씩 예약해 커밋하고, 재시작하면 상한부터 쓴다(RFC 8613 B.1.1과 같은 방법). 이 커밋은 앱의 + 저장에 실려 가므로 따로 동기화하지 않는다. -- 노드: 키 한 쌍을 갖는다(088의 `--key`). -- 세션: 두 노드 사이에 하나를 둔다. 키, 혼잡 제어기, 패킷 번호 공간을 하나씩 갖는다. 경로(주소 쌍)는 바뀔 수 있다. -- 전달선: 보내는 앱의 출력 하나에서 받는 앱의 입력 하나로 간다. 저장된 번호 공간을 갖는다. 한 세션의 여러 전달선은 혼잡 - 제어기를 나눠 쓰고, 순서는 전달선마다 따로 지킨다. 한 전달선의 손실이 다른 전달선을 막지 않는다. +**메모리(캐시).** + +- 활성 링크의 행과 AEAD 문맥 +- 경로마다: 주소, RTT, 혼잡 창, 손실 탐지 상태(RFC 9002). 모두 휘발한다. 다시 시작하면 처음 창부터 간다. TCP가 쉬었다 다시 + 보낼 때와 같은 규칙이다(RFC 5681 §4.1). +- 받았지만 아직 저장하지 않은 기록. 허가가 그 양을 묶는다. 기록 하나만큼은 늘 허가해 진행이 막히지 않게 한다. **패킷.** -- 평문 머리: 판, 종류, 세션 식별자, 패킷 번호. 패킷 번호는 손실 탐지와 논스를 위한 것이다. 기록 번호와는 다른 공간이며, - 한 번 쓴 번호를 다시 쓰지 않는다. -- 봉인된 몸(AEAD)에는 프레임이 든다. - - 기록: 전달선, 첫 번호, 개수, 저장한 바이트와 필드 표 - - 패킷 ACK: 범위와 지연 - - 저장함: 전달선과 저장한 마지막 번호 - - 허가: 전달선, 다음 저장에 들일 기록 수와 바이트 - - 핑, 닫기 -- 잃은 패킷은 다시 보내지 않는다. 그 안에 있던 기록 가운데 아직 확인되지 않은 번호를 새 패킷에 실어 다시 보낸다(QUIC처럼 - 프레임 단위다. 다만 내용은 저장소에서 읽는다). -- 머리 보호(QUIC의 패킷 번호 가리기)는 남은 질문이다. 중간 장비가 프로토콜을 굳히지 못하게 하는 효과와 비용을 함께 본다. - -**핸드셰이크와 키.** - -- 보내는 노드는 받는 노드의 공개키를 배선할 때 안다(088의 `--seal`, `--sender`). 첫 패킷이 HPKE auth 모드로 캡슐화한 키와 - 기록을 함께 싣는다. 모양은 Noise IK의 첫 메시지와 같다. -- 받는 노드는 첫 응답에 제 임시 키를 싣는다. 그 뒤 세션은 임시 키끼리의 합의로 갈아탄다. 첫 비행 이후의 기록은 전방 비밀성을 - 얻는다. 첫 비행의 기록은 정적 키로만 보호되며, QUIC 0-RTT와 같은 한계다. 전방 비밀성이 필요한 출력은 첫 응답을 기다린 뒤에 - 보내게 할 수 있다. -- 증폭: 주소를 확인하기 전에는 받은 바이트의 3배 넘게 보내지 않는다(RFC 9000 §8.1과 같은 규칙). 받는 쪽의 답은 대개 작은 - ACK라 이 한도에 거의 닿지 않는다. -- CPU 소모 공격: 부하가 걸리면 X25519를 풀기 전에 주소에 묶인 쿠키를 먼저 요구한다. WireGuard의 쿠키 답과 같은 방식이다. - -**손실과 혼잡.** - -- 손실 탐지는 RFC 9002를 그대로 쓴다(패킷 번호, ACK 범위, RTT 추정, PTO). -- 혼잡 제어도 표준을 쓴다. NewReno부터 시작하고, CUBIC(RFC 9438)을 따른다. 자체 알고리즘은 만들지 않는다. 인터넷에서 다른 - 흐름을 굶기는 전송은 성능이 아니라 해악이다. -- 페이싱, ECN, 경로 MTU 찾기(DPLPMTUD, RFC 8899)를 쓴다. - -**두 단계의 확인.** - -- 패킷 ACK는 받았다는 뜻이다. 보내는 쪽은 그 기록을 다시 보내지 않지만 아직 덜어 내지 않는다. -- "저장함"은 받는 쪽이 동기화했다는 뜻이다. 보내는 쪽은 그때 덜어 낸다(`pure_app_trim`). -- 받는 쪽이 저장 전에 죽으면, 다시 이어질 때 "저장함"이 알리는 번호 다음부터 저장소에서 다시 보낸다. 지금의 HTTP 전달과 같은 - 의미이며, 빠른 길(패킷 손실)과 느린 길(재시작) 모두 같은 저장소에서 나온다. -- 받는 쪽이 받았지만 아직 저장하지 않은 기록은 메모리에 있다. 허가 프레임이 그 양을 다음 저장 한 번에 들일 만큼으로 묶는다. - 큰 기록이 허가보다 크면 기록 하나만큼은 늘 허가해 진행이 막히지 않게 한다([006](006-native-protocol.md)의 교착 조건). +- 평문 머리: 판, 종류, 링크 식별자, 패킷 번호. 링크 식별자는 받는 쪽이 행을 곧바로 찾는 짧은 번호다. +- 봉인된 몸에는 프레임이 든다. + - 기록: 링크, 첫 번호, 개수, 저장한 바이트와 필드 표 + - 패킷 ACK + - 저장함: 이 번호까지 저장했다 + - 허가 + - 에폭 바꿈 + - 핑 +- 잃은 패킷은 다시 보내지 않는다. 그 안의 확인되지 않은 기록을 저장소에서 읽어 새 패킷에 싣는다. +- 확인은 두 단계다. 패킷 ACK는 받았다는 뜻이고, 보내는 쪽은 그 기록을 다시 보내지 않는다. "저장함"은 동기화했다는 뜻이고, + 보내는 쪽은 그때 덜어 낸다(`pure_app_trim`). 받는 쪽이 저장 전에 죽으면, 이어질 때 "저장함" 다음 번호부터 다시 간다. + +**키.** + +- 첫 에폭은 배선할 때 두 노드의 정적 키로 만든다(HPKE auth 모드, Noise KK 모양). 첫 패킷부터 쓴다. +- 에폭은 주기적으로 바꾼다. 기준은 시간과 바이트다. 임시 키 교환을 패킷에 싣고, 새 에폭은 저장하며, 지난 에폭 키는 지운다. + 전방 비밀성은 이렇게 에폭 단위로 얻는다. 첫 에폭의 기록은 정적 키로만 보호된다. QUIC 0-RTT와 같은 한계다. +- 재전송 공격: + - 기록은 저장된 들은 번호로 걸러지므로, 재시작 뒤에도 창을 잃지 않는다. OSCORE가 재부팅 뒤 쓰는 Echo 왕복(RFC 8613 B.1.2)이 + 필요 없다. + - 패킷 안의 나머지(ACK, 허가)는 최댓값으로만 받아들인다. 그래서 다시 보낸 패킷이 상태를 되돌리지 못한다. +- CPU 소모: 링크 식별자로 행을 찾고 AEAD 한 번으로 버린다. 비대칭 연산은 에폭 바꿈에만 있고, 비율을 제한한다. +- 증폭: 새 주소로는 확인하기 전에 받은 바이트의 3배까지만 보낸다(RFC 9000 §8.1과 같은 규칙). +- NAT 뒤의 쉬는 기기에 먼저 보내는 일은 이 설계가 풀지 않는다. 푸시나 기기의 주기적 패킷이 여전히 필요하다. 이 설계가 보장하는 + 것은, 기기가 깨어 보낸 첫 패킷으로 핸드셰이크 없이 이어진다는 것이다. + +**손실과 혼잡은 표준대로 한다.** 손실 탐지는 RFC 9002, 혼잡 제어는 NewReno에서 CUBIC(RFC 9438)으로 간다. 페이싱, ECN, +DPLPMTUD(RFC 8899)를 쓴다. 자체 혼잡 제어 알고리즘은 만들지 않는다. 다른 흐름을 굶기는 전송은 해악이다. **송수신 경로.** -- `sendmmsg`와 `recvmmsg`를 쓰고, Linux에서는 UDP GSO(`UDP_SEGMENT`)와 GRO(`UDP_GRO`)도 쓴다. -- 저장소의 기록 바이트를 AEAD의 출력 버퍼(곧 패킷 버퍼)로 봉인하는 것이 유일한 복사다. -- `SOCK_RAW`와 커널 우회는 쓰지 않는다. `SOCK_RAW`는 권한이 필요하고 NAT와 방화벽을 지나지 못한다. 필요하다면 측정이 그 비용을 - 보여 준 뒤에 따로 판단한다([006](006-native-protocol.md)). +- `sendmmsg`와 `recvmmsg`를 쓰고, Linux에서는 UDP GSO와 GRO도 쓴다. +- 저장소의 바이트를 패킷 버퍼로 봉인하는 것이 유일한 복사다. +- `SOCK_RAW`와 커널 우회는 쓰지 않는다. 권한이 필요하고 NAT를 지나지 못한다. **경계(갈라파고스 금지).** -- UDP가 막힌 곳에서는 기존 HTTP 전달(083)로 돌아간다. 같은 프레임을 TCP 위의 길이 붙은 레코드로 실을 수도 있다. 평문으로 - 낮추지는 않는다. -- 제3자와 브라우저는 HTTP 경계로 잇는다. 새 전송은 Pure 노드끼리 쓰는 것이다. -- 암호는 표준만 쓴다(HPKE RFC 9180, X25519, HKDF, AES-128-GCM이나 ChaCha20-Poly1305). 혼잡 제어도 표준이다. -- 명세를 이 저장소에 문서로 둔다. 판 협상, 모르는 프레임의 처리, 오류 부호를 정해 다른 구현이 만들어질 수 있게 한다. +- UDP가 막히면 HTTP 전달로 간다. 평문으로 낮추지는 않는다. +- 제3자는 HTTP로 잇는다. +- 암호와 혼잡 제어는 표준만 쓴다. +- 명세를 문서로 두어 다른 구현이 가능하게 한다. 판 협상과 오류 부호도 거기서 정한다. ## 이미 있는 것과의 차이 -개별 기법은 모두 이미 있다. 새 이름으로 포장하지 않는다. - -| 이미 있는 것 | 가져오는 것 | 다른 점 | +| 이미 있는 것 | 닮은 점 | 다른 점 | |---|---|---| -| QUIC(RFC 9000–9002) | 손실 탐지, 혼잡 제어, 증폭 한도, 프레임 단위 재전송 | QUIC은 범용 스트림이라 신뢰성과 버퍼, 암호를 전송이 소유한다 | -| WireGuard(Donenfeld, NDSS 2017), Noise IK | 미리 아는 정적 키, 첫 메시지의 키 교환, 쿠키, 카운터 논스, 키로 식별하는 세션과 로밍 | WireGuard는 신뢰성이 없는 터널이다 | -| Aeron | 로그를 버퍼로 쓰는 UDP 메시징, 받는 위치에 따른 흐름 제어 | Aeron의 로그는 메모리이고, 앱의 저장이나 한 번 적용과 묶이지 않으며, 암호가 없다 | -| Homa(SIGCOMM 2018) | 받는 쪽이 주는 허가 | 데이터센터용이다 | -| Kafka | 저장 형식이 곧 선 위의 형식(sendfile) | 전송은 TCP다 | -| KCP 같은 게임용 UDP | — | 혼잡 제어를 버리고 지연을 산다. 이 설계는 그러지 않는다 | +| CoAP·OSCORE(RFC 7252, 8613), EDHOC(RFC 9528) | 가장 가깝다. 미리 세운 보안 문맥, 메시지 단위 AEAD, 재부팅을 견디는 번호 예약 | 재부팅하면 재전송 창을 잃어 Echo 왕복으로 다시 세운다. 신뢰성은 수명이 짧은 메시지 ID 창이다. 혼잡 제어가 단순하다. 앱의 적용과 원자적이지 않다 | +| MQTT 지속 세션 | 끊긴 클라이언트의 구독과 메시지를 브로커가 보관한다 | TCP(와 TLS) 연결은 다시 연다 | +| Signal | 래칫 상태를 기기에 저장해 재시작을 견딘다 | 전송(TLS, 웹소켓)은 따로 연다 | +| Orleans 가상 액터 | 액터가 저장에서 필요할 때 깨어난다 | 사일로 사이는 TCP 연결이다 | +| WireGuard, Noise | 키로 식별하는 피어, 로밍, 카운터 논스 | 세션 키가 메모리에 있어 재시작하면 새 핸드셰이크를 한다. 신뢰성이 없다 | +| QUIC | 연결 ID로 주소 이동, 티켓으로 0-RTT 재개 | 연결 상태가 메모리에 있어 서버가 재시작하면 새 연결을 맺는다 | +| 끝에서 끝 논증, Aeron, Kafka | 끝점의 신뢰성, 로그를 버퍼로, 저장 형식이 곧 선 위 형식 | 연결 모델은 그대로다 | -가설은 조합에 있다. 이미 저장, 번호, 한 번 적용, 봉인 세션을 가진 런타임에서는 전송이 그것들을 빌려 쓸 수 있다. 그러면 전송의 -신뢰성, 전송 암호, 앱 전달 프로토콜(HTTP와 헤더, JSON)의 세 층이 한 층으로 준다. 학술적 신규성은 이 측정과 별도로 판단한다. +기법 하나하나는 새것이 아니다. 가설은 모델에 있다. 전송 전체를 앱의 트랜잭션 저장에 원자적으로 넣는다. 키 에폭, 신뢰성, +재전송 방지, 흐름, 앱의 적용이 모두 거기에 든다. 그러면 연결이라는 개념이 사라진다. 이 조합의 신규성은 문헌 조사로 다시 +확인하며, 측정 전에는 주장하지 않는다. -## 측정 계획: quinn·ngtcp2 대비 +## 가설 + +- **H1.** 쉬는 링크의 비용은 저장소의 행(약 100 B)이다. 프로세스 메모리는 활성 링크 수에만 비례한다. +- **H2.** 어느 쪽 노드를 kill -9 해도, 다시 뜬 뒤 첫 패킷으로 이어진다. 핸드셰이크는 없고, 잃거나 겹치는 기록도 없다. +- **H3.** 저장소를 다른 프로세스나 주소로 옮겨도 링크가 이어진다. +- **H4.** 활성 링크의 전달당 CPU는 QUIC 기준선보다 나쁘지 않다. 층을 합친 이득이 행을 찾는 비용을 덮는다. -**비교 대상.** 모두 같은 의미를 지킨다. 보내는 앱이 저장한 것만 보내고, 전달선마다 순서대로 보내며, 받는 쪽이 저장한 뒤에만 -덜어 내고, 받는 쪽은 한 번만 적용한다. 암호는 모두 AES-128-GCM이다(이 A76에서 가장 빠르다, [086](../research/086-sealed-sends.md)). +## 측정 계획: quinn·ngtcp2 대비 -1. 지금의 Pure 전달: HTTP/1.1 지속 연결과 HPKE 봉인 세션([088](../research/088-sealed-sessions.md), 봉인 보내기 약 46만/s) -2. QUIC 스트림 위의 같은 기록 형식 - - quinn 0.11(Rust, rustls): 이 Pi의 cargo 저장소에 있다. - - ngtcp2(C): 원본에서 OpenSSL 3.5 암호 백엔드로 빌드한다. 설치된 1.11은 머리 파일이 없다. - - OpenSSL 3.5.7의 QUIC: 참고로 둔다. -3. TLS 1.3/TCP 위의 같은 기록 형식([026](../research/026-secure-peer-link.md)) -4. Pure 전송 시제품 +**비교 대상.** 모두 같은 의미를 지킨다. 보내는 앱이 저장한 것만 보내고, 링크마다 순서대로 보내며, 받는 쪽이 저장한 뒤에만 +덜어 내고, 한 번만 적용한다. 암호는 AES-128-GCM이다. -QUIC 기준선도 같은 의미를 위해 저장소에 기록을 남기고 앱 수준의 "저장함"을 기다린다. 두 겹의 버퍼는 그 구조에 딸린 비용이므로 -그대로 잰다. +- quinn 0.11: 이 Pi의 cargo 저장소에 있다. +- ngtcp2: 원본에서 OpenSSL 3.5 백엔드로 빌드한다. +- OpenSSL 3.5.7의 QUIC: 참고로 둔다. +- 지금의 HTTP 전달 +- 시제품 **작업.** -| | 무엇 | 보는 값 | -|---|---|---| -| W1 | 쌓인 보내기 20,000개 풀기(64 B, 1 KiB, 16 KiB) | 전달/s, 전달당 CPU(양쪽의 user+sys), 전달당 선 위 바이트 | -| W2 | 한 줄 왕복(메신저): 저장에서 상대의 저장 확인까지 | p50, p99 지연 | -| W3 | 처음 만남: 프로세스 시작부터 첫 기록의 적용까지 | 시간, 왕복 수 | -| W4 | 손실과 지연: RTT 20·100 ms, 손실 1·5 %, 재정렬 | 처리량, 꼬리 지연, 재전송 바이트 | -| W5 | 쉬는 전달선 1,000개 | 세션당, 전달선당 메모리 | -| W6 | 공정성: 50 Mbit/s 병목에서 TCP CUBIC 흐름과 나란히 | 대역의 몫 | -| W7 | 주소 바뀜: 전달 중에 보내는 쪽의 포트를 바꿈 | 끊김 시간, 잃거나 겹친 기록 | +| | 무엇 | 보는 값 | QUIC 쪽 | +|---|---|---|---| +| M1 | 쉬는 링크 1만, 10만, 100만 개 | 프로세스 RSS와 저장소 크기의 링크당 기울기, 깨우는 첫 패킷의 지연 | 이 Pi에서 늘릴 수 있는 만큼 늘려 연결당 기울기를 잰다 | +| M2 | 활성 링크 1만 개에 부하를 건 채 kill -9 | 다시 전달하기까지의 시간, 양쪽 CPU와 비대칭 연산 횟수, 잃음·겹침 | 연결 1만 개를 다시 맺는다(1-RTT, 티켓 0-RTT) | +| M3 | 저장소를 다른 프로세스로 옮김, 주소가 바뀜 | 끊긴 시간, 잃음·겹침 | 주소 이동은 같은 프로세스 안에서만 된다 | +| M4 | 처음 만남 | 첫 기록을 적용하기까지의 왕복 수 | 1-RTT | +| M5 | 쌓인 보내기 20,000개 풀기(64 B, 1 KiB, 16 KiB) | 전달/s, 전달당 CPU | 가드레일 | +| M6 | RTT 20·100 ms에 손실 1·5 %와 재정렬. 50 Mbit/s 병목에서 TCP CUBIC과 나란히 | 처리량, 꼬리 지연, 대역의 몫 | 가드레일 | **조건.** -- 장치는 Raspberry Pi 5(A76 네 개)다. 지연, 손실, 재정렬, 대역 제한은 비특권 네트워크 이름공간(`unshare -rn`)과 netem·tbf로 - 만든다. 이 Pi에서 되는 것을 확인했다(한쪽 10 ms 지연으로 RTT 20 ms). -- 저장소는 두 가지로 한다. RAM(tmpfs)에서는 전송 비용이 드러나고, SD 카드에서는 실제 모습이 나온다 - ([092](../research/092-tpcc.md)의 저장 장치 차이를 참고한다). -- 후보는 번갈아 돌린다. 세 라운드 이상 하고, 원시 결과를 `research/experiments/`에 남긴다. 처리량과 함께 CPU, 메모리, 선 위 - 바이트를 적는다. 헤더 바이트 절감만으로 우위를 말하지 않는다. - -**0단계, 비용 분해.** W1을 지금의 전달과 quinn 기준선에서 돌린다. 전달 하나의 CPU를 적용·저장, 형식(JSON이나 기록), 암호, -시스템 호출, 복사, 전송 상태(ACK, 타이머)로 나눈다. `perf`를 쓸 수 있으면 쓰고, 없으면 계측으로 한다. +- 장치는 Raspberry Pi 5다. 지연·손실·병목은 비특권 네트워크 이름공간과 netem·tbf로 만든다. 이 Pi에서 되는 것을 확인했다(한쪽 + 10 ms로 RTT 20 ms). +- 저장소는 RAM(tmpfs)과 SD 카드 두 가지로 한다. +- 후보는 번갈아 돌리고, 세 라운드 이상 한다. 원시 결과는 `research/experiments/`에 남긴다. ## 중단 기준 -| 단계 | 조건 | 결정 | -|---|---|---| -| 0 비용 분해 | RAM 저장소에서 전송(형식, 암호, 시스템 호출, 복사, 전송 상태)이 전달당 CPU의 30 % 미만이다 | 새 전송을 만들지 않는다. (나)를 쓰고 이 문서를 닫는다 | -| 1 시제품(한 전달선) | W1에서 CPU당 전달이 가장 좋은 QUIC 기준선의 1.5배 미만이다 | 멈추고 (나)를 쓴다 | -| 채택 | 조건 모두를 만족한다: <br>- W1 CPU당 전달 2배 이상<br>- W3 한 왕복 적음<br>- W5 쉬는 전달선당 메모리 1/4 이하<br>- W4 처리량이 QUIC보다 10 % 넘게 나쁘지 않음<br>- W6 TCP와의 몫 차이 ±25 % 안<br>- 아래 검사 모두 통과 | 노드 사이 전달의 기본으로 삼는다. HTTP 전달은 경계로 남는다 | -| 언제든 | 무작위 검사에서 한 번이라도 잃거나 겹친다. 검사는 kill -9와 손실·중복·재정렬·변조·잘림 퍼징이다 | 되돌리고 원인을 찾는다 | -| 언제든 | 보안 검사 가운데 하나라도 통과하지 못한다. 검사는 증폭 한도, 쿠키, 재전송된 패킷, 보낸 이 위장, 변조된 머리다 | 인터넷용으로 켜지 않는다 | -| 언제든 | 전송 코드가 C 5,000줄이나 `text` 64 KiB를 넘는다 | 설계를 다시 본다. QUIC 구현을 다시 만드는 일이 되어서는 안 된다 | +| 조건 | 결정 | +|---|---| +| H2·H3의 검사에서 한 번이라도 잃거나 겹친다. 또는 재시작 뒤 논스를 다시 쓴다 | 되돌리고 원인을 찾는다 | +| M1: 쉬는 링크당 프로세스 메모리가 QUIC 연결당 메모리의 1/100 이하가 아니다 | 모델의 주장이 틀렸다. 다시 설계한다 | +| M2: 재시작 뒤 핸드셰이크 없이 이어지지 않는다. 또는 이어지는 데 링크 수에 비례하는 비대칭 연산이 든다 | 다시 설계한다 | +| 전송 상태 때문에 패킷마다 동기화가 필요해진다(지연이 RTT와 저장 한 번을 넘는다) | 예약 방식을 다시 본다 | +| M5·M6: 전달당 CPU가 QUIC보다 10 % 넘게 나쁘다. 또는 TCP와의 몫이 ±25 %를 벗어난다 | 고친 뒤에 채택한다 | +| 보안 검사(증폭, 보낸 이 위장, 재전송, 변조)에 실패한다 | 인터넷용으로 켜지 않는다 | +| 전송 코드가 C 5,000줄을 넘는다 | 설계를 다시 본다 | +| H1–H4가 모두 서고 검사를 모두 통과한다 | 노드 사이 전달의 기본으로 삼는다 | + +## 이 너머: 기다리지 않는 상호작용 + +연결을 없애도 왕복 자체는 남는다. 빛의 속도는 줄일 수 없다. 다음 후보는 결정적 모듈의 투기 실행이다. -"차별화가 없다"도 유효한 결론이다([000](../research/000-working-method.md)). 그때는 (나)로 옮기고, 이 문서는 그 근거로 남는다. +Pure의 모듈은 검증되어 결정적이고, 이벤트의 실행량에 상한이 있다. 보내는 노드가 받는 앱의 모듈과 관련 상태의 복제를 가지면, +답을 기다리지 않고 받는 쪽의 결과를 미리 계산해 보여 줄 수 있다. 그 뒤 실제 답으로 확정하거나 되돌린다. 게임의 롤백 넷코드와 +Replicache의 뮤테이터와 같은 생각이다. 다른 점은 앱 코드를 두 벌 쓰지 않는다는 것이다. 이것은 전송이 아니라 런타임의 능력이라 +따로 설계한다. ## 순서 -1. 0단계 비용 분해(지금의 전달, quinn 기준선) → 첫 중단 기준 -2. quinn과 ngtcp2에 같은 의미의 전달 하네스를 만든다 -3. 시제품: 한 세션, 한 전달선, RFC 9002 손실 탐지, NewReno, 저장소에서 재전송, 정적 키 첫 비행 -4. W1–W4 측정 → 둘째 중단 기준 -5. 핸드셰이크 완성(임시 키, 쿠키), 경로 바뀜, 여러 전달선, 허가 → W5–W7, 보안 검사 -6. 채택 여부. 명세 문서 +1. 링크 행 시제품을 만든다: 한 노드 쌍, 링크 여럿, 에폭 하나, NewReno, 저장소에서 재전송. 여기에 kill -9 검사(H2)를 붙인다. +2. quinn·ngtcp2 하네스를 같은 의미로 만든다. +3. M1(메모리 기울기), M2(재시작), M4를 잰다. +4. 에폭 바꿈, 경로 바뀜, 저장소 옮김(H3, M3)과 보안 검사를 한다. +5. M5·M6 가드레일을 잰다. 채택 여부를 정하고 명세를 쓴다. |
