Ruby 애플리케이션의 메모리 팽창 문제는 자주 논의되는 주제입니다. 이 게시물에서는 Ruby 메모리 관리가 어떻게 잘못될 수 있는지, Ruby 애플리케이션이 중단되는 것을 방지하기 위해 무엇을 할 수 있는지 살펴보겠습니다.
먼저 애플리케이션 메모리의 맥락에서 팽창이 무엇을 의미하는지 이해해야 합니다.
뛰어들자!
Ruby의 메모리 팽창이란 무엇입니까?
메모리 팽창은 명백한 설명 없이 애플리케이션의 메모리 사용량이 갑자기 증가하는 경우입니다. 이러한 증가는 빠르거나 빠르지 않을 수 있지만 대부분의 경우 계속됩니다. 일정 시간 동안 여러 번 실행되어 발생하는 메모리 누수와 다릅니다.
메모리 팽창은 프로덕션 환경에서 Ruby 애플리케이션에 발생하는 최악의 상황 중 하나일 수 있습니다. 그러나 적절한 조치를 취하면 피할 수 있습니다. 예를 들어 애플리케이션의 메모리 사용량이 급증한 경우 다른 문제를 해결하기 전에 메모리 팽창의 징후가 있는지 확인하는 것이 가장 좋습니다.
메모리 팽창을 진단하고 수정하는 방법을 살펴보기 전에 Ruby의 메모리 아키텍처를 간단히 살펴보겠습니다.
루비의 메모리 구조
Ruby의 메모리 사용량은 사용 가능한 시스템 리소스의 적절한 사용을 관리하는 특정 요소를 중심으로 이루어집니다. 이러한 요소에는 Ruby 언어, 호스트 운영 체제 및 시스템 커널이 포함됩니다.
이 외에도 가비지 수집 프로세스는 Ruby 메모리를 관리하고 재사용하는 방법을 결정하는 데 중요한 역할을 합니다.
Ruby 힙 페이지 및 메모리 슬롯
Ruby 언어는 객체를 힙 페이지라고 하는 세그먼트로 구성합니다. 전체 힙 공간(사용 가능한 메모리)은 사용된 섹션과 빈 섹션으로 나뉩니다. 이러한 힙 페이지는 각각 하나의 단위 크기 개체를 허용하는 동일한 크기의 슬롯으로 더 분할됩니다.
새 객체에 메모리를 할당할 때 Ruby는 먼저 사용된 힙 공간에서 여유 슬롯을 찾습니다. 아무것도 발견되지 않으면 빈 섹션에서 새 힙 페이지를 할당합니다.
메모리 슬롯은 각각 크기가 거의 40바이트인 작은 메모리 위치입니다. 이 슬롯에서 넘친 데이터는 힙 페이지 외부의 다른 영역에 저장되고 각 슬롯은 외부 정보에 대한 포인터를 저장합니다.
시스템의 메모리 할당자는 힙 페이지 및 외부 데이터 포인터를 포함하여 Ruby 런타임 환경에서 모든 할당을 수행합니다.
Ruby의 운영 체제 메모리 할당
Ruby 언어에 의한 메모리 할당 호출은 호스트 운영 체제의 메모리 할당자가 처리하고 응답합니다.
일반적으로 메모리 할당자는 C 함수 그룹, 즉 malloc으로 구성됩니다. , 호출 , realloc , 및 무료 . 각각에 대해 빠르게 살펴보겠습니다.
- Malloc :Malloc은 메모리 할당을 의미하며 객체에 여유 메모리를 할당하는 데 사용됩니다. 할당할 메모리의 크기를 받아서 할당된 메모리 블록의 시작 인덱스에 대한 포인터를 반환합니다.
- 캘록 :Calloc은 연속 할당을 나타내며 Ruby 언어가 연속적인 메모리 블록을 할당할 수 있도록 합니다. 알려진 길이의 객체 배열을 할당할 때 유용합니다.
- 재할당 :Realloc은 re-allocation의 약자로 언어가 새로운 크기로 메모리를 재할당할 수 있도록 합니다.
- 무료 :Free는 미리 할당된 메모리 위치 집합을 지우는 데 사용됩니다. 해제해야 하는 메모리 블록의 시작 인덱스에 대한 포인터를 받습니다.
Ruby의 가비지 컬렉션
언어 런타임의 가비지 수집 프로세스는 사용 가능한 메모리를 얼마나 잘 활용하는지에 큰 영향을 미칩니다.
Ruby는 위에서 설명한 모든 API 메서드를 사용하여 항상 애플리케이션 메모리 소비를 최적화하는 고급 가비지 수집 기능을 가지고 있습니다.
Ruby의 가비지 수집 프로세스에 대한 흥미로운 사실은 전체 응용 프로그램을 중지한다는 것입니다! 이렇게 하면 가비지 수집 중에 새로운 개체 할당이 발생하지 않습니다. 이 때문에 가비지 수집 루틴은 드물고 가능한 한 빨라야 합니다.
Ruby에서 메모리 팽창의 두 가지 일반적인 원인
이 섹션에서는 Ruby에서 메모리 팽창이 발생하는 가장 중요한 두 가지 이유인 단편화와 느린 릴리스에 대해 설명합니다.
메모리 단편화
메모리 조각화는 메모리의 개체 할당이 전체에 분산되어 사용 가능한 메모리의 연속 청크 수를 줄이는 경우입니다. 디스크에 사용 가능한 여유 메모리가 충분하더라도 연속 블록이 없는 개체에는 메모리를 할당할 수 없습니다. 이 문제는 모든 프로그래밍 언어나 환경에서 발생할 수 있으며 언어마다 문제 해결 방법이 있습니다.
단편화는 언어 수준과 메모리 할당자 수준의 두 가지 수준에서 발생할 수 있습니다. 이 두 가지를 자세히 살펴보겠습니다.
루비 수준의 조각화
언어 수준의 단편화는 가비지 수집 프로세스의 설계로 인해 발생합니다. 가비지 수집 프로세스는 Ruby 힙 페이지 슬롯을 여유 공간으로 표시하여 해당 슬롯을 재사용하여 메모리에 다른 객체를 할당할 수 있도록 합니다. 완전한 Ruby 힙 페이지가 여유 슬롯으로만 구성된 경우 해당 힙 페이지는 재사용을 위해 메모리 할당기로 해제될 수 있습니다.
그러나 힙에서 아주 적은 수의 슬롯이 free로 표시되지 않으면 어떻게 될까요? 메모리 할당자로 다시 해제되지 않습니다. 이제 가비지 수집에 의해 동시에 할당되고 해제되는 다양한 힙 페이지의 많은 슬롯에 대해 생각해 보십시오. 전체 힙 페이지가 한 번에 해제되는 것은 불가능합니다. 가비지 컬렉션 프로세스가 메모리를 해제하더라도 메모리 블록이 메모리를 부분적으로 차지하기 때문에 메모리 할당자에서 재사용할 수 없습니다.
메모리 할당자 수준에서 조각화
메모리 할당자 자체도 비슷한 문제에 직면해 있습니다. OS 힙이 완전히 해제되면 해제해야 합니다. 그러나 가비지 수집 프로세스의 임의적 특성을 고려할 때 전체 OS 힙을 한 번에 해제할 수 있는 가능성은 거의 없습니다.
메모리 할당자는 또한 애플리케이션 사용을 위해 시스템 메모리에서 OS 힙을 프로비저닝합니다. 기존 힙에 애플리케이션의 메모리 요구 사항을 충족하기에 충분한 여유 메모리가 있더라도 새 OS 힙을 프로비저닝하기 위해 이동합니다. 이것은 애플리케이션의 메모리 메트릭 급증에 대한 완벽한 방법입니다.
느린 릴리스
Ruby에서 메모리 팽창의 또 다른 중요한 원인은 해제된 메모리가 시스템으로 느리게 릴리스되기 때문입니다. 이 상황에서 메모리는 새 메모리 블록이 개체에 할당되는 속도보다 훨씬 느리게 해제됩니다. 이것은 해결해야 할 기존의 문제나 초보적인 문제는 아니지만 메모리 팽창에 큰 영향을 미칩니다. 조각화보다 훨씬 더 그렇습니다!
메모리 할당자의 소스를 조사한 결과 할당자는 OS 힙의 끝에서 OS 페이지를 릴리스하도록 설계되었으며 심지어 아주 가끔만 발생했습니다. 이는 아마도 성능상의 이유일 수 있지만 역효과를 일으키고 비생산적일 수 있습니다.
루비 메모리 팽창을 수정하는 방법
이제 Ruby의 메모리가 팽창하는 원인을 알았으므로 조각 모음 및 트리밍을 통해 이러한 문제를 해결하고 앱 성능을 개선하는 방법을 살펴보겠습니다.
조각 모음으로 Ruby 메모리 팽창 수정
조각화는 가비지 수집의 설계로 인해 발생하며 이를 해결하기 위해 할 수 있는 일은 많지 않습니다. 그러나 조각난 메모리 디스크로 끝날 가능성을 줄이기 위해 따를 수 있는 몇 가지 단계가 있습니다.
- 상당한 양의 메모리를 사용하는 개체에 대한 참조를 선언하는 경우 작업이 완료되면 수동으로 해제해야 합니다.
- 하나의 큰 블록에서 모든 정적 개체 할당을 선언하십시오. 이렇게 하면 모든 영구 클래스, 개체 및 기타 데이터가 동일한 힙 페이지에 저장됩니다. 나중에 동적 할당을 가지고 놀 때 정적 힙 페이지에 대해 걱정할 필요가 없습니다.
- 가능하면 초기에서 대규모 동적 할당을 시도합니다. 당신의 코드의. 이렇게 하면 더 큰 정적 할당 메모리 블록에 가깝게 배치되고 나머지 메모리를 깨끗하게 유지합니다.
- 작고 거의 지워지지 않는 캐시를 사용하는 경우 처음에 영구 정적 할당으로 그룹화하는 것이 좋습니다. 앱의 메모리 관리를 개선하기 위해 완전히 제거하는 것을 고려할 수도 있습니다.
- 표준 glibc 메모리 할당자 대신 jemalloc을 사용합니다. 이 작은 조정으로 Ruby 메모리 소비를 최대 4배까지 줄일 수 있습니다. 여기서 주의할 점은 모든 환경에서 호환되지 않을 수 있으므로 프로덕션에 적용하기 전에 앱을 철저히 테스트해야 한다는 것입니다.
루비 메모리 팽창을 수정하기 위한 트리밍
느린 메모리 해제를 수정하려면 가비지 수집 프로세스를 재정의하고 메모리를 더 자주 해제해야 합니다. malloc_trim이라는 API가 있습니다. . 가비지 컬렉션 프로세스 중에 이 함수를 호출하도록 Ruby를 수정하기만 하면 됩니다.
다음은 malloc_trim을 호출하는 수정된 Ruby 2.6 코드입니다. gc.c 함수 gc_start
에서 :
gc_prof_timer_start(objspace);
{
gc_marks(objspace, do_full_mark);
// BEGIN MODIFICATION
if (do_full_mark)
{
malloc_trim(0);
}
// END MODIFICATION
}
gc_prof_timer_stop(objspace);
참고: 이는 앱을 불안정하게 만들 수 있으므로 프로덕션 애플리케이션에서는 권장하지 않습니다. 그러나 느린 메모리 해제로 인해 성능이 크게 저하되고 모든 솔루션을 시도할 준비가 된 경우에 유용합니다.
마무리 및 다음 단계
메모리 팽창은 식별하기 어렵고 수정하기가 훨씬 더 어렵습니다.
이 기사에서는 Ruby 앱의 메모리 팽창에 대한 두 가지 중요한 이유(조각화 및 느린 릴리스)와 조각 모음 및 트리밍이라는 두 가지 수정 사항을 살펴보았습니다.
임박한 팽창 사고를 식별하고 앱이 다운되기 전에 수정하려면 앱의 측정항목을 지속적으로 주시해야 합니다.
Ruby 애플리케이션에서 메모리 팽창 문제를 해결하는 데 도움이 되었기를 바랍니다.
추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!
우리 게스트 작가 Kumar Harsh는 공예에 의해 떠오르는 떠오르는 소프트웨어 개발자입니다. 그는 Ruby 및 JavaScript와 같은 인기 있는 웹 기술을 중심으로 콘텐츠를 모으는 열정적인 작가입니다. 그의 웹사이트를 통해 그에 대해 자세히 알아보고 Twitter에서 그를 팔로우할 수 있습니다.