4 min read

Docker 컨테이너 내 좀비 프로세스 발생 원인과 해결 (tini)

Docker 컨테이너 내 좀비 프로세스 발생 원인과 해결 (tini)

Docker 컨테이너를 운영하다 보면 분명 프로세스는 종료되었는데 <defunct> 상태로 남아 리소스를 점유하는 **좀비 프로세스(Zombie Process)**를 목격할 때가 있다.

단순히 컨테이너를 재시작하면 해결될 문제처럼 보이지만, 근본적인 원인을 파악하지 못하면 대규모 환경에서는 시스템 리소스 고갈로 이어질 수 있다. 시스템 엔지니어 관점에서 이 문제를 추적하고 해결하는 과정을 기록한다.


1. 현상 파악: 왜 좀비가 생기는가?

일반적인 리눅스 시스템에서는 PID 1번(init)이 고립된 자식 프로세스를 입양하고, 종료 시 상태 코드를 회수(reap)하는 역할을 수행한다.

하지만 Docker 컨테이너에서는 상황이 다르다.

  • DockerfileCMDENTRYPOINT에 지정된 프로세스가 PID 1을 부여받는다.
  • 만약 이 프로세스가 **'Signal Handling'**이나 'Child Reaping' 기능이 없는 일반 애플리케이션(예: Python, Node.js, 단순 Shell Script)일 경우, 자식 프로세스가 종료되어도 이를 정리하지 못하고 좀비로 남게 된다.

좀비 프로세스 확인법

컨테이너 내부나 호스트에서 ps 명령어로 상태를 확인했을 때 Z 상태인 프로세스가 보인다면 좀비가 발생한 것이다.

$ ps aux | grep defunct
# 출력 예시
USER       PID  %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        42   0.0  0.0      0     0 ?        Z    14:20   0:00 [my-app] <defunct>

2. 트러블슈팅: PID 1의 책임

리눅스 커널은 프로세스가 종료되면 부모 프로세스에게 SIGCHLD 신호를 보내고, 부모가 wait() 계열 시스템 콜을 호출하여 종료 상태를 읽어가길 기다린다.

문제는 대다수의 애플리케이션이 PID 1번 역할을 수행하도록 설계되지 않았다는 점이다.

  1. 시그널 전달 문제: docker stopSIGTERM이 전달되어도 PID 1이 이를 무시하면 컨테이너는 10초 뒤 강제 종료(SIGKILL)된다.
  2. 좀비 증식: 자식 프로세스가 먼저 죽었을 때 PID 1이 이를 '수확(Reaping)'하지 않으면 커널 프로세스 테이블이 가득 차게 된다.

3. 해결책: tini (Tiny Init) 도입

가장 깔끔하고 표준적인 해결책은 tini와 같은 경량 init 프로세스를 사용하는 것이다. tini는 PID 1번 역할을 대신 수행하며, 시그널을 자식에게 전달하고 좀비 프로세스를 투명하게 정리한다.

방법 A: Dockerfile에 포함하기 (권장)

컨테이너 자체의 이식성을 위해 Dockerfile 내부에 설치하는 방식이다.

# Tini 설치 (Alpine 기준)
RUN apk add --no-local-cache tini

# Tini를 Entrypoint로 지정
ENTRYPOINT ["/sbin/tini", "--"]

# 기존에 사용하던 실행 명령을 CMD로 전달
CMD ["python", "app.py"]

방법 B: Docker Run 커맨드 사용

이미지를 수정할 수 없는 상황이라면 실행 시 --init 플래그를 사용하면 된다. (Docker 1.13+ 필요)

$ docker run --init my-high-quality-image

시스템 엔지니어로서 인프라를 설계할 때, 단순히 서비스가 "떠 있는가"를 넘어 내부 프로세스 생명주기가 정상적으로 관리되는지 확인하는 것은 매우 중요하다.

특히 Python 기반의 AI 에이전트나 쉘 스크립트로 동작하는 자동화 봇을 컨테이너화할 때는 tini 적용을 기본값으로 가져가는 것이 불필요한 리소스 낭비와 좀비 프로세스로 인한 시스템 불안정성을 막는 지름길이다.