# 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는 096에서 1.06–1.12배로 기준(10 %)의 경계였다. 이 문서 뒤에 기록마다의 나눗셈과 줄 걷기를 없애고 같은 라운드에서 쟀더니 0.93배였다(096의 덧붙임). - 보안 검사(098)와 전방 비밀성(099)도 통과했다. - 남은 것은 011의 5단계다. 채택 여부를 정하고 명세를 쓴다. ## 한계와 다음 - **보내는 쪽의 망이 바뀌는 경우.** 규칙은 같다. 옛 망의 소켓이 보내지 못하거나 탐침이 답을 받지 못하면 새 소켓으로 간다. 다만 실제 인터페이스를 바꿔 가며 재지 않았다. - **이름 풀기는 링크 자신의 스레드에서 한다.** 실제 DNS가 느리면 그동안 그 링크는 기다린다. 다른 링크는 기다리지 않는다. - **새 소켓은 새 경로다.** 받는 쪽은 새 포트를 확인할 때까지 답을 받은 바이트의 3배로 묶는다. 기록 하나에 대한 답은 그보다 작아서 M3에서는 걸리지 않았다. - **옮긴 뒤의 첫 기록.** 이 기록을 탐침 없이 곧바로 새 주소로 다시 보내면 30–45 ms를 더 줄일 수 있다. 다만 보낼 것이 없는 링크를 깨울 방법을 따로 두어야 해서 하지 않았다. - 다음은 011의 5단계, 채택 여부와 명세다.