버그로 접수된 적이 없는 실패가 하나 있다. 아무도 그걸 본 적이 없기 때문이다.
한 팀이 IPv6를 켠다. AAAA 레코드를 추가하고, 로드밸런서에서 듀얼스택을 켜고, 배포한다. 경로 어딘가에서 — 인바운드 IPv6를 드롭하는 방화벽 규칙, 리턴 라우트가 없는 라우터, IPv4 규칙만 받아온 보안 그룹 — v6 경로는 블랙홀이다. 패킷은 들어가는데 아무것도 돌아오지 않는다. 그런데도: 티켓 없음. 느린 페이지 불평 없음. 대시보드는 초록색. IPv6 롤아웃은 성공으로 선언되고 다들 다음으로 넘어간다.
그러다 어느 날 누군가 작은 백엔드 서비스 — 헬스 체커, cron 잡, 맨 소켓으로 같은 호스트명을 때리는 스크립트 — 를 짜는데, 매번 30초씩 멈췄다가 겨우 작동한다. 이제 버그가 생겼다. 그런데 버그는 그 새 서비스에 있지 않다. 버그는 처음부터, 프로덕션에, 몇 달째 있었다. 새 서비스는 그저 그걸 당신에게 알려줄 만큼 순진한 첫 클라이언트일 뿐이다.
그걸 숨겨온 것의 이름이 Happy Eyeballs다. 당신 브라우저가 돌리는, 가장 조용하게 중대한 알고리즘 중 하나다.
이게 풀려고 만들어진 문제
호스트명에 A 레코드(IPv4)와 AAAA 레코드(IPv6)가 둘 다 있으면, 클라이언트는 어느 쪽에 연결할지 골라야 한다. 오랫동안 규칙은 — RFC 6724의 주소 선택 로직에 따라 — 단순했다: IPv6를 선호하라. 먼저 시도하라.
IPv6가 작동할 땐 정확히 옳고, 안 될 땐 재앙인 규칙이다. v6 경로가 망가졌다면 “IPv6 먼저 시도”란 결코 답하지 않을 주소로 TCP 연결을 열고, 그 연결이 타임아웃될 때까지 기다린 뒤에야 IPv4로 폴백한다는 뜻이다. TCP 연결 타임아웃은 수십 초 단위다. 그래서 가령 2011년, 미묘하게 망가진 IPv6 네트워크의 사용자 경험은 에러 페이지가 아니었다. 그것은: 인터넷 전체가 알 수 없이, 간헐적으로 느리다는 것이었다. 어떤 사이트는 20초 멈췄다가 멀쩡히 열린다. 아무도 설명하지 못했다. 사용자 의자에서 보면 두 홉 떨어진 라우팅 버그가 아니라 불안정한 Wi-Fi처럼 보였으니까.
디버깅하기 비참하고 사용자에게 참아달라 하기 불가능한 일이었으며, IPv6 배포에 실질적인 발목이었다 — v6를 켠 운영자마다 듀얼스택 사용자 경험을 더 나쁘게 만들 위험을 졌으니, 많은 이가 그냥 안 켰다.
기다리지 마라. 경주시켜라.
그 해법은 RFC 6555(2012, Dan Wing과 Andrew Yourtchenko)에서 Happy Eyeballs로 처음 적혔고, 돌이켜보면 모욕적일 만큼 단순하다: IPv6 연결이 실패하기를 기다리지 마라. 잠시 뒤 IPv4 연결을 병렬로 시작하고, 먼저 완료되는 쪽을 써라. 진 쪽은 취소하라.
RFC 8305(2017)는 그 아이디어를 진짜 명세로 — 원본을 폐기하는 Happy Eyeballs 버전 2로 — 만들었고, 장인정신은 디테일에 있다.
AAAA와 A, 두 DNS 쿼리를 동시에 쏜다. 하지만 AAAA 응답을 기다리며 블록하지 않는다: RFC는 Resolution Delay 50밀리초를 권장하는데, 그 뒤로 A 레코드는 이미 돌아왔고 AAAA는 아직이면 멈춰 서지 않고 IPv4로 연결을 시작한다. 결과 주소들을 정렬해 패밀리가 교차하도록 — IPv6, 그다음 IPv4, 그다음 IPv6 — 하되, 의도적으로 IPv6 주소 하나를 먼저 시도하도록 치우친다. 그런 다음 첫 주소에 연결을 시작하며 타이머를 건다. 그 타이머, Connection Attempt Delay는 기본 250밀리초다(명세는 최소 100ms, 최대 2초로 못박는다). 첫 시도가 그 창 안에 완료되지 않으면 마냥 기다리지 않는다 — 다음 주소를, 그다음을, 같은 간격으로 엇갈리게 쏘며 한꺼번에 경주시킨다. 먼저 끝난 핸드셰이크가 이기고, 나머지는 헐린다.
250ms라는 숫자가 트릭의 전부이고, 진짜로 훌륭한 설계다. 너무 길면 망가진 v6 경로가 v4로 넘어가기 전에 사용자에게 실제로 느껴지는 시간을 물린다. 너무 짧으면 IPv6가 막 성공하려던 순간에도 모든 서버에 불필요한 중복 IPv4 연결을 퍼부어, v6 선호의 효율을 내다버린다. 4분의 1초는 건강한 IPv6가 거의 항상 경주를 완승할 만큼 길고 — 그래서 원하던 v6-우선 동작을 여전히 얻고 — v6가 블랙홀일 땐 사용자가 30초가 아니라 250밀리초를 무는 만큼 짧다. 사용자는 눈치채지 못한다. 그게 핵심이다.
슬라이드에 아무도 안 넣는 함정
Happy Eyeballs는 탁월한 엔지니어링이다. 동시에, IPv6 인터넷의 실제 건강을 신경 쓴다면, 조용한 재앙이다 — 정확히 그것이 탁월한 그 이유 때문에.
너무 잘 작동해서 근본 병을 고칠 압력을 없애버렸다. Happy Eyeballs 이전엔 망가진 IPv6가 시끄러웠다: 사이트를 느리게 만들고, 사용자가 불평하고, 당신에겐 드롭된 라우트를 찾아 나설 이유가 있었다. Happy Eyeballs 이후엔 망가진 IPv6가 조용하다. 모든 주류 브라우저가 250밀리초 안에 덮어버린다. 사용자는 행복하다. 지표는 멀쩡하다. 그리고 당신의 IPv6 경로는 완전히, 통째로 망가져 — 모든 패킷을 드롭하며 — 있을 수 있는데 당신은 그에 대해 한마디도 듣지 못한다.
이걸 업계 전체에 곱하면 우리가 실제로 처한 상황이 나온다: 인터넷의 AAAA 레코드 중 미지의, 그러나 실재하는 일부가 작동하지 않는 경로를 가리키고 있고, 그걸 유지하는 사람들은 전혀 모른다. 클라이언트가 어쨌든 성공하도록 설계됐기 때문이다. IPv6 채택을 무통증으로 만들려던 알고리즘이, 정작 중요한 단 하나의 관측 지점 — 그걸 써야 할 클라이언트 — 에서 IPv6 고장을 탐지 불가능하게도 만들었다. 이걸 프로빙하는 측정 프로젝트(APNIC은 Happy Eyeballs가 클라이언트마다 얼마나 들쭉날쭉 구현됐는지에 대한 지속 분석을 내놓았다)는 실제 동작이 RFC의 깔끔한 의사코드보다 훨씬 지저분하다는 걸 계속 발견한다.
그리고 여기서 당신을 물어뜯는다. 안전망은 보편적이지 않다. 브라우저는 Happy Eyeballs를 구현하고, 그럭저럭 잘한다. 그 외엔 거의 아무것도 그만큼 잘하지 못한다. 수많은 언어의 HTTP 클라이언트, DB 드라이버, 커맨드라인 도구, 손으로 짠 connect() 호출은 아예 구현하지 않거나 반쪽만 망가지게 구현한다. 그래서 happy eyeballs가 없는 코드 — 백엔드 마이크로서비스, 모니터링 프로브, 마이그레이션 스크립트, 새벽 3시 cron 슬롯에서 도는 그것 — 이 바로, 사용자는 가려졌던 30초짜리 멈춤을 통째로 물려받는 코드다. 당신은 그걸 애플리케이션 버그로, 혹은 DNS 버그로, 혹은 “네트워크가 불안정하다”는 비-답으로 디버깅하며 오후를 날릴 것이다. 진실을 보여줬을 도구가 브라우저인데, 브라우저는 작동함으로써 당신에게 거짓말을 했으니까.
실제로 뭘 할 것인가
교훈은 IPv6 너머로 일반화된다: 너무 잘 작동하는 폴백은 자기가 폴백하는 대상을 숨기고, 숨겨진 실패는 비싼 종류다. 복원력과 관측 가능성은 서로를 당기고, Happy Eyeballs는 그 거래의 가장 깔끔한 예다 — 당신이 알아챌 능력을 지불해 복원력을 산 것이다.
그러니 듀얼스택을 운영한다면 그 초록 대시보드를 믿지 마라. IPv6 경로를 직접, 홀로 테스트하라. v4 탈출구 없이 v6를 강제하고, IPv6가 실제로 작동하는 네트워크에서 — 그게 당신 사용자의 브라우저가 예의상 숨기고 있는 걸 볼 유일한 방법이니까. 클라이언트가 멈췄다 회복하면, 애플리케이션을 의심하지 말고 한 주소 패밀리가 블랙홀인지 가서 보라. 그리고 버전 번호가 아직 움직이고 있음을 알아두라: IETF에 Happy Eyeballs v3 초안이 활발히 돌며 해석·주소 선택 디테일을 다듬고 있는데, 이는 “풀린” 문제가 매끄러운 250밀리초 연결이 보여준 만큼 온전히 풀린 적이 없었음을 말해준다.
연결은 순식간처럼 느껴진다. 그 좋은 느낌은 알고리즘이 제 일을 하는 것이다. 그 밑의 무언가가 실제로 작동하고 있다는 증거는 아니다.