diff options
| -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 가드레일을 잰다. 채택 여부를 정하고 명세를 쓴다. |
