LabHub
배우기 러닝패스 코스

ボリューム・ネットワーク・Compose

コンテナは生きているのにサービスは死んでいる

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

프로세스가 살아 있다는 것과 요청을 처리할 수 있다는 것은 다른 사실입니다. 헬스체크는 그 둘 사이의 간격을 메우고, 재시작 정책은 실패했을 때 무엇을 할지 정합니다.

概念マップ: 헬스체크・재시작 정책・PID 1 이 아직 안 죽었다・중요한 함정

왜 이게 필요했나

docker psUp 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

상태는 startinghealthy / unhealthy 로 움직입니다. docker inspect --format '{{.State.Health.Status}}' 로 볼 수 있습니다.

중요한 함정: 도커 단독으로는 unhealthy 가 되어도 컨테이너를 재시작하지 않습니다. 상태를 표시할 뿐입니다. 재시작은 오케스트레이터(Compose 의 depends_on: condition: service_healthy, 스웜, 쿠버네티스의 liveness probe)가 합니다. 헬스체크를 넣었으니 알아서 살아나겠지 — 라고 믿으면 안 됩니다.

헬스체크 엔드포인트를 어떻게 쓸 것인가

/healthz 가 무조건 200 OK 만 돌려주면 아무것도 검사하지 않는 것과 같습니다. 반대로 DB·캐시·외부 API 를 전부 확인하면, 외부 API 하나가 흔들릴 때 우리 서비스가 통째로 unhealthy 가 됩니다.

경계는 이렇게 잡습니다.

이 구분을 안 하면 "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 이미지에는 curlwget 도 없어서 헬스체크가 항상 실패한다. 그럴 때는 애플리케이션 바이너리가 자기 자신을 검사하는 모드를 갖게 하거나, 쿠버네티스의 HTTP 프로브처럼 밖에서 검사하는 방식을 쓴다.

두 검사를 나눈다. 트래픽을 받을 준비가 되었는지(readiness)와 살아 있는지 (liveness)는 다른 질문이다. 앞의 것이 실패하면 트래픽만 빠지고, 뒤의 것이 실패하면 재시작한다. 하나로 합치면 잠깐 바쁜 인스턴스가 재시작된다.

현장에서 만나는 모습

다음 확인에서 볼 것

이어지는 퀴즈에서는 실행 중(running)과 정상(healthy)을 구분하고, start-period·재시작 정책·Compose 의 헬스 조건을 어떤 장애에 적용해야 하는지 판단합니다. 앞 실습에서 만든 컨테이너의 상태 전이와 종료코드를 근거로 답해 보세요.