TrotVote
[IT 심층 분석]

리눅스 커널 OOM Killer의 억울한 오작동과 페이지 캐시 단편화(Fragmentation) 해결 경험

김민준 · IT 시스템 엔지니어|
새벽 3시 PagerDuty 경고음이 울리고, 대용량 트래픽을 처리하는 프론트엔드 캐시 서버 3대가 동시에 셧다운됐다는 알람을 받았습니다. 급하게 서버에 SSH로 접속해 /var/log/messages를 뒤져보니, 황당하게도 가용 메모리가 충분히 남았음에도 불구하고 커널의 OOM (Out of Memory) Killer가 최우선 프로세스인 Nginx를 무자비하게 학살해버린 기록이 남아있었습니다. "메모리가 40GB나 남아있는데 대체 왜?"라는 근본적인 의문에 사로잡혀 저는 뜬눈으로 밤을 새우며 메모리 덤프와 커널 트레이싱 영역으로 빠져들기 시작했습니다. 이 경험은 제게 리눅스 메모리 관리 시스템이 생각보다 완벽하지만은 않다는 것을 일깨워준 소중한 기점이 되었습니다. 문제의 핵심은 단순 점유량이 아니라, 커널 내부의 물리 메모리 단편화(Memory Fragmentation)에 있었습니다. 캐시 서버 특성상 수시로 디스크 I/O를 수행하며 페이지 캐시(Page Cache)를 생성하고 소멸시키는데, 이로 인해 물리 메모리가 거대한 스위스 치즈처럼 구멍이 숭숭 뚫려버린 상태였습니다. 이때 특정 커널 드라이버가 연속된 고차원(High-order) 물리 메모리 블록을 갑자기 요청하자, 여유 공간의 총합은 넉넉하지만 '연속된 공간'이 부족했던 커널은 패닉 상태에 빠져 당장 가장 덩치가 큰 프로세스를 죽이는 것으로 자원을 확보하려 들었던 것입니다. eBPF(Extended Berkeley Packet Filter) 스크립트를 직접 짜서 메모리 할당 요청 실패 순간을 캡처해보니 제 가설이 정확히 들어맞았습니다. 당시의 그 소름 돋는 카타르시스는 말로 다 할 수 없습니다. 이 어처구니없는 죽음을 막기 위해 저는 커널 파라미터(sysctl) 튜닝이라는 칼을 빼들었습니다. vm.min_free_kbytes 값을 대폭 상향하여 커널이 긴급하게 사용할 수 있는 최소한의 연속 메모리 풀을 미리 방벽처럼 확보해두고, 메모리 압축(vm.extfrag_threshold) 임계치를 조정하여 메모리가 단편화되는 즉시 커널 스레드(kcompactd)가 백그라운드에서 메모리를 조각모음 하도록 성향을 매우 공격적으로 튜닝했습니다. 또한 OOM 스코어 조정을 통해 향후 유사한 상황이 발생하더라도 코어 프로세스만큼은 학살 대상에서 제외되도록 방어막을 씌웠습니다. 이 튜닝을 적용한 후 지난 1년 반 동안 해당 서버군에서는 단 한 번의 OOM 크래시도 발생하지 않았습니다. 기술의 본질은 단순히 코드를 짜는 것이 아니라, 보이지 않는 운영체제의 호흡을 내 의도대로 통제하는 것임을 배웠습니다.