コンテナは生きているのにサービスは死んでいる
한국어 원문으로 표시합니다.
한 줄 요약
프로세스가 살아 있다는 것과 요청을 처리할 수 있다는 것은 다른 사실입니다. 헬스체크는 그 둘 사이의 간격을 메우고, 재시작 정책은 실패했을 때 무엇을 할지 정합니다.
왜 이게 필요했나
docker ps 에 Up 3 hours 라고 나옵니다. 그런데 사용자는 500 을 받고
있습니다. 프로세스는 살아 있지만 DB 커넥션 풀이 고갈됐거나, 스레드가
데드락이거나, 디스크가 차서 쓰기를 못 하는 상태입니다.
도커가 아는 것은 PID 1 이 아직 안 죽었다는 사실 하나뿐입니다. 그것만으로 "서비스 중"이라고 판단하면 장애를 몇 시간씩 놓칩니다.
어떻게 동작하나
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS http://localhost:8080/healthz || exit 1
| 옵션 | 뜻 | 잘못 잡으면 |
|---|---|---|
interval |
검사 주기 | 너무 짧으면 부하, 길면 감지가 늦다 |
timeout |
한 번의 검사 제한 시간 | 너무 짧으면 정상인데 실패 판정 |
start-period |
기동 유예 — 이 구간의 실패는 재시도로 안 침 | 없으면 느리게 뜨는 앱이 계속 죽는다 |
retries |
연속 실패 허용 횟수 | 1이면 순간적인 지연에도 unhealthy |
상태는 starting → healthy / unhealthy 로 움직입니다.
docker inspect --format '{{.State.Health.Status}}' 로 볼 수 있습니다.
중요한 함정: 도커 단독으로는 unhealthy 가 되어도 컨테이너를 재시작하지
않습니다. 상태를 표시할 뿐입니다. 재시작은 오케스트레이터(Compose 의
depends_on: condition: service_healthy, 스웜, 쿠버네티스의 liveness probe)가
합니다. 헬스체크를 넣었으니 알아서 살아나겠지 — 라고 믿으면 안 됩니다.
헬스체크 엔드포인트를 어떻게 쓸 것인가
/healthz 가 무조건 200 OK 만 돌려주면 아무것도 검사하지 않는 것과
같습니다. 반대로 DB·캐시·외부 API 를 전부 확인하면, 외부 API 하나가
흔들릴 때 우리 서비스가 통째로 unhealthy 가 됩니다.
경계는 이렇게 잡습니다.
- liveness(살아 있나) — 프로세스 자체가 회복 불가 상태인가. 재시작이 답인 것만.
- readiness(받을 준비가 됐나) — 지금 트래픽을 받아도 되나. DB 커넥션 등 포함.
이 구분을 안 하면 "DB 가 잠깐 느려졌다 → liveness 실패 → 재시작 → 커넥션 다시 맺느라 더 느려짐 → 또 실패" 라는 되먹임에 빠집니다. 실제로 자주 일어납니다.
재시작 정책
| 정책 | 동작 |
|---|---|
no (기본) |
죽으면 그대로 둔다 |
on-failure[:N] |
0 이 아닌 종료코드일 때만, 최대 N 번 |
always |
항상 재시작. 데몬 재시작 시에도 다시 뜬다 |
unless-stopped |
always 와 같되, 사람이 stop 한 것은 그대로 둔다 |
always 는 무한 재시작 루프를 만들 수 있습니다. 설정 오류로 즉시 죽는
컨테이너가 초당 몇 번씩 재시작하며 로그를 채우는 상황이 대표적입니다.
도커는 재시작 간격을 점점 늘려(백오프) 완화하지만, 근본 해결은 아닙니다.
depends_on 은 순서만 보장한다
services:
app:
depends_on: [db] # db 컨테이너가 '시작'된 뒤 app 을 시작할 뿐
db 가 요청을 받을 준비가 됐는지는 보지 않습니다. PostgreSQL 은 프로세스가
뜨고 나서도 초기화에 몇 초가 더 걸립니다. 그 사이 앱이 접속하면 실패합니다.
condition: service_healthy 로 헬스체크와 묶어야 의미가 생깁니다.
헬스체크가 오히려 장애를 만드는 경우
헬스체크는 안전장치인데, 잘못 만들면 없는 것보다 나쁘다. 셋을 피하면 된다.
의존성을 검사에 넣으면 함께 죽는다. /health 가 데이터베이스를 조회하면,
DB 가 잠깐 느려질 때 모든 인스턴스가 동시에 unhealthy 가 되어 재시작한다.
재시작한 인스턴스들이 한꺼번에 DB 에 붙으면서 상황이 더 나빠진다. 살아 있는지
보는 검사는 자기 자신만 본다.
시작이 느린 프로그램을 죽인다. JVM 이나 큰 모델을 올리는 서비스는 뜨는 데
1분이 걸리는데, 검사가 30초에 실패하면 영원히 재시작만 반복한다. 도커는
--start-period, 쿠버네티스는 startupProbe 가 이 시간을 준다.
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 CMD curl -fsS http://localhost:8080/healthz || exit 1
재시작 정책이 실패를 가린다. restart: always 는 죽을 때마다 되살리므로,
설정이 틀려서 30초마다 죽는 서비스가 겉보기에는 "돌고 있는" 것으로 보인다.
on-failure:5 처럼 횟수를 정하면 계속 실패할 때 멈추고, 그때 사람이 알아챈다.
쿠버네티스의 CrashLoopBackOff 도 같은 뜻이다 — 지수적으로 늦추면서 문제를
드러내는 장치다.
검사에 쓰는 도구가 이미지에 없을 수 있다. distroless 이미지에는 curl 도
wget 도 없어서 헬스체크가 항상 실패한다. 그럴 때는 애플리케이션 바이너리가
자기 자신을 검사하는 모드를 갖게 하거나, 쿠버네티스의 HTTP 프로브처럼 밖에서
검사하는 방식을 쓴다.
두 검사를 나눈다. 트래픽을 받을 준비가 되었는지(readiness)와 살아 있는지 (liveness)는 다른 질문이다. 앞의 것이 실패하면 트래픽만 빠지고, 뒤의 것이 실패하면 재시작한다. 하나로 합치면 잠깐 바쁜 인스턴스가 재시작된다.
현장에서 만나는 모습
- "재시작하니 되던데요" 가 반복된다 → liveness 는 통과하는데 실제로는 못 쓰는 상태.
- 배포 직후에만 실패한다 →
start-period가 없어 기동 중 검사에 걸린다. - 로그가 재시작 메시지로 가득하다 →
always+ 즉시 실패 조합.
다음 확인에서 볼 것
이어지는 퀴즈에서는 실행 중(running)과 정상(healthy)을 구분하고,
start-period·재시작 정책·Compose 의 헬스 조건을 어떤 장애에 적용해야 하는지
판단합니다. 앞 실습에서 만든 컨테이너의 상태 전이와 종료코드를 근거로 답해 보세요.