diff options
Diffstat (limited to 'docs/research/100-moved-receiver.md')
| -rw-r--r-- | docs/research/100-moved-receiver.md | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/docs/research/100-moved-receiver.md b/docs/research/100-moved-receiver.md new file mode 100644 index 0000000..40d5f2f --- /dev/null +++ b/docs/research/100-moved-receiver.md @@ -0,0 +1,125 @@ +# 100 — 받는 쪽을 다른 기계로 옮겨도 링크가 따라간다 + +2026-09-29. [설계 011](../design/011-pure-transport.md) 4단계의 경로 바뀜과 저장소 옮기기(H3, M3)다. + +착수 기준: + +- **바꿀 결정.** H3이다. 저장소를 다른 프로세스나 주소로 옮겨도 링크가 이어진다. + - QUIC은 연결을 다른 프로세스로 옮기지 못한다. 서버의 주소가 바뀌는 길은 핸드셰이크 때 알린 preferred_address 하나다 + (RFC 9000 §9.6). 옮긴 서버에는 모든 클라이언트가 새로 연결해야 한다. +- **최소 확인.** + - 받는 쪽의 저장소, 키, 메모를 다른 기계로 옮긴다. 옛 주소는 사라지고, 보내는 쪽이 쓰는 이름이 새 주소를 가리킨다. + - 끊긴 시간, 잃거나 겹친 기록, 비대칭 연산을 본다. M2와 같은 부하(기기 1만 개, 초당 2,000건)로 잰다. +- **중단 기준(011).** H3의 검사에서 한 번이라도 잃거나 겹치면, 되돌리고 원인을 찾는다. + +## 쉬운 말로 + +- 알아낸 것: + - 처음 해 보니 링크가 옮겨 간 받는 쪽을 영영 찾지 못했다. 링크는 시작할 때 이름을 한 번만 풀었다. + - 그 검사를 만들다 더 오래된 결함이 나왔다. + - 받는 쪽은 노드의 모든 주소에서 받지만, 답은 시스템이 고른 주소에서 나갔다. + - 보내는 쪽이 노드의 두 번째 주소로 보내면 답이 하나도 닿지 않았다. 기록은 도착했는데 확인이 오지 않았다. + - 이제 패킷이 도착한 주소에서 답한다. QUIC 구현들이 하는 대로다. + - 이제 링크는 다음 둘 중 하나가 일어나면 이름을 다시 풀고 새 소켓으로 보낸다. + - 탐침 세 번이 연달아 답을 받지 못했다. + - 보내다가 시스템이 길이 없다고 했다(길 없음, 받는 이 없음). + - 받는 쪽이 옮겨 가도, 보내는 쪽의 망이 바뀌어도 같은 규칙이다. + - 기기 1만 개가 초당 2,000건을 보내는 중에 받는 쪽을 다른 주소로 옮겼다. + - 저장이 멈춘 100 ms 칸이 없다. + - 옮긴 노드는 X25519를 하지 않았다. 잃거나 겹친 기록도 없다. + - 같은 주소에서 다시 띄운 경우(M2)와 다른 점은 하나다. 옮긴 뒤 기기마다 첫 기록이 탐침 한 번(이 루프백에서 30–45 ms)을 + 기다린다. +- 아직 모르는 것: + - 보내는 쪽의 망이 바뀌는 경우(와이파이에서 LTE로)는 같은 규칙으로 다룬다. 다만 한 기계의 루프백으로는 옛 망을 끊을 수 없어 + 따로 재지 않았다. + - 실제 DNS에서는 이름이 바뀌어 퍼지는 시간(TTL)이 든다. 재지 않았다. +- 사용 경험의 차이: 장애 조치가 "저장소를 옮기고 이름을 바꾸는 일"이 된다. 기기들은 핸드셰이크 없이 다음 패킷으로 따라온다. + +## 한 것 + +### 경로는 두 주소다 + +받는 쪽이 한 run에 대해 기억하는 경로를 (상대의 주소, 패킷이 도착한 노드의 주소) 한 쌍으로 바꿨다(48 B, 전에는 28 B). +QUIC이 경로를 4-튜플로 보는 것과 같다. + +- 리스너는 패킷마다 도착한 주소를 받는다(IP_PKTINFO). IPv4도 IPv6 소켓 하나에서 `::ffff:a.b.c.d`로 받는다. +- 답은 그 주소에서 나간다. +- 도착한 노드의 주소가 바뀐 패킷은 상대 주소가 바뀐 패킷과 똑같이 다룬다. 가장 새 번호일 때만 경로를 옮기고, 새 경로는 + CHALLENGE로 확인한다(098). + +### 받는 쪽을 다시 찾는다 + +링크(`link/link.c`)가 받는 쪽의 이름을 다시 풀고, 새 소켓을 그 주소에 연결한다. 옛 소켓은 닫는다. 다시 찾는 때는 둘이다. + +- 탐침 세 번이 연달아 답을 받지 못했을 때, 그리고 그 뒤 탐침마다. 탐침 간격은 두 배씩 늘어 1초에서 멈춘다. +- 보내다가 시스템이 길이 없다고 했을 때(ECONNREFUSED, EHOSTUNREACH, ENETUNREACH, ENETDOWN, EADDRNOTAVAIL). 이때는 다음 + 패킷을 보내기 전에 곧바로 찾는다. + +두 가지를 조심했다. + +- **쉬던 링크는 찾지 않는다.** 답을 들은 지 오래됐다는 것만으로는 찾지 않는다. 5초마다 기록 하나를 보내는 기기가 기록마다 + 이름을 풀고 소켓을 새로 열 것이기 때문이다. 답을 받지 못한 탐침만 센다. +- **탐침을 보내기 전에 찾는다.** 처음에는 탐침을 보낸 뒤에 소켓을 바꿨다. 그러자 그 탐침의 답이 방금 닫은 소켓으로 돌아왔고, + 다음 탐침에서도 같은 일이 되풀이됐다. 소켓만 번갈아 바뀌고 답은 하나도 받지 못했다. + +새 소켓은 새 포트이므로, 받는 쪽은 새 경로로 보고 확인한다. 확인 전의 답은 받은 바이트의 3배까지이고, 보내는 쪽의 다음 +패킷에 RESPONSE가 실려 간다(099). + +### 검사와 하네스 + +- `make check`의 `server_links_moved`는 네트워크와 `/etc/hosts`를 따로 가진 이름공간(`unshare -rmn`)에서 돈다. + 1. 보내는 노드는 `pure://receiver.test`로 보낸다. 이 이름은 127.0.0.2를 가리킨다. + 2. 주문이 흐르는 중에 받는 노드를 kill -9 한다. + 3. 저장소, 키, 메모를 다른 디렉터리로 복사한다. + 4. 127.0.0.2를 라우팅에서 닿지 않게 하고, 이름을 127.0.0.3으로 바꾼 뒤, 복사본을 거기서 띄운다. + 5. 모든 주문이 한 번씩 적용되고, 옮긴 노드가 X25519를 하지 않으면 통과다. +- M2 하네스에 `--move`를 더했다([run.py](../../research/experiments/019-idle-links/run.py)). 같은 이름공간에서 같은 옮기기를 + 한다. 부하 생성기의 기기들도 `link.c`와 같은 규칙으로 다시 찾는다. + +## 결과 + +M2와 같은 부하다. 기기 1만 개가 5초마다 주문 하나를 보낸다(초당 2,000건). 80초를 돌리고 60초에 받는 쪽을 kill -9 한다. +에폭 수락은 약 40초에 끝나므로, 60초에는 모든 기기가 에폭 1에 있다. 두 라운드씩이다. 원시 결과는 +`research/experiments/019-idle-links/2026-09-29-move-linux-aarch64.jsonl`에 있다. + +| | 같은 주소에서 다시 띄움(M2) | 다른 주소로 옮김(M3) | +|---|---|---| +| 죽인 뒤 다시 뜨기까지(옮기기 포함) | 18 ms | 24–33 ms | +| 저장이 멈춘 100 ms 칸 | 0 | 0 | +| 옮긴 뒤 5초에 만든 기록의 p50 / p99 | 0.6–0.7 / 7.9–8.4 ms | 28–29 / 44–45 ms | +| 전체 p99 / 최대 | 5.0–5.1 / 36–39 ms | 36–37 / 107–111 ms | +| 다시 보낸 기록 / 보낸 패킷 | 33 / 160,039–160,043 | 10,022 / 170,028–170,033 | +| 다시 뜬 노드의 X25519 | 0 | 0 | +| 다시 뜬 뒤 10초의 CPU, 노드 / 생성기 | 1.17–1.40 / 0.72–0.83초 | 1.08–1.24 / 0.89–1.00초 | +| 적용 | 160,006–160,010, 모두 한 번 | 160,006–160,011, 모두 한 번 | + +- 옮긴 뒤 기기마다 첫 기록 하나가 한 번 더 간다(10,022건). + - 옛 주소로 간 첫 패킷은 시스템이 "길 없음"으로 돌려준다. + - 기기는 곧바로 이름을 다시 풀지만, 그 기록은 탐침이 울릴 때(30–45 ms 뒤) 새 주소로 간다. + - 기기가 5초마다 보내므로, 옮긴 뒤 5초 동안 모든 기기가 한 번씩 이것을 겪는다. +- 하네스의 "따라잡기"는 옮긴 쪽이 1.4–3.1초로 나온다. 밀린 것이 아니다. + - 100 ms 칸의 저장량은 옮긴 직후부터 약 200으로 같다. + - 5초 동안 기록마다 약 30 ms씩 늦으니, 누적량이 "정상 속도의 99 %"에 조금 못 미치는 동안이 그만큼 이어진 것이다. +- quinn은 같은 주소의 재시작에서도 1초 멈추고, 10.9초 뒤 따라잡았다. 그동안 핸드셰이크 1만 번에 서버 CPU 약 7초를 썼다(095). + 옮긴 서버라면 적어도 이만큼 든다. 옮긴 서버에는 연결이 하나도 없기 때문이다. + +## 판정 + +- H3이 선다. 받는 쪽의 저장소, 키, 메모를 다른 기계로 옮기면, 기기들은 핸드셰이크 없이 다음 패킷으로 따라온다. 잃거나 겹친 + 기록이 없고, 옮긴 노드의 비대칭 연산은 0이다. +- 011의 네 가설을 모두 쟀다. + - H1(쉬는 링크의 비용, 094), H2(kill -9, 095), H3(이 문서)은 선다. + - H4(전달당 CPU, 096)는 1 KiB와 16 KiB에서 quinn보다 25 % 적다. 61 B에서는 1.06–1.12배라, 채택 기준(10 %)의 경계에 있다. + - 보안 검사(098)와 전방 비밀성(099)도 통과했다. +- 남은 것은 011의 5단계다. 채택 여부를 정하고 명세를 쓴다. 61 B의 전달당 CPU를 먼저 다시 본다. + +## 한계와 다음 + +- **보내는 쪽의 망이 바뀌는 경우.** 규칙은 같다. 옛 망의 소켓이 보내지 못하거나 탐침이 답을 받지 못하면 새 소켓으로 간다. + 다만 실제 인터페이스를 바꿔 가며 재지 않았다. +- **이름 풀기는 링크 자신의 스레드에서 한다.** 실제 DNS가 느리면 그동안 그 링크는 기다린다. 다른 링크는 기다리지 않는다. +- **새 소켓은 새 경로다.** 받는 쪽은 새 포트를 확인할 때까지 답을 받은 바이트의 3배로 묶는다. 기록 하나에 대한 답은 그보다 + 작아서 M3에서는 걸리지 않았다. +- **옮긴 뒤의 첫 기록.** 이 기록을 탐침 없이 곧바로 새 주소로 다시 보내면 30–45 ms를 더 줄일 수 있다. 다만 보낼 것이 없는 + 링크를 깨울 방법을 따로 두어야 해서 하지 않았다. +- 다음은 011의 5단계, 채택 여부와 명세다. |
