https://b.luavis.kr/server/missing-800mb

메쉬코리아 부릉에서는 라이더분들이 배달을 실행하기 위해서 배달 요청을 잡는 일을 AI가 가장 효율적인 배차가 가능하도록 자동으로 할당해주는 시스템인 “추천배차”라는 시스템이 있습니다. 이 추천배차 시스템에서 사용하는 메모리량이 꾸준히 증가하고 있는 이슈가 생겨서 NewRelic과 sysfs를 이용해서 원인을 파악해보고 누수가 생긴 이유까지 찾은 이야기를 글로 적은 내용입니다.

문제를 찾아가는 과정

[꾸준히 상승하는 메모리 사용량]

7일만에 메모리가 약 3배 이상으로(대략 600M → 1.4G) 올라가면 문제가 있다고 판단했고 해당 문제의 원인을 파악해보기 위해서 소스코드를 확인해봤지만 별다른 이상한 점은 발견하기 힘들었습니다. 그렇다면 자바 Heap 영역에서 문제/누수가 발생하고 있는것은 아닌지 확인해볼 필요가 있어서 APM에서 나오는 Heap 메모리의 사용량도 확인해보았고, Thread stack 메모리가 사용하는 메모리는 아닐까 의심되어 확인해봤지만 Thread 수에도 큰 문제가 발생하지 않는것을 확인할 수 있었습니다.

[평화로운 JAVA heap memory]

문제가 java에 있지 않다면 NewRelic의 수집 방식에 문제가 있거나 혹은 linux 시스템적으로 사용하는 메모리가 증가하고 있을거라 생각이 되어서 k9s를 이용해서 kubernetes가 판단하고 있는 현재 메모리 사용률에 대해서 확인해봤습니다. 확인해본 결과 kubernetes에서 사용하는 판단하는 메모리 사용량은 635MB 정도로 자바가 차지하는 Heap memory를 고려할때 이상이 없는 수치라고 생각이 됩니다. 그러면 문제는 없어진 800M의 메모리는 어디로 간 것인가 입니다. 이 문제를 파악해보기 위해 많은 리서치를 해봤고 하나 알아낸것은 컨테이너를 매니징하는 프로그램(docker와 같은)과 newrelic이 생각하는 메모리와 kubernetes가 생각하는 메모리가 다르다는 것을 확인할 수 있었습니다.

리눅스는 머신이 갖고 있는 메모리를 swap, 캐시, 버퍼 캐시 등등 다양한 방법으로 최대한 많은 메모리를 사용하여 머신의 성능을 끌어올립니다. 따라서 한 process가 memory를 사용한다는 개념이 어떤 부분(RSS, cache, …)들을 사용량으로 볼 것인지 조금씩 다르다는것을 확인할 수 있었습니다.

docker와 NewRelic에서는 RSS와 그외 buffer / cache등 모든 메모리 사용의 총합을 고려해서 판단합니다. 하지만 이와 반대로 k8s에서는 memory 사용량을 kubelet으로 확인하면 물리적 메모리 사용량(RSS)를 가져가는것으로 확인했습니다. 이에 대한 discussion이 이 글에 자세히 나와 있습니다.

The value K8sContainerSample.memoryUsedBytes will pull the usageBytes metric from the /stats/summary endpoint of the kubelet, scoped to the container. As you noted that value included the linux buffers and cache.

따라서 위의 NewRelic 대시보드에 메모리 사용도 application에서 사용하는 실제 메모리가 아닌 시스템 레벨에서 사용되는 메모리에 문제가 있을 것이라 추측할 수 있었습니다. 따라서 추천배차 시스템에서 RSS의 사용량 추세가 수평적인지 선형적인지를 판단해보면 cache외의 메모리또한 증가세인지 확인할 수 있습니다. 이를 위해서는 NewRelic의 Insights를 통해서 NRQL을 사용해 확인해볼 수 있었습니다.

[평화로운 memoryWorkingSetBytes]

NRQL> SELECT average(memoryWorkingSetBytes / 1000000) AS ‘MB of Mem’ FROM K8sContainerSample FACET clusterName, podName, containerName TIMESERIES LIMIT 50 Since 1 week ago UNTIL 1595409275180 WHERE containerName IN (‘***’)

NewRelic에서는 RSS를 memoryWorkingSetBytes라는 이름으로 수집하고 있고 이 메모리 사용량만 확인해보면 기간 대비 평탄한 사용량을 갖고 있음을 확인할 수 있었습니다. 그러면 나머지 800M의 메모리는 어떻게 누가 먹고 있는가를 확인해보기 위해서 찾아봤습니다.

실제 컨테이너에 접속하여 확인해봤는데 ps, free 와 같은 커맨드를 사용할 수도 설치할 수도 없어서 확인이 어려웠지만, 이 프로그램들은 procfs를 읽어서 사용하기 때문에 컨테이너의 정보가 아닌 실제 물리머신 node의 성능이 나옵니다. 일례로 container에 접속하여 /proc/meminfo 를 확인하면 약 64기가, 물리 머신의 성능이 나오는 것을 확인할 수 있습니다.

...
MemTotal:      		65150604 kB
MemFree:       		41145196 kB
MemAvailable:       49117724 kB
Buffers:       		    2088 kB
Cached:       		 7098816 kB
...