Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

TCmalloc으로 Ruby 메모리 할당 프로파일링하기

Ruby에서 메모리 할당은 어떻게 이루어질까요?

Ruby는 페이지(page)라고 불리는 일정한 덩어리 단위로 메모리를 확보하며, 새롭게 생성된 객체들은 이 페이지 안에 저장됩니다.

그런데…

페이지가 가득 차면 추가적인 메모리가 필요해집니다.

이때 Ruby는 malloc 함수를 통해 운영체제에 더 많은 메모리를 요청합니다.

malloc 함수는 운영체제 자체에 포함된 기능이지만, 상황에 따라 다른 구현체로 교체하여 사용할 수도 있습니다.

그중 하나가 바로 구글(Google)의 tcmalloc입니다.

TCmalloc은 Google Performance Tools(gperftools) 스위트의 일부입니다.

이 도구들을 활용하면 Ruby가 메모리를 할당하는 과정을 아주 정밀하게 들여다볼 수 있습니다.

그리고 리눅스 환경에서 제공되는 LD_PRELOAD 환경 변수 덕분에, 시스템 기본 malloc 함수를 tcmalloc으로 손쉽게 교체할 수 있습니다.

사용 방법은 다음과 같습니다:

LD_PRELOAD="/usr/lib/libtcmalloc.so" ruby -e "puts 123"

다만 위 명령어는 라이브러리를 로드할 뿐, 아직 데이터 수집 기능은 활성화되지 않습니다.

프로파일러 활성화하기

tcmalloc의 프로파일러는 HEAPPROFILE이라는 환경 변수를 하나 더 추가하면 활성화할 수 있습니다.

LD_PRELOAD="/usr/lib/libtcmalloc.so" HEAPPROFILE=/tmp/profile ruby -e "puts 123"

실행하면 다음과 같은 출력 결과를 얻을 수 있습니다:

Starting tracking the heap
123
Dumping heap profile to /tmp/profile.0001.heap (Exiting, 2 MB in use)

여기서 프로파일러가 정상적으로 활성화되었다는 확인 메시지를 볼 수 있습니다.

그리고 그 아래에는:

  • 프로그램의 실행 결과(출력값)
  • 생성된 파일명(profile.0001.heap)
  • 프로그램이 사용한 메모리 양(2 MB)

이 파일의 내용을 읽으려면 tcmalloc에 함께 포함된 또 다른 도구가 필요합니다.

pprof --text `which ruby` /tmp/profile.0001.heap | head -10

실행 결과는 다음과 같습니다:

Total: 2.4 MB
   1.1  44.7%  44.7%      1.1  44.7% 0x00005570fa4df074
   0.7  27.8%  72.5%      0.7  27.8% 0x00005570fa4e0c09
   0.4  15.3%  87.8%      0.4  15.3% 0x00005570fa4db460
   0.1   5.9%  93.7%      0.1   5.9% 0x00005570fa4df19f
   0.1   3.2%  96.9%      0.1   3.2% 0x00005570fa6349a0
   0.0   1.4%  98.3%      0.0   1.4% 0x00005570fa589924
   0.0   0.3%  98.6%      0.0   0.3% 0x00005570fa59c4f2
   0.0   0.3%  98.8%      0.0   0.3% 0x00005570fa4db48a
   0.0   0.2%  99.0%      0.0   0.2% 0x00005570fa4dbaa5
   0.0   0.2%  99.1%      0.0   0.2% _dl_new_object

결과가 그저 알아볼 수 없는 메모리 주소들뿐이죠? 함수 이름을 확인하려면 디버깅 심볼(debugging symbols)이 포함된 버전의 Ruby가 필요합니다.

디버깅 심볼이 있으면 다음과 같은 출력을 얻을 수 있습니다:

Using local file ruby.
Using local file /tmp/profile.0001.heap.
Total: 2.9 MB
   1.0  36.2%  36.2%      1.0  36.2% objspace_xmalloc0
   0.7  26.1%  62.4%      0.7  26.1% aligned_malloc
   0.5  18.8%  81.1%      0.5  18.8% objspace_xcalloc
   0.3   9.9%  91.0%      0.3   9.9% stack_chunk_alloc
   0.1   3.7%  94.7%      0.1   3.7% objspace_xrealloc
   0.1   2.7%  97.4%      0.1   2.7% Init_Method
   0.0   1.3%  98.7%      0.0   1.7% onig_new_with_source
   0.0   0.4%  99.2%      0.8  26.6% heap_page_allocate
   0.0   0.2%  99.4%      0.0   0.2% add_bitset

여기에 표시되는 내용은 MRI(Ruby 인터프리터)의 어떤 함수가 얼마나 많은 메모리를 할당했는지를 보여줍니다.

흥미로운 점은, aligned_malloc은 Ruby 객체를 위한 새 페이지를 할당하는 함수이고, stack_chunk_alloc은 GC(가비지 컬렉터)가 마킹(mark) 단계에서 사용하는 함수라는 것입니다. 또한 objspace_xmalloc0 / objspace_xcalloc은 문자열, 배열 등 RVALUE 구조체에 담을 수 없는 데이터를 위한 공간을 할당합니다.

tcmalloc과 Ruby 객체의 관계

중요한 점은, tcmalloc은 Ruby 객체에 대해 전혀 알지 못한다는 사실입니다. tcmalloc이 하는 일은 오직 malloc, calloc, realloc 호출을 추적하여 얼마나 많은 메모리가 요청되었는지 파악하는 것뿐입니다.

Ruby 레벨에서 힙 덤프(heap dump)를 얻고 싶다면 ObjectSpace.dump_all을 사용할 수 있습니다. 이를 통해 애플리케이션에 존재하는 모든 살아있는 객체와 각각의 메모리 크기가 담긴 JSON 파일을 얻을 수 있습니다.

반면 tcmalloc이 보여주는 것은, 최종적으로 메모리를 요청하게 되는 모든 C 함수들의 시각화입니다.

pprof --web `which ruby` /tmp/profile.0001.heap

이 명령을 실행하면 Chrome이나 Firefox에서 다음과 같은 SVG 파일이 열립니다:

TCmalloc으로 Ruby 메모리 할당 프로파일링하기

TCmalloc은 이처럼 훌륭한 프로파일링 기능을 제공할 뿐만 아니라, 이를 사용하면 애플리케이션 성능을 4~9%까지 향상시킬 수도 있습니다! 프로파일러를 포함하고 있는 또 다른 malloc 구현체인 jemalloc도 함께 시도해 볼 만합니다.

정리

이번 글에서는 gperftools(Google Performance Tools)를 활용하여 Ruby 인터프리터의 메모리 사용량을 시각화하고 분석하는 방법을 배웠습니다.

읽어주셔서 감사합니다! 🙂