Ruby의 TCP 소켓에 Happy Eyeballs v2 도입기

[JA] Making TCPSocket.new "Happy"! / Misaki Shioi @coe401_

작성자
RubyKaigi
발행일
2025년 05월 27일

핵심 요약

  • 1 Ruby 3.4에서 TCP 소켓의 연결 속도 개선을 위해 Happy Eyeballs 버전 2 알고리즘이 성공적으로 도입되었습니다.
  • 2 이 알고리즘은 IPv6와 IPv4 주소 해결 및 연결 시도를 병렬로 처리하여, 특히 IPv6 연결이 지연될 때 발생하는 지연 문제를 해결합니다.
  • 3 구현 과정에서 복잡한 상태 관리, 테스트 실패, 파일 디스크립터 제한 등 다양한 기술적 난관을 극복하며 안정적인 통합을 이루어냈습니다.

도입

본 발표는 Ruby 3.4 버전에서 소켓 라이브러리의 TCP 연결 성능을 최적화하기 위해 'Happy Eyeballs 버전 2' 알고리즘을 도입한 과정과 그에 따른 기술적 도전 과제를 다룹니다. 기존 Ruby의 `Socket::TCP` 및 `TCPSocket.new` 메서드는 IPv6와 IPv4 주소를 순차적으로 처리하여, IPv6 주소 해결이나 서버 연결에 시간이 소요될 경우 IPv4로의 즉각적인 폴백이 어렵다는 한계가 있었습니다. Happy Eyeballs 버전 2는 이러한 문제를 해결하고 보다 효율적인 연결을 목표로 하는 알고리즘으로, 병렬적인 이름 해결 및 연결 시도를 통해 연결 지연을 최소화합니다.

Happy Eyeballs 버전 2 알고리즘은 IPv6와 IPv4의 이름 해결을 동시에 시작하고, 먼저 완료된 주소 중 하나로 연결을 시도합니다. 이때, IPv6 연결을 우선시하기 위해 IPv4 이름 해결이 먼저 완료되더라도 50밀리초(Resolution Delay) 동안 IPv6 해결을 기다립니다. 연결 시도 후 250밀리초(Connection Attempt Delay) 내에 연결이 확립되지 않으면 다른 주소로 두 번째 연결을 시도하는 방식으로 진행됩니다. 성공적인 연결이 이루어지면 다른 모든 시도는 취소됩니다.

Socket::TCP에 이 알고리즘을 구현하는 초기 단계에서는 상태 기반의 코드를 사용했으나, 이는 의도치 않은 중복 처리를 야기할 수 있었습니다. 예를 들어, IPv6와 IPv4 이름 해결이 동시에 완료될 경우 불필요한 IO.select 호출이 발생하는 문제점이 발견되었습니다. 이를 개선하기 위해 상태 관리를 제거하고, 매 루프마다 가능한 모든 처리를 조건부로 실행하는 방식으로 재구현하여 코드의 복잡성을 줄이고 효율성을 높였습니다.

TCPSocket.new는 C 언어로 구현되어 있어 Happy Eyeballs를 도입하는 데 추가적인 난관이 있었습니다. 특히, 이름 해결 결과를 파이프와 select 시스템 콜을 통해 기다릴 수 있는 내부 함수가 없다는 점이 큰 문제였습니다. Ruby 3.3부터 pthread 라이브러리를 활용하여 getaddrinfo 함수가 실행 중에 중단될 수 있도록 개선되었지만, 기존의 rb_getaddrinfo 함수는 Happy Eyeballs의 병렬 처리 요구사항(주소 패밀리별 스레드, 조건 변수 대신 파이프/select 사용)을 충족하지 못했습니다. 이에 Init_fast_fallback_inet_sock_internal이라는 새로운 C 함수를 도입하여 주소 패밀리별로 별도의 스레드를 생성하고, select 시스템 콜을 사용하여 이름 해결 및 연결 완료를 기다리도록 설계했습니다.

개발 과정에서는 여러 중요한 이슈가 발생했습니다. 첫째, net/http 테스트에서 ConnectionAttemptDelay로 인해 쓰기 타임아웃 대신 연결 타임아웃이 발생하는 문제가 있었습니다. 이는 IPv6 연결 시도가 실패한 후 IPv4로 폴백해야 할 때 ConnectionAttemptDelay (250ms)가 테스트의 연결 타임아웃 (100ms)보다 길어 발생한 것이었습니다. 이 문제는 이전의 모든 연결 시도가 실패했을 경우 connection_attempt_delay_expires_at 변수를 즉시 리셋하여 다음 연결 시도를 즉각적으로 시작할 수 있도록 수정함으로써 해결되었습니다. 둘째, TCPSocket.newFD_SETSIZE (파일 디스크립터 최대 개수)를 초과하는 파일 디스크립터를 열 때 세그먼트 폴트가 발생하는 문제가 발생했습니다. 이는 select 시스템 콜이 FD_SETSIZE 이상의 FD 번호를 모니터링할 수 없다는 man 페이지의 경고와 일치하는 현상이었습니다. 이 문제를 해결하기 위해 select 대신 Ruby의 내부 함수인 rb_fd_select를 사용했습니다. rb_fd_select는 FD 세트의 비트맵 영역을 동적으로 할당하여 FD_SETSIZE 제한을 우회할 수 있도록 설계되어 있어, 현대 운영체제에서 더 큰 FD를 처리할 수 있게 합니다.

최종적으로 Happy Eyeballs 기능은 메서드 인자, 소켓 클래스 접근자, 환경 변수를 통해 활성화/비활성화를 제어할 수 있도록 제공되며, 기본적으로는 활성화된 상태로 배포되었습니다.

결론

이러한 노력 끝에 Happy Eyeballs 버전 2는 Ruby 3.4의 소켓 라이브러리에 성공적으로 통합되었습니다. 성능 측정 결과, IPv6 이름 해결이 지연되는 최악의 시나리오에서는 Happy Eyeballs를 통해 연결 시간이 약 132배 단축(15초에서 0.1초)되는 극적인 개선을 보여주었습니다. 반면, 일반적인 상황에서는 약간의 오버헤드(0.129초에서 0.144초)가 관찰되었습니다. 이 기능은 특정 환경(예: 동일 IP 주소에서 여러 연결을 제한하는 서버, 엄격한 FD 수 제한)에서는 비활성화할 필요가 있을 수 있으며, 이를 위한 제어 기능이 제공됩니다. 본 프로젝트는 커뮤니티의 적극적인 기여와 협력을 통해 성공적으로 마무리되었으며, 발표자는 이 과정에서 Ruby 커미터가 되는 영광을 얻었습니다. 이는 Ruby 커뮤니티의 활발한 활동이 언어 자체의 지속적인 발전에 기여함을 보여주는 사례입니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!