Ruby HEY 앱 메모리 누수 진단 및 해결 과정

37signals Dev — My adventures hunting down a Ruby memory leak 🎢

3줄 요약

  • HEY 앱에서 발생한 장기적인 메모리 누수 문제를 Ruby의 고급 진단 도구를 활용하여 해결한 과정을 설명합니다.
  • 초기 힙 할당 분석의 한계를 넘어 `rbtrace`, `ObjectSpace.trace_object_allocations_start`, `heapy`, `sheap`을 통해 YJIT 관련 근본 원인을 파악했습니다.
  • 이 사례는 Ruby 개발자가 메모리 누수를 효과적으로 진단하고 해결하는 데 필요한 도구와 방법을 제시합니다.

이 글에서는 HEY 앱에서 발생한 심각한 메모리 누수 문제에 대한 진단 및 해결 과정을 상세히 다룹니다. 특히, 주말 동안 배포가 없는 상황에서 메모리 사용량이 100%에 육박하는 현상이 반복적으로 관찰되었으며, 이는 느리지만 꾸준한 메모리 누수를 명확히 시사했습니다. 본 문서는 이러한 복잡한 문제를 해결하기 위해 사용된 다양한 도구와 단계별 탐색 과정을 설명함으로써, 유사한 상황에 직면한 다른 개발자들에게 실질적인 도움을 제공하고자 합니다.

문제 해결을 위한 첫 번째 접근 방식은 루비 힙 할당 통계를 로깅하고 분석하는 것이었습니다. 가비지 컬렉션(GC) 실행 후 heap_available_slots가 크게 증가하는 엔드포인트(MessagesController#update, TopicsController#show)를 발견했지만, 이들은 앱에서 가장 많이 사용되는 엔드포인트였기에 누수의 근본 원인을 특정하기에는 부족했습니다.

다음 단계로 rbtrace를 사용하여 루비 힙 덤프를 추출하려 했습니다. 그러나 ObjectSpace.trace_object_allocations_start를 활성화하지 않은 초기 힙 덤프는 GC 생성 세대, 할당된 파일명 및 줄 번호, 객체 바이트 크기 등 핵심 정보가 누락되어 문제의 원인을 명확히 파악하기 어려웠습니다. 이에 따라, 성능 저하와 메모리 사용량 증가를 감수하더라도 ObjectSpace.trace_object_allocations_start를 활성화하는 것이 불가피하다고 판단했습니다. 영향을 최소화하기 위해 단일 앱 호스트에서 필요한 시간 동안만 해당 기능을 활성화하고, 이 과정을 면밀히 모니터링했습니다.

이러한 준비 끝에 배포 후 약 15분, 40분, 1시간 간격으로 총 세 개의 힙 덤프(0.json, 1.json, 2.json)를 성공적으로 추출할 수 있었습니다. 추출된 힙 덤프는 heapy diff 도구를 사용하여 초기 분석되었고, 이 과정에서 lib/okra/html.rb:19에서 상당량의 STRING, OBJECT, ARRAY 객체가 남아 있음을 확인했습니다. 처음에는 이를 유력한 원인으로 보았으나, 해당 위치에서 직접적인 메모리 누수 코드를 찾을 수 없었습니다.

ObjectSpace.trace_object_allocations_start가 활성화된 힙 덤프에는 객체 의존성 트리가 포함되어 있었기에, sheap 도구를 활용하여 lib/okra/html.rb:19와 관련된 객체들의 의존성 트리를 심층적으로 분석했습니다. 이 분석을 통해 모든 문제 객체들이 yjit_root 노드를 통해 YJIT에 의해 유지되고 있다는 공통된 패턴을 발견했습니다. 이는 메모리 누수의 근본 원인이 YJIT 내부의 버그임을 명확히 시사했습니다. 근본 원인을 파악한 즉시 YJIT 팀에 해당 문제를 보고했으며, YJIT 팀은 신속하게 이 버그를 수정하여 문제를 해결했습니다.

결론적으로, HEY 앱의 메모리 누수 문제 해결 사례는 루비의 현재 메모리 누수 진단 도구들이 매우 발전되어 있음을 입증합니다. `rbtrace`를 사용하여 객체 추적 할당을 활성화한 힙 덤프를 추출하고, 이를 `heapy`, `sheap`과 같은 전문 도구를 통해 분석하는 것이 효과적인 접근 방식임이 확인되었습니다. 이러한 고급 도구들을 적극적으로 활용함으로써, 개발자들은 복잡하고 추적하기 어려운 메모리 누수 문제를 체계적으로 진단하고 성공적으로 해결할 수 있습니다.