LabHub

도커 기초 · 컨테이너 안으로 들어가기 · 이론

docker stop 은 왜 10초나 걸리는가

LabHub 에서 이어서 보기

한 줄 요약

컨테이너의 첫 프로세스는 PID 1 이고, 리눅스 커널은 PID 1 을 다르게
취급합니다. docker stop 이 10초를 기다리는 것도, Ctrl+C 가 안 먹히는
것도, 좀비 프로세스가 쌓이는 것도 전부 여기서 나옵니다.

왜 이게 필요했나

배포 스크립트에 docker stop 을 넣었더니 컨테이너 하나당 정확히 10초씩
걸립니다. 20개면 200초. 로그를 봐도 에러는 없고, 그냥 느립니다.

원인은 이렇습니다. docker stop 은 먼저 SIGTERM 을 보내고, 기다렸다가,
안 죽으면 SIGKILL 을 보냅니다. 그 "기다리는" 시간의 기본값이 10초입니다.
즉 10초가 걸렸다는 건 SIGTERM 을 받고도 안 죽었다는 뜻입니다.

왜 안 죽었을까요. 일반 프로세스는 시그널 핸들러를 등록하지 않으면 커널이
기본 동작(종료)을 대신 해 줍니다. 그런데 **PID 1 에는 그 기본 동작이
없습니다.** 커널은 PID 1 이 init 이라고 가정하고, 핸들러가 등록되지 않은
시그널은 그냥 버립니다. 시스템의 마지막 프로세스가 실수로 죽으면 커널
패닉이니, 보호 장치인 셈입니다.

그래서 CMD ["python", "app.py"] 로 띄운 파이썬이 SIGTERM 핸들러를 등록하지
않았다면, 그 컨테이너는 SIGTERM 을 무시합니다. 10초 뒤 SIGKILL 로
강제 종료됩니다. 열려 있던 커넥션도, 쓰다 만 파일도 그대로 잘립니다.

어떻게 동작하나

두 번째 함정은 셸 폼입니다.

CMD python app.py          # 셸 폼 → /bin/sh -c "python app.py"CMD ["python", "app.py"]   # exec 폼 → python 이 직접 PID 1

셸 폼으로 쓰면 PID 1 은 파이썬이 아니라 /bin/sh 입니다. sh 는 SIGTERM 을
받아도 자식에게 전달하지 않습니다. 애플리케이션은 시그널을 구경도 못 합니다.

| 상황 | PID 1 | SIGTERM 결과 |
| --- | --- | --- |
| 셸 폼 CMD python app.py | /bin/sh | 앱에 전달 안 됨 → 10초 후 KILL |
| exec 폼, 핸들러 없음 | python | 커널이 버림 → 10초 후 KILL |
| exec 폼, 핸들러 있음 | python | 즉시 정상 종료 ✅ |
| --init 사용 | docker-init | 전달됨 + 좀비 수거 ✅ |

세 번째는 좀비입니다. 자식 프로세스가 죽으면 부모가 wait() 로 종료
상태를 거둬야 프로세스 테이블에서 사라집니다. 부모가 안 거두면 좀비로
남습니다. 원래 이 뒷정리는 init 의 일인데, 컨테이너의 PID 1 이 애플리케이션
이라면 그 앱은 좀비 수거를 하지 않습니다. 셸 스크립트를 계속 실행하는
컨테이너에서 프로세스 수가 며칠에 걸쳐 늘어나는 건 대개 이것입니다.

--init 은 이 두 문제를 한 번에 해결하는 아주 작은 init 프로세스를
PID 1 에 끼워 넣습니다.

exec 와 attach 는 다르다

| | docker exec | docker attach |
| --- | --- | --- |
| 하는 일 | 새 프로세스를 컨테이너 안에서 실행 | PID 1 의 표준입출력에 연결 |
| 나갈 때 | 그 프로세스만 종료 | Ctrl+C 하면 컨테이너가 죽는다 |
| 쓰임새 | 디버깅, 파일 확인 | 원래 프로세스의 출력을 직접 보기 |

운영 중인 컨테이너에 attach 로 붙었다가 습관적으로 Ctrl+C 를 눌러
서비스를 내려 본 사람이 아주 많습니다. 붙을 때는 exec 를 쓰세요.

현장에서 만나는 모습

다음 확인에서 볼 것

이어지는 퀴즈에서는 앞 실습의 관찰을 바탕으로 PID 1의 시그널 전달과 좀비 수거,
docker stop의 강제 종료 조건을 구분합니다. exec 폼과 셸 폼의 차이가 종료 시간과
프로세스 트리에 어떻게 나타났는지를 근거로 답해 보세요.