컨테이너 안에서 우리 프로세스는 PID 1 이 된다. 리눅스에서 PID 1 은 특별하다.
-
일반 프로세스 — 시그널 핸들러가 없으면 기본 동작이 적용된다
- SIGTERM → 종료
-
PID 1 — 기본 동작이 적용되지 않는다
- 핸들러를 등록하지 않으면 SIGTERM 을 '무시' 한다
- docker stop 이 안 먹고 10초 뒤 SIGKILL 로 강제 종료
$ docker stop web
# 10초를 기다렸다가 강제 종료된다 — 종료 처리가 로그에 안 남는다
셋 중 하나로 푼다
# ① exec form — 애플리케이션이 직접 PID 1 이 된다
ENTRYPOINT ["java", "-jar", "/app.jar"]
# ② init 프로세스를 넣는다
ENTRYPOINT ["/usr/bin/tini", "--", "java", "-jar", "/app.jar"]
# ③ 런타임 옵션
docker run --init myapp
좀비 프로세스도 PID 1 의 일이다
자식 프로세스가 끝나면 부모가 wait 으로 거둬야 한다 거두지 않으면 좀비로 남는다
원래는 PID 1(init)이 고아가 된 프로세스를 입양해 거둬 준다 그런데 컨테이너의 PID 1 은 우리 애플리케이션이고, 대개 남의 자식을 거두는 코드가 없다
- 좀비가 쌓인다 → cgroup 의 pids 한도 도달 → fork 실패
- 증상: "resource temporarily unavailable" · 스레드 생성 실패
자식 프로세스를 만드는 컨테이너(셸 스크립트 · 감시 프로세스)는 tini 같은 최소 init 을 반드시 넣는다
종료 절차
docker stop
- ① SIGTERM 을 PID 1 에게 보낸다
- ② 유예 시간을 기다린다 (기본 10초, -t 로 조정)
- ③ 여전히 살아 있으면 SIGKILL
쿠버네티스도 같다 — terminationGracePeriodSeconds (기본 30초)
- 유예 시간이 처리 시간보다 짧으면 결국 잘린다
- 가장 긴 요청의 처리 시간 + 여유로 잡는다
시그널 자체를 바꿀 수도 있다
STOPSIGNAL SIGQUIT
nginx 는 SIGTERM 이 '즉시 종료' 이고 SIGQUIT 가 '우아한 종료' 다 그대로 두면 배포마다 진행 중 요청이 끊긴다
애플리케이션이 어떤 시그널을 어떻게 처리하는지 확인하고 맞춘다
시그널이 실제로 갔는지 확인한다
# 종료에 걸린 시간을 잰다 — 유예 시간을 꽉 채우면 시그널이 안 간 것이다
time docker stop web
# 애플리케이션에 종료 로그가 남는지
docker logs web | tail -20
docker stop 이 항상 10초 걸린다면 SIGTERM 이 무시되고 있다는 뜻이다 정상이면 애플리케이션이 정리를 마치고 바로 끝난다
이 한 번의 확인으로 '무중단이라고 믿는 배포' 를 검증할 수 있다
graceful shutdown 의 전체 순서
-
① 헬스체크를 실패로 바꾼다 (준비 상태 해제)
-
② 로드밸런서가 이 인스턴스를 뺄 때까지 기다린다 ← 자주 빠뜨린다
-
③ 새 요청 수용 중단
-
④ 처리 중인 요청 완료 대기
-
⑤ 커넥션 풀·큐 컨슈머 정리
-
⑥ 종료
-
② 를 빠뜨리면 LB 가 아직 모르는 상태에서 닫아
-
그 사이 요청이 전부 실패한다
-
유예 시간 ≥ ② 대기 + ④ 최장 처리 시간 + 여유
면접 함정
- ❌ "SIGTERM을 보내면 종료된다" → PID 1은 핸들러가 없으면 무시한다.
- ❌ "좀비는 메모리를 먹는다" → PID를 먹는다. pids 한도에 걸려 fork가 실패한다.