OOM Killer 동작 방식
OOM Killer는 리눅스 커널 메모리를 더 이상 확보할 수 없을 때 최후 수단으로 프로세스를 강제 종료하는 메커니즘이다.
OOM Killer 발생 조건 #
- 물리 메모리 부족
- swap도 부족하거나 사용이 제한
- 메모리 회수(reclaim) 실패
- page cache drop
- slab shrink
- memory compaction
- 더 이상 커널이 할당 실패를 호출자에게 반환할 수 없는 상황
이 시점에서 커널은 이 시스템을 살리려면 누군가는 죽어야된다는 판단을 하게된다.
전체 흐름 #
1단계로 할당 실패. 프로세스가 malloc() -> 커널 내부 __alloc_pages()에서 실패
2단계로 메모리 회수 시도로 다음을 순차적 시도한다.
- page cache 회수
- slab cache shrink
- memory compaction
- swap out (가능한 경우)
여기서 모두 실패하면 oom 상태 진입
그리고 상황 판단을 3단계로 진행한다.
커널함수 out_of_memory() 여기서 oom killer 활성화 여부, 메모리 cgroup여부랄 판단한다.
그 이후 어떤 프로세스를 죽일지 결정하는데
OOM Score라는 개념이 있다. 커널은 모든 프로세스에 대해 oom score를 계산하는데
oom_score = f(메모리 사용량, 중요도, 보호 여부)
점수가 높을수록 죽을 확률이 높다.
메모리 사용량 RSS(실제 물리 메모리 사용), page cache 포함 여부등이 주요 평가 요소중 하나로 메모리 많이 쓰는애가 우선적으로 죽는다.
nice 값이 높아도 죽기 쉽다 (cpu 우선순위를 간접적으로 조절하는 숫자)
nice값: 범위 -20~+19로 값이 작을수록 우선순위가 높다. nice가 높다는 것은 다른프로세스에게 cpu를 잘양보하는 착한(nice) 프로세스라는 뜻으로 Nice란다...ㅎ
oom_score_adj 범위 -1000~+1000
- -1000: 커널 스레드급 절대 안죽음
- 0: 기본값
- +1000: 가장 먼저 죽음
이 값을 확인하기도 한다.
특수 보호 대상들도 있는데 root process, systemd, init, kernel thread, oom_score_adg=-1000인 것들은 죽이면 안되니 oom killer 대상에서 제외된다.
실제 kill 동작 방식 #
타겟이 결정되면 커널은 아래를 수행한다.
- SIGKILL 전송
- 프로세스 즉시 종료
- 메모리 해제
- 시스템 정상 상태 복귀 시도
Out of memory: Kill process 12345 (java) score 987 or sacrifice child
Killed process 12345 (java) total-vm:4096000kB, anon-rss:2048000kB
cgroup 기반 OOM (container 환경) #
k8s/docker에서의 차이점은 컨테이너는 cgroup 단위로 oom이 발생한다.
- node 전체 oom -> system killer
- container memory exceed -> cgroup oom killer
차이점으로는 컨테이너 내부 프로세스만 종료하고 노드는 살아있다.
OOMKilled: true
ExitCode: 137
oom killer는 버그가 아니며 메모리 누수의 결과일수는 있으나 정상 동작이라고 볼수있다.
oom killer는 협상따위 하지않는다. 즉 SIGTERM없고 SIGKILL 바로 갈기며 finally, shotdown hook등이 실행이 안된다. (커널을 살리기위한 발악이기 때문에)
즉 정리하면 OOM Killer는 메모리 할당이 완전히 불가능해진 순간 시스템을 살리기위해 가장 메모리를 많이쓰거나 덜 중요한 프로세스를 즉시 제거하는최후수단이다.