CS Insights

멀티스레드 환경의 메모리 할당 병목과 tcmalloc/jemalloc 스레드 캐시 아키텍처 비교

멀티스레드 환경의 메모리 할당 병목과 tcmalloc/jemalloc 스레드 캐시 아키텍처 비교
최근 사내 자체 C++ 기반 고성능 트레이딩 서버를 튜닝하던 중, 이상하게 코어 수를 늘릴수록 처리량이 떨어지는 기현상을 겪었습니다. 일주일간의 프로파일링 끝에 범인을 잡고 보니 어이없게도 기본 메모리 할당 함수였습니다. 멀티스레드 프로그래밍에서 동적 메모리 할당(malloc)은 필연적으로 힙(Heap) 영역을 조작해야 하므로 커널 스페이스와 유저 스페이스 간의 잦은 문맥 교환을 동반합니다. 전통적인 glibc의 ptmalloc 구조 방식은 전역 메모리 아레나(Arena) 영역에 대해 Mutex 락(Lock)을 걸게 되는데, 16개의 스레드가 동시에 객체 생성을 요청하게 되면 하나의 스레드만 할당을 받고 나머지 15개는 모두 대기 큐에서 블로킹되는 끔찍한 잠금 경합(Lock Contention)이 일어납니다. 스레드가 늘어날수록 CPU 가동률은 100%를 찍지만, 정작 코드는 실행되지 않고 서로 락만 쳐다보고 있는 교착 상태와 다름없는 스로틀링을 겪게 됩니다. 이 병목을 아키텍처적으로 파괴하기 위해 탄생한 것이 구글의 tcmalloc(Thread-Caching Malloc)과 페이스북의 jemalloc입니다. 이들의 핵심 사상은 "스레드마다 자기만의 공구함(Thread Local Cache)을 주자"는 것입니다. 메인 힙까지 내려갈 필요 없이, 각 스레드는 자신이 독점하는 아주 작고 락이 없는 전용 메모리 풀(Pool)에서 작은 사이즈의 객체들을 즉시 떼어갑니다. 이 전용 깡통이 바닥날 때에만 전역 힙 관리자에게 거대한 뭉텅이를 한 번에 요청하여 채워 넣습니다. 실제 저희 프로덕션 서버의 링크 태그에 LD_PRELOAD 기법을 이용해 기본 할당자를 jemalloc으로 교체하는 단 한 줄의 환경 설정만으로, 거짓말처럼 응답 지연 시간이 40% 이상 단축되고 스루풋 병목이 완벽히 해결되는 경험을 했습니다. 다중 코어 시스템 시대에 시스템 기본 라이브러리의 맹신이 얼마나 위험한 병목 설계로 이어질 수 있는지 뼈저리게 느낀 순간이었습니다.

Related Posts