자식 프로세스가 끝났는데 부모가 아직 종료 상태를 거두어 가지 않은 상태다. 커널은 부모가 wait() 계열 호출로 종료 코드를 가져갈 수 있도록 프로세스 테이블 항목만 남겨 둔다. 실행 중인 코드도, 할당된 메모리도 없다.
그래서 좀비 하나가 CPU 나 메모리를 먹는 일은 없다. 문제가 되는 것은 수가 계속 늘어날 때다. PID 공간과 프로세스 테이블은 유한하므로, 좀비가 쌓이면 새 프로세스를 못 만드는 상태에 이른다. 좀비가 늘고 있다는 것은 부모 프로그램이 자식을 거두지 않고 있다는 신호다.
ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/'
ps -eo stat | grep -c '^Z'
top 의 요약 줄에도 zombie 개수가 나온다. 부모를 찾으려면 PPID 를 본다.
ps -o ppid= -p <zombie-pid>
좀비 자체는 죽일 수 없다. 이미 죽은 프로세스이기 때문에 kill 이 닿지 않는다. 방법은 두 가지다.
SIGCHLD 를 보내면 핸들러를 제대로 구현한 프로그램은 밀린 자식을 거둔다.kill -CHLD <parent-pid>
init(PID 1) 에게 입양되고, init 이 즉시 거두어 사라진다. 다만 부모가 서비스 프로세스면 서비스가 함께 멈춘다. 영향 범위를 확인하고 한다.근본 대책은 부모 쪽 코드다. SIGCHLD 핸들러에서 waitpid() 를 반복 호출하거나, 자식을 거둘 필요가 없으면 SIGCHLD 를 SIG_IGN 으로 두면 커널이 알아서 정리한다. 컨테이너 안에서 PID 1 로 도는 프로세스가 자식을 거두지 않는 구조라면 --init 같은 작은 init 프로세스를 앞에 둔다.
top 의 wa 가 뜻하는 것%Cpu(s): 5.3 us, 2.1 sy, 0.0 ni, 89.6 id, 2.7 wa, 0.0 hi, 0.3 si, 0.0 st
wa 는 CPU 가 유휴 상태인데 그중 디스크·네트워크 I/O 완료를 기다리는 작업이 있어서 놀고 있는 시간의 비율이다. 두 가지를 혼동하기 쉽다.
wa 가 높다고 해서 CPU 가 바쁜 것은 아니다. 오히려 놀고 있다.wa 가 0 이라고 해서 I/O 가 빠른 것도 아니다. CPU 를 쓰는 다른 작업이 있으면 그 시간은 us·sy 로 잡힌다.그래서 wa 하나만 보고 판단하지 않는다. 장치별 지연을 함께 본다.
iostat -x 1
await 가 장치 응답 시간, %util 이 장치가 바빴던 시간 비율이다. NVMe 처럼 큐가 깊은 장치에서는 %util 이 100% 여도 여유가 있을 수 있으므로 await 를 더 신뢰한다.
어느 프로세스가 I/O 를 쓰는지 본다.
iotop -oPa
ps -eo pid,stat,wchan:30,comm | awk '$2 ~ /D/'
상태가 D(uninterruptible sleep) 인 프로세스는 I/O 완료를 기다리는 중이다. 이런 프로세스가 쌓여 있고 wchan 이 NFS 관련 함수면 스토리지 쪽을 본다.
wa 가 높을 때 의심하는 것