[Ruby on Rails] Memory Bloat

inho ha·2024년 5월 23일
1

swatchon

목록 보기
1/4

서울의 한 월요일.
내가 개발한 기능이 비동기 서버를 다운시켰다. (다행히 development 서버에서...)
이를 해결하는 과정에서 공부한 Ruby 에서 발생하는 memory bloat 와 해결 방법에 대해 공유해 봅니다.
누군가는 또 비슷한 일을 겪고 이 글을 본다면 도움이 되길 바라며...

Memory Bloat

Memory bloat refers to a situation where a program consumes more memory than is necessary to execute its intended tasks. It typically occurs due to poor memory optimization, excessive data caching, or the accumulation of redundant objects. As a result, the program’s memory usage increases, leading to performance degradation and potential slowdowns. Memory bloat is usually a gradual and incremental process, and its effects may become noticeable over time.

메모리 팽창은 프로그램이 의도한 작업을 실행하는 데 필요한 것보다 더 많은 메모리를 사용하는 상황을 말합니다.
일반적으로 메모리 최적화 불량, 과도한 데이터 캐싱 또는 중복 객체의 축적으로 인해 발생합니다.
결과적으로 프로그램의 메모리 사용량이 증가하여 성능 저하 및 잠재적인 속도 저하로 이어집니다.
메모리 팽창은 일반적으로 점진적이고 점진적으로 진행되며 시간이 지남에 따라 그 영향이 눈에 띄게 나타날 수 있습니다.

Ruby 에서 메모리 할당 방법

ruby 는 malloc arena 를 사용하여 쓰레드에 메모리를 할당해 줍니다.
malloc arena는 여러 개가 될 수 있는데요.

메모리를 할당해 주는 malloc arena가 많으면

  • 쓰레드가 동시에 메모리 할당 요청을 해도 여러 malloc arena가 빠르게 할당해 줄 수 있습니다.
  • 대신 malloc arena는 각각 독립적인 메모리 공간을 관리하기 때문에 전체적인 메모리 사용량이 많아질 수 있습니다.

Ruby에서 Memory Bloat 발생 과정

  1. 한 malloc arena가 쓰레드에 메모리를 할당해 줍니다.
  2. 한 malloc arena가 관리하는 메모리를 모두 할당해 줘서 메모리가 부족합니다.
  3. 한 malloc arena는 os로부터 메모리를 더 받아서 쓰레드에 메모리를 할당해 줍니다.
  4. 쓰레드의 작업이 끝나 malloc arena로 메모리를 반환했지만, malloc arena는 os에 메모리를 반환하지 않습니다.
  5. 다른 쓰레드가 다른 malloc arena에 메모리 할당 요청을 합니다.
  6. 다른 malloc arena가 관리하는 메모리를 모두 할당해 줘서 메모리가 부족합니다.
  7. 다른 malloc arena는 os로부터 메모리를 더 받아서 쓰레드에 메모리를 할당해 줍니다.
  8. 쓰레드의 작업이 끝나 malloc arena로 메모리를 반환했지만, malloc arena는 os에 메모리를 반환하지 않습니다.
  9. 또 다른 쓰레드가 또 다른 malloc arena에 메모리 할당 요청을...
  10. 각각의 malloc arena가 관리하는 메모리가 많아지면서 서버의 전체적인 메모리 사용량이 늘어납니다.
  11. 제 경우에는 쿠버네티스에서 설정한 서버 메모리 limit 을 초과하게 되어 out of memory killed 로 서버가 다운되었습니다.(정확히는 재실행됨)

Malloc Arena

malloc arena는 os로부터 가져온 메모리를 웬만하면 os로 반환하지 않습니다.
대신에 잘 가지고 있다가 쓰레드로부터 할당 요청이 오면 할당해 주죠

쓰레드가 malloc arena로 메모리를 반환할 때마다
malloc arena도 os로 메모리를 반환한다면 그때마다 system call 을 사용하는 비싼 비용이 발생하게 됩니다.
이후 또 쓰레드가 메모리 할당 요청을 하고 메모리가 부족하다면 다시 os로부터 할당받아야 하는 문제가 발생하기도 하겠죠?
이 때문에 os로 메모리를 잘 반환하지 않습니다.

해결 방법

  1. malloc arena max 값 줄이기
  2. jemalloc 사용
  3. 쓰레드가 메모리 적당히 쓰게하기...

Malloc Arena Max

Ruby 에는 Malloc Arena Max 값을 설정할 수 있습니다.
Malloc Arena의 수가 적다면 독립적으로 메모리를 관리하는 수가 줄어서 전체적인 메모리 사용량을 줄 수 있습니다.
그러나 멀티쓰레드들이 메모리 할당을 위해 경합이 발생하여 처리 시간이 늘어날 수 있다는 trade off 가 있습니다.

https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html

Jemalloc 사용

Ruby의 기본 malloc 구현체는 glibc의 ptmalloc 입니다.
ptmalloc 은 메모리를 os에 잘 반환하지 않습니다.

다른 malloc 구현체로 jemalloc 가 있습니다.
jemalloc 은 메모리 반환에 보다 적극적이라고 하는데요.

https://medium.com/motive-eng/we-solved-our-rails-memory-leaks-with-jemalloc-5c3711326456

다만 malloc 구현체를 jemalloc 으로 변경 시에 기존의 로직에 영향이 갈 수 있으니 충분한 테스트가 동반되어야 할듯합니다.

쓰레드가 메모리 적당히 쓰게하기...

저의 경우에는 대용량 데이터에 대해 처리하는 기능이었습니다.
메모리를 500메가 정도 차지했고 동시에 여러 워커가 이 작업을 수행 시에 사용하는 메모리의 총합은 1.8기가까지 찍고 서버 따운! 되었습니다. (쿠버네티스에서 설정된 메모리 limit은 1기가)

저는 이 작업을 batch로 처리하여 메모리 사용량을 30메가 수준으로 줄였습니다.
active record로 db 데이터를 읽어올 때 find_in_batches 를 사용했습니다.

물론 find_in_batches 를 쓰는 것만으로 해결된 것은 아니고요
작업이 끝난 active record 인스턴스를 gc에서 수집해가지 않아서
gc.start(); gc.compact(); 를 직접 호출해 줬습니다.

gc가 실행되는 동안에 애플리케이션의 다른 작업들이 중단되기 때문에 매우 비싼 작업입니다.
다만 해당 기능이 사용되는 빈도가 매우 낮은 점, malloc_arena_max 나 jemallc으로 변경은 기존의 다른 로직에 전체적인 영향을 줄 수 있는 점을 고려하여 batch + gc 실행 방법으로 해결하였습니다.

thanks to

위 작업은 스와치온에 입사하고 처음으로 받은 테스크를 해결하는 과정에서 겪은 것입니다.
덕분에 ruby 메모리에 대하여 깊게 이해해 볼 수 있었습니다.

원단은? 스와치온!
https://swatchon.com

profile
inho ha / ian(swatchon) / iha(42seoul)

0개의 댓글