메모리 누수는 의도하지 않고, 통제할 수 없으며, 메모리 사용량이 끝없이 증가하는 것입니다. 아무리 작더라도 결국에는 누수로 인해 프로세스의 메모리가 부족해지고 충돌이 발생하게 됩니다. 이러한 충돌을 피하기 위해 주기적으로 앱을 다시 시작하더라도(판단할 필요는 없습니다. 그렇게 했습니다!) 여전히 메모리 누수로 인해 성능에 영향을 받습니다.
메모리 누수에 관한 두 부분으로 구성된 시리즈 중 첫 번째인 이 게시물에서는 Ruby가 메모리를 관리하는 방법, GC(가비지 수집) 작동 방식, 누수를 찾는 방법부터 살펴보겠습니다.
두 번째 부분에서는 누출 추적에 대해 더 자세히 살펴보겠습니다.
시작해 보세요!
Ruby 메모리 관리
Ruby 개체는 힙에 저장되며 각 개체는 힙의 한 슬롯을 채웁니다.
Ruby 3.1 이전에는 힙의 모든 슬롯 크기가 동일했습니다(정확하게는 40바이트). 슬롯에 들어갈 수 없을 정도로 큰 개체는 힙 외부에 저장되었습니다. 각 슬롯에는 개체가 이동된 위치에 대한 참조가 포함되어 있습니다.
Ruby 3.1에서는 String에 대한 가변 너비 할당 개체가 병합되었습니다. 곧 가변 너비 할당이 모든 개체 유형의 표준이 될 것입니다.
가변 너비 할당은 캐시 지역성을 개선하여 성능을 향상시키는 것을 목표로 합니다. 즉, 개체의 모든 정보가 두 개의 메모리 위치가 아닌 한 곳에 저장됩니다.
또한 메모리 관리의 일부 부분을 단순화해야 합니다. 현재 두 개의 '힙'이 있습니다:
- 더 작은 Ruby 객체를 저장하는 Ruby 힙(또는 GC 힙)
- 더 큰 객체를 저장하는 C 힙(또는 malloc/일시 힙)
가변 너비 할당이 표준이면 후자의 힙이 필요하지 않습니다.
힙은 지정된 크기(기본적으로 10,000개 슬롯)에서 시작되며 객체는 생성될 때 사용 가능한 슬롯에 할당됩니다. Ruby가 객체를 생성하려고 시도하고 사용 가능한 여유 슬롯이 없으면 일부 여유 슬롯을 사용 가능하게 만들기 위해 GC(가비지 수집)가 발생합니다.
GC 이후 여유 슬롯이 너무 적으면 힙이 확장됩니다(이에 대해서는 나중에 자세히 설명합니다).
환경 변수와 함께 제어할 수 있는 요소는 다음과 같습니다.
- 힙의 초기 크기 -
RUBY_GC_HEAP_INIT_SLOTS - GC 발생 후 사용할 수 있는 여유 슬롯 수 -
RUBY_GC_HEAP_FREE_SLOTS - 힙이 확장되는 양 -
RUBY_GC_HEAP_GROWTH_FACTOR
Ruby의 가비지 컬렉션
Ruby의 가비지 수집은 '세상을 중지'합니다. GC가 발생할 때 다른 프로세스는 발생하지 않습니다. Ruby의 가비지 수집(2.1부터)도 세대입니다. , 이는 가비지 수집기에 두 가지 모드가 있음을 의미합니다:
- 마이너 GC - '젊은' 개체(최근에 생성된 개체)를 검사합니다.
- 주요 GC - '오래된' 객체뿐만 아니라 '젊은' 객체도 검사합니다(모든 개체)
참고 :'오래된' 개체가 3에서 살아 남았습니다. 메이저 또는 마이너 GC가 실행됩니다.
힙이 가득 차면 마이너 GC가 먼저 호출됩니다. 한도 미만이 되도록 충분한 슬롯을 확보할 수 없으면 주요 GC가 호출됩니다. 그런 다음 여유 슬롯이 여전히 충분하지 않으면 힙이 확장됩니다.
Major GC는 더 많은 객체를 보기 때문에 Minor GC보다 비용이 더 많이 듭니다.
세대별 GC의 성능이 더 뛰어난 이유에 대한 이론은 개체가 일반적으로 두 가지 범주로 분류된다는 것입니다.
- 할당된 후 빠르게 범위를 벗어나는 개체 Rails 앱에서 페이지를 렌더링하기 위해 DB에서 가져온 모델은 요청이 끝나면 범위를 벗어나게 됩니다.
- 할당되어 장기간 보관되는 개체 클래스와 캐시는 앱 수명 내내 계속 사용될 가능성이 높습니다.
메이저 GC는 여유 슬롯이 충분하더라도 오래된 객체의 수가 특정 임계값을 초과하는 경우 마이너 GC 이후에도 실행됩니다. 이 제한은 힙 크기가 커짐에 따라 증가하며 RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR으로 제어할 수 있습니다. 환경변수입니다.
누출이 발생하면 정리할 수 없는 개체가 생성됩니다. old가 점점 더 많아지고 있습니다. 객체. 이는 메이저(비싼) GC가 필요한 것보다 훨씬 더 자주 실행된다는 것을 의미합니다. GC가 실행되는 동안에는 다른 어떤 것도 실행되지 않으므로 낭비되는 시간입니다.
Ruby의 메모리 레이아웃과 가비지 수집기에 대해 더 자세히 읽을 수 있도록 이 기사 끝에 몇 가지 링크를 남겼습니다.
Ruby에서 메모리 누수는 어떻게 되나요?
모든 Unix 시스템에서 사용할 수 있는 간단한 도구를 사용하여 메모리 누수를 확인할 수 있습니다. 다음 코드를 예로 들어보겠습니다.
이 코드가 '누설'되었다고 말하는 것은 약간 불공평합니다. 코드가 하는 일은 누출뿐입니다! —하지만 그것은 우리의 목적에 부합합니다.
한 터미널에서 이 프로그램을 실행하고 watch하여 명령줄에서 아주 간단하게 누출을 관찰할 수 있습니다. -ps를 사용하여 시간이 지남에 따라 메모리가 증가합니다. .
pgrep -f "ruby ./leaky.rb" ps를 제한할 수 있도록 프로세스 ID를 찾습니다. 우리가 관심 있는 프로세스에만 출력합니다. 추측할 수 있듯이 grep와 같습니다. 프로세스를 위해.
watch 도구를 사용하면 특정 명령의 출력을 폴링하고 업데이트하여 터미널 내에서 실시간 대시보드를 제공할 수 있습니다.
몇 초마다 업데이트되는 다음과 같은 출력을 얻게 됩니다.
%MEM가 표시되어야 합니다. 및 RSS 증가. 그들은:
%MEM- 호스트 시스템의 메모리 비율로 프로세스가 사용하는 메모리 양.RSS(상주 세트 크기) - 프로세스가 사용하는 RAM의 양(바이트)
이 기본 OS 전용 정보는 누출이 있는지 알아내기에 충분합니다. 메모리가 계속 증가한다면 누출이 있다는 뜻입니다!
가비지 수집기 모듈로 Ruby 누출 찾기
GC을 사용하여 Ruby 코드 자체 내에서 누출을 감지할 수도 있습니다. 모듈입니다.
GC.stat 메소드는 유용한 정보가 많이 포함된 해시를 반환합니다. 여기서는 :heap_live_slots에 관심이 있습니다. 는 사용 중인 힙의 슬롯 수입니다. :heap_free_slots의 반대입니다. .루프가 끝나면 주요 GC를 강제 실행하고 사용된 슬롯 수, 즉 GC 이후에 남아 있는 객체 수를 인쇄합니다.
작은 프로그램을 실행하면 무한히 증가하는 것을 볼 수 있습니다. 누출이 있습니다! GC.stat(:old_objects)를 사용할 수도 있습니다. 같은 효과입니다.
GC 모듈을 사용하여 if를 확인할 수 있습니다. 누출이 있습니다. (puts을 잘 알고 계시다면 문) 누수가 발생할 수 있는 위치에서 ObjectSpace을 사용하여 누수될 수 있는 개체 유형을 확인할 수 있습니다. 모듈입니다.
ObjectSpace.count_objects 메소드는 라이브 객체의 개수가 포함된 해시를 반환합니다. T_STRING 예를 들어, 메모리에 있는 문자열의 수입니다. 다소 누출이 심한 프로그램의 경우 이 값은 GC 이후에도 각 루프마다 증가합니다. 문자열 개체가 누출되고 있음을 알 수 있습니다.
AppSignal을 사용한 프로덕션 환경의 애플리케이션 성능 모니터링
ps을 가지고 놀면서 및 GC 장난감 프로젝트를 위한 합리적인 경로가 될 수 있습니다. 또한 사용하기에 재미있고 유익합니다! — 저는 하지 않을 것입니다 프로덕션 앱의 메모리 누수 감지 솔루션으로 추천해 보세요.
여기에서 APM(애플리케이션 성능 모니터링) 도구를 사용할 수 있습니다. 매우 큰 회사라면 직접 구축할 수도 있습니다. 하지만 작은 의상의 경우 기성품 APM을 선택하는 것이 좋습니다. 월간 구독료를 지불해야 하지만 그들이 제공하는 정보가 이를 보상하고도 남습니다.
메모리 누수를 감지하기 위해 시간 경과에 따른 서버 또는 프로세스 메모리 사용(RSS라고도 함) 그래프를 찾고 싶습니다. 다음은 배포 직후 정상적인 앱에 대한 AppSignal의 '프로세스 메모리 사용량' 대시보드의 예시 스크린샷입니다.

배포 후 비정상 앱은 다음과 같습니다.

AppSignal은 GC 및 힙 슬롯과 같은 Ruby VM 통계도 표시하므로 메모리 누수에 대한 더욱 명확한 신호를 제공할 수 있습니다. 라이브 슬롯 수가 계속 증가하면 누출이 있는 것입니다!

Ruby용 AppSignal에 대해 자세히 알아보세요.
마무리 및 추가 자료
이번 포스팅에서는 Ruby의 메모리 관리와 가비지 컬렉터에 대해 간략하게 살펴보았습니다. 그런 다음 Unix 도구와 Ruby의 GC 모듈을 사용하여 메모리 누수를 발견하는 방법을 진단했습니다.
다음번에는 memory_profiler 사용법을 알아보겠습니다. 및 derailed_benchmarks 누출을 찾아 고치기 위해.
그동안 우리가 사용한 도구에 대한 자세한 내용을 읽어보실 수 있습니다:
watchpspgrep
추가 자료:
GC모듈 문서ObjectSpace모듈 문서- 쓰레기 수집 심층 분석
- 가변 너비 할당
즐거운 코딩 되시고 다음에 뵙겠습니다!
추신 Ruby Magic 게시물이 보도되는 즉시 읽으려면 Ruby Magic 뉴스레터를 구독하고 단 하나의 게시물도 놓치지 마세요!