Linux Internals
오늘은 실제 서버 운영 중 있었던 slab 메모리 누수에 대해 이야기해 보려고 합니다. 전부터 써야지 써야지 했던 내용인데, 시간이 없어서 못쓰고 있었네요 ^^;; 그럼 시작합니다.
어느 날 서버에서 메모리 사용률이 90%가량 된다는 알럿 메시지를 받았습니다. 딱히 메모리를 많이 사용할 이슈가 없는 서버인데, 뭔가 이상하다는 생각이 들었습니다. 그리고 아래는 해당 서버의 메모리 그래프입니다. 우측에 메모리 사용률이 내려간 건 조치를 한 후였고요, 그래프를 전체적으로 살펴보면 매우 빠른 시간 동안에 메모리 사용률이 선형적으로 증가했다는 것을 알 수 있습니다.
무슨 조치를 했을까요?
서버에 접속하자마자 free 명령으로 메모리의 사용률을 확인했습니다. (free 명령에 대한 캡처 내용은 없네요..ㅠㅠ) 실제로도 메모리의 사용률이 90%를 육박하는 상태였습니다. 오탐이 아님을 확인한 후, /proc/meminfo를 살펴봤습니다. 그런데, 이상한 수치를 확인할 수 있었습니다. slab 영역이 비정상적으로 상당한 크기를 차지하고 있었습니다. 역시 캡처한 내용은 없지만 7GB 정도의 크기를 차지하고 있었습니다. 그중에서도 dentry cache가 6GB 정도 차지하고 있었습니다. 아무리 I/O 가 많다고 해도 사실 저 정도의 크기는 비정상적인 크기입니다. 게다가 이 서버는 그렇게 I/O가 많은 서버가 아니었습니다. 우선 서버의 메모리를 확보해야 했기 때문에 아래 명령을 통해서 slab 메모리 영역을 비웠습니다.
echo 2 > /proc/sys/vm/drop_caches
하지만 시간이 지나면 다시 차오르는 현상이 반복되었습니다. 뭔가 문제가 남아 있는 상태였습니다.
해당 서버는 jenkins가 돌아가는 서버였습니다. 다른 프로세스는 없었고 jenkins를 통해서 CI 등의 작업을 하는 서버였습니다. 그래서 jenkins 가 어떤 작업을 하는지 어디서 문제가 발생할 수 있을지를 찾아야 했습니다. 이를 하기 위해서는 여러 가지 방법이 있겠지만 strace 만한 게 없었습니다. strace로 시스템 콜 덤프를 확인하던 중 인상적인 부분을 발견했습니다.
29911 10:24:00 execve("/usr/bin/curl", ["curl", "-X", "GET", "--header", "Accept: application/json", "https://XXXXX"], [/* 70 vars */] <unfinished ...>
그리고 그 이후로 아래와 같은 시스템 콜이 잡혔습니다.
29911 10:24:00 access("/home/XXX/.pki/nssdb/.205485873_dOeSnotExist_.db", F_OK) = -1 ENOENT (No such file or directory)
29911 10:24:00 access("/home/XXX/.pki/nssdb/.205485874_dOeSnotExist_.db", F_OK) = -1 ENOENT (No such file or directory)
29911 10:24:00 access("/home/XXX/.pki/nssdb/.205485875_dOeSnotExist_.db", F_OK) = -1 ENOENT (No such file or directory)
29911 10:24:00 access("/home/XXX/.pki/nssdb/.205485876_dOeSnotExist_.db", F_OK) = -1 ENOENT (No such file or directory)
29911 10:24:00 access("/home/XXX/.pki/nssdb/.205485877_dOeSnotExist_.db", F_OK) = -1 ENOENT (No such file or directory)
29911 10:24:00 access("/home/XXX/.pki/nssdb/.205485878_dOeSnotExist_.db", F_OK) = -1 ENOENT (No such file or directory)
slab 메모리 중 dentry cache의 경우는 access() 시스템 콜을 통해 생성되기 때문에 위와 같이 동작하는 부분이 dentry cache를 증가시키는 것임을 확인할 수 있었습니다. 엄청난 양의 access() 시스템 콜이 불려졌고 이 순간 dentry cache 역시 증가 했습니다.
그래서 구글에서 slab 메모리 릭을 검색해 보면 결국 아래와 같은 버그 질라 페이지를 만날 수 있습니다. (https://bugzilla.redhat.com/show_bug.cgi?id=1044666) 그리고 해당 이슈가 이번 상황과 정확히 일치합니다.
그래서 2014년 경에 nss-softokn 라이브러리는 패치가 되었습니다. (https://rhn.redhat.com/errata/RHBA-2014-1378.html) sdb_init() 함수 내부에서 NSS_SDB_USE_CACHE 환경 변수가 설정되어 있으면 sdb_measureAccess() 함수를 호출하지 않도록 되었습니다.
하지만 주의해야 할 점은, NSS_SDB_USE_CACHE 환경 변수가 없으면 여전히 sdb_measureAccess() 함수를 호출하기 때문에 반드시 해당 환경 변수를 설정해야 한다는 것입니다. 그래서 nss-softokn 라이브러리 업데이트 만으로는 효과가 없습니다. /etc/profile 등과 같이 서버 전역으로 설정될 수 있는 환경 변수로 설정해 주어야 합니다. 게다가 jenkins의 경우는 execve()를 이용하기 때문에 jenkins 내부의 환경 변수로도 설정해 주어야 합니다. execve() 시스템 콜은 세번째 인자를 통해서 환경 변수를 받게 되는데 이 때 전달 받은 환경 변수가 시스템 전역 설정을 덮어 쓰기 때문에 시스템 전역 변수로 설정해 주었다고 해도 효과를 받지 못합니다.
int execve(const char *filename, char *const argv[], char *const envp[]);
그래서 jenkins와 같이 execve()를 사용하는 경우는 내부 환경 변수로도 지정해 주어야 합니다. (이 방법은 애플리케이션 별로 다르기 때문에 잘 확인해 봐야 합니다.)
이번 글을 통해서 slab 메모리 누수에 관해 살펴봤습니다. 정리하자면 아래와 같습니다.
1. libcurl + nss_softokn 라이브러리의 조합에서 발생하는 건이며 curl을 통해서 https 주소를 호출할 때 발생하는 이슈입니다.
2. 운영 중인 서버에서 slab 메모리가 선형적으로 증가하는 패턴을 보인다면 반드시 nss-softokn 라이브러리 이슈를 확인해야 하며 NSS_SDB_USE_CACHE 환경 변수를 설정 함으로써 메모리 누수를 피할 수 있습니다.
또한 strace를 통해서 추적하는 과정도 함께 포함시켰는데요, strace는 엔지니어의 관점에서 애플리케이션의 동작 원리를 파악할 수 있게 해주는 툴이기 때문에 반드시 익혀두어야 할 툴 중에 하나입니다.
긴 글 읽어 주셔서 감사합니다. (__)