What happens when you run out of memory in Linux?의 원저자에게 허락을 맡고 번역한 글입니다
자연스러운 전개를 위해 번역 중 약간의 의역이 들어갔습니다
consol output의 값들이 밀려 보일 수 있으므로 큰 화면으로 보시는 것을 추천합니다
난 항상 Linux에서 메모리를 다 써버렸을 때 어떤 일이 발생하는지에 대해서 궁금해했었다. 그래서 최근에 이것을 알아내기 위한 실험을 진행했다.
먼저 AWS의 EC2에 DHBox를 배포하려고 한다. DHBox는 여러 가지 데이터분석 툴을 쉽게 사용할 수 있는 가상 환경을 만드는 것을 도와주는 프로그램이다. 이 가상 환경은 Docker 컨테이너 위에서 돌아간다. 이 때 이 컨테이너가 도는데 많은 메모리가 사용되기 때문에 하나의 EC2에 너무 많은 컨테이너를 올릴 수 없다. 여기까지가 우리가 알고 있는 부분이다. 하지만 이 과정이 실제로 어떻게 일어나는지 살펴보자.
시작하기 전에, 여기에 느려진 Linux 박스를 디버깅하는데 쓰이는 툴들을 소개한 Netflix의 훌륭한 글들이 있다.
나는 1GB의 메모리를 탑재한 Ubuntu EC2 이미지에 DHBox를 배포했다. 이 때 swap file(가상 메모리)은 사용하지 않도록 했다. 왜일까? swap file을 사용하는 것은 많은 EBS IO를 발생시킬 수도 있기 때문이다. 이건 높은 AWS 요금을 초래한다. 하지만 swap file을 사용하는 것도 흥미로운 디버깅 시나리오가 될 수 있을 것 같다. 아무튼 계속 해보자.
이것이 시작하는 시점인 지금의 메모리 상황이다:
“buff/cache” 부분에 392MB가 잡혀있는 것을 볼 수 있다. 이게 대체 뭘까? 여기에 이것에 대한 좋은 설명이 있다. buffer cache는 디스크에 들어 있는 캐싱 데이터다(RAM). 예를 들어서 ls
명령어라던지 glibc 라이브러리가 이곳에 캐싱된다고 할 수 있다.
제일 처음 한 일은 DHBox를 위한 Python 웹 앱을 시작하는 것이었다. 웹 앱은 gunicorn에서 실행되는 간단한 Flask 앱이다. 이것을 실행하고 난 후 다시 free
명령어를 통해 메모리를 조회해봤다:
예상하던 대로다. 우리는 아까보다 조금 더 많은 메모리를 쓰고 있다. 디스크로부터 약간 더 많은 데이터를 캐싱했다.
이제 vmstat을 실행한 다음 네 개의 가상 컨테이너를 차례대로 돌려볼 것이다. 가상 컨테이너가 모두 실행되면 대략 1GB의 메모리를 사용하게 되므로 머신에 어느 정도의 부하를 줄 수 있을 것 같다. vmstat
은 아주 유용한 정보들을 찍어주는 훌륭한 툴이다. 그럼 Docker 컨테이너를 실행하기 전에 vmstat이 찍어주는 정보들을 확인해보자:
여기서 중요한 부분은 바로 id
열이다. id 값은 CPU가 유휴 또는 대기 상태인 시간의 백분율을 의미한다. 보다시피 지금 CPU는 할 일이 없어 꽤나 편해 보인다.
그러면 어서 첫번째 Docker 컨테이너를 띄워보자. 몇 개의 값들이 잠시 치솟아 오르기 시작한다. vmstat를 통해 그 값을 살펴보면:
재미있게도, 보다시피 id
의 값이 감소했다. 덜 한가해진 것이다. 하지만 잠시 후 Docker 컨테이너는 실행되고 상황은 다시 잠잠해진다.
여기서 free
명령어로 메모리 상태를 다시 체크하자:
메모리의 사용을 나타내는 used
가 127MB에서 350MB로 상승했다. 그리고 buffer cache 또한 같이 늘어났다. 사용할 수 있는 메모리가 적어진 것이다.
두번째 Docker 컨테이너를 실행시키고 vmstat를 다시 확인하자.
비슷한 상황이 일어났다. io가 치솟았고, id가 감소했다. 그리고 다시 잠잠해진다. 메모리 상태를 보자:
메모리가 더 많이 사용되고 있다. 하지만 여기서 buff/cache
값이 낮아진 것을 볼 수 있다. 왤까? 실행되고 있는 프로세스들이 더 많은 메모리를 사용하면서 부족한 메모리를 어딘가로부터 가져와야했기 때문일 것이다. Linux의 커널이 buffer cache로부터 메모리를 가져와 그것을 프로세스들에게 주었다. 역시나 예상하던 대로이다.
마지막으로 세번째 그리고 네번째(마지막) Docker 컨테이너를 시작한 다음, vmstat 결과를 다시 한 번 보자
바로 여기가 시스템 상황이 안좋아지는 구간이다! EC2 머신이 굉장히 비반응적으로 변화했다. ls
같은 아주 간단한 명령어를 실행하는 것도 수 초가 걸린다. free
를 통해 시스템 정보를 확인하고 분석을 이어서 해보자:
보다시피, 사용할 수 있는 메모리의 양이 거의 없어졌고 buffer cache 또한 더더욱 줄어들었다. 하지만 vmstat
에서 일어난 일을 보면 bi
(blocks received from a block device) 이 일정한 값에서 멈추었다는 것을 볼 수 있다. 이것은 지금 머신에서 충분히 많은 디스크 읽기가 발생하고 있다는 것을 의미한다.
왜 이러한 현상이 일어날까? 우리의 프로세스들이 직접 디스크 작업들을 하고 있는 것일까? 그렇다고 볼 수 없다. 여기서 프로세스들은 디스크 위에서 실행되고 있다.
Linux 커널이 이들을 실행할 때, 커널은 지시어들을 디스크로부터 읽어와야 한다. 만약 우리가 충분한 양의 buffer cache가 있었다면 이 지시어들은 buffer cache에 캐싱되어 반복하여 디스크로부터 읽어올 필요가 없었을 것이다. 그러나 지금 buffer cache의 양은 너무 작다. 그래서 프로세스들이 실행될 때, 커널은 그때 그때 지시어들을 디스크로부터 가져오고 있다. 아마 개별로 봤을 때 각각의 프로세스들은 지시어들을 buffer cache에 저장을 하기는 할 것이다. 하지만 다음의 프로세스가 이전의 프로세스가 저장해놓은 지시어를 치우고 그곳에 다시 자신의 지시어들을 저장해버릴 것이다.
여기서 끝난 것이 아니다. 이 모든 어지러운 일들이 일어날 때 Linux의 커널은 OOM Killer이라 불리는 무언가를 호출한다. 무엇이 일어나는지 보고 싶다면 dmesg
명령어롤 통해 운영체제의 로그를 불러와서 확인하면 된다:
여기서 이상한 점은 OOM Killer가 계속해서 Jupyter 프로세스들을 죽이고 있다는 것이다. 프로세스들이 죽고 나서 무언가가 그것들을 재실행하고 있다고 추측해볼 수 있다.
시스템이 이런 상황에 빠졌을 때 할 수 있는 최고의 액션은 간단하게 모든 실행중인 Docker 컨테이너들을 죽여서 EC2를 다시 사용 가능하게 만드는 것이다:
docker kill $(docker ps -q)
만약 디스크에 swap 파티션이 있었다면, 우리가 보는 것은 조금 달라졌을 것이다. 물론 그것 또한 직접 해볼 수 있다. 생각해보면 swap은 프로세스들에 의해서 사용되는 메모리의 일부를 디스크로 보내는 작업을 했을 것이다. 그건 충분히 효율적이라 볼 수 있는게, 메모리의 일부는 액세스하지 않는 경우가 거의 없기 때문이다. 그러나 swap 없이는 이러한 옵션을 선택할 수 없다.