Profile picture

[Docker] 헬스 체크(health check)

JaehyoJJAng2024년 02월 05일

▶︎ 실습 소스코드


▶︎ Health Check를 지원하는 도커 이미지 빌드하기

안정적인 운영을 위해 컨테이너에 Health check를 도입해보자!


‣ Health check (헬스 체크)

  • docker swarm이나 k8s의 경우 Container Platform 상에서 Application이 스스로 장애에서 회복하는 기능을 제공한다.
    • Platform이 container에서 실행 중인 Application 상태가 정상인지 확인하는 정보를 Image에 함께 패키징 함.
    • 비정상적인 Application이 발생하면 Container를 삭제하고 새 Container로 대체함.
  • Docker가 확인하는 것은 단순히 Process의 실행 여부이지, 정상/비정상 상태를 검사하지는 않음.
    • Process가 종료되었다면 Container도 종료되므로 기본적인 Health check가 이루어지기는 함.
  • Dockerfile에 명시하여 Docker Image에 상태 확인을 위한 로직을 추가해주기만 하면 된다.

‣ Health check 로직이 없는 경우

  • Container runtime은 Process 안에서 무슨 일이 일어나는지, Application의 정상 작동 여부를 판별할 수 없게된다.
docker run -d -it --name test -p 8080:80 diamol/ch08-numbers-api

# 아래 명령어를 계속 반복하면 4번째부터 요청이 실패된다.
curl localhost:8080/rng

/rng 경로로 여러번 요청을 시도하니 아래와 같이 요청이 실패되었다.

{"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1","title":"An error occured while processing your request.","status":500,"traceId":"|343bc9fc-4b5a2dad0196c6ef."}

그러나 컨테이너는 종료되지 않았다.
image

  • Application에 Status: 500 에러가 발생하여 동작하지 않게 되었음.
  • 그럼에도 container는 여전히 up인 상태로 정상 동작 중임.

‣ health check instruction

Dockerfile

HEALTHCHECK CMD curl --fail http://localhost/health
  • health check instruction: Docker가 컨테이너 안에서 실행하는 명령을 지정
    • 명령이 반환하는 상태코드로 Application 상태 판단
  • --fail: curl 명령이 상태 코드를 Docker에 전달
    • 성공하면 0, 실패하면 0 이외의 상태 코드 반환
    • 상태를 판단할 수 있다면 어떤 명령을 지정해도 무방
  • 도커는 해당 명령을 일정 시간 간격으로 수행하여, 일정 횟수 이상 실패 시 Container를 이상 상태로 간주

‣ health check가 반영된 컨테이너 실행

Dockerfile

FROM diamol/dotnet-sdk AS builder

WORKDIR /src
COPY src/Numbers.Api/Numbers.Api.csproj .
RUN dotnet restore

COPY src/Numbers.Api/ .
RUN dotnet publish -c Release -o /out Numbers.Api.csproj

# app image
FROM diamol/dotnet-aspnet

ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"]
HEALTHCHECK CMD curl --fail http://localhost/health

WORKDIR /app
COPY --from=builder /out/ .
# 끝에 .은 현재 작업 디렉토리를 의미. -f로 Dockerfile 경로를 명시
docker build --tag diamol/ch08-numbers-api:v2 -f ./numbers-api/Dockerfile.v2 .
docker run -d -it -p 8081:80 --name test diamol/ch08-numbers-api:v2

# 4번을 반복하면 마찬가지로 에러가 발생한다.
curl http://localhost:8081/rng

watch docker container ls

image
unhealthy로 변경되었음.

  • health check가 감지한 Container의 이상 상태는 Docker API를 통해 Docker에게 보고됨.
    • Docker가 이상 상태를 통보받고 Application 복구를 위한 조치를 취할 수도 있다.

‣ Container 상태 확인하기

docker container inspect $(docker container ls --last 1 --format '{{.ID}}') | grep -A 30 'State"'

image

  • 가장 최근의 health check 수행 결과가 저장되기 때문에 로그 열람이 가능하다.
    • Status: 현재 container 상태
    • FailingStreak: Container에 대해 연속으로 check 실패한 횟수. 기본값은 3번을 넘으면 비정상 상태로 간주한다.

🟠 Docker가 상태 이상의 Container를 다른 Container로 교체하지 않는 이유

  • Cluster 환경에서 동작하는 Docker swarm이나 Kubernetes와는 달리 Docker engine은 단일 서버에서 동작하기 때문에, Container 교체 작업을 안전하게 처리할 수 없다! (리소스 한계)
  • 이상이 생긴 Container를 중지하고 재시작할 수는 있지만, 그동안 Application은 동작하지 않는 문제가 발생한다.
  • 설령 완전히 같은 설정으로 Container를 실행한다 하더라도, 기존의 Container에 보관된 데이터가 유실되고 그 시간 동안 Application이 동작하지 않는다.
  • 즉, Docker는 할 수는 있지만 리스크가 너무 크기 때문에 그냥 냅두고 Health check만 반복하는 것이다.
  • 반면에 Cluster 환경에서는 Container를 추가로 실행할 여력이 항상 남아 있기 때문에 이상 상태를 보이는 Container를 그대로 두고, 대체 Container를 실행하여 Application 중단 시간 없이 회복이 가능하다.

▶︎ Dependency check가 적용된 컨테이너 실행하기

🔑 Cluster 환경에서 Container 재실행시 발생하는 문제점

  • Cluster 환경에서 Health check는 용이하지만 Dependency check는 어려움.
    • 서버가 한 대인 경우 Container 실행 순서를 보장할 수는 있지만, Cluster 환경은 서버가 여러 대이므로 그렇지 않음.
    • 이상이 생긴 Container 교체시 처음 실행할 때 처럼 Container의 Dependency를 고려하지 않음
  • 이로 인해 API는 실행되기도 전에 web application이 먼저 실행될 수도 있음

‣ API 없이 web Application만 실행하는 경우

# 기존 컨테이너 모두 삭제
docker rm -f $(docker ps -aq)

docker run -d -it --name test -p 8082:80 diamol/ch08-numbers-web

docker container ls

핵심 의존 관계를 만족하지 않았기 때문에 Application이 정상 작동하지 않는 상태 image


‣ Dependency check를 위한 명령어 수정

{% include codeHeader.html name="Dockerfile" %}

FROM diamol/dotnet-aspnet

ENV RngApi:Url=http://numbers-api/rng

CMD curl --fail http://numbers-api/rng && \
    dotnet Numbers.Web.dll

WORKDIR /app
COPY --from=builder /out/ .

curl --fail http://~ 가 성공해야 && 뒤의 명령이 실행된다.


# api Container가 실행되지 않았으므로 반드시 실패한다.
docker container run -d -p 8084:80 diamol/ch08-numbers-web:v2

docker container ls --all
  • Dependency check는 Application 실행 전에 한 번만 실행되며, 필요한 요구 사항을 확인한다.
  • 별도의 Instruction으로 구현되는 것이 아니라, Application 실행 명령에 로직을 추가한다.
  • 만약 curl 명령의 반환값이 실패한다면 Container를 종료해버린다.

▶︎ Docker Compose에 Health & Dependency check 명시하기

🟢 Docker compose와 상태 이상의 container

  • Docker Compose 또한 Application 상태 이상 발생 시 이를 복원할 능력이 있지만 대체하지 않음. (단일 서버이므로)
  • 하지만 Dockerfile에 정의되지 않은 Health check를 추가하고, 종료된 Container를 재시작할 수 있다.

‣ Compose 파일에서 Health check 명시하기

healthcheck.yaml

version: "3.8"

services:
  numbers-api:
    image: diamol/ch08-numbers-api:v3
    ports:
      - "8087:80"
    healthcheck:
      interval: 5s
      timeout: 1s
      retries: 2
      start_period: 5s
    networks:
      - app-net

networks:
  app-net:
    driver: bridge
    external: false

Docker compose에서는 Health check 옵션을 더 세세하게 설정할 수 있다.

  • interval : Health check 실시 간격
  • timeout : 이 때까지 응답을 받지 못하면 실패로 간주
  • retries : Container 상태를 이상으로 간주할 때까지 필요한 연속 실패 횟수
  • start_perioid : Container 실행 후 처음 Health check를 실시하는 간격. (Application 시작하는 데 시간이 오래 걸리면 필요)
  • 일반적으로 설정값은 이상 발생을 파악하는 속도와 허용할 수 있는 장애 오탐지 빈도를 고려해 결정한다.
  • CPU와 메모리 자원이 필요하므로 운영 환경에서는 Health check 간격을 좀 더 길게 잡는 것이 좋다.

healthcheck-cmd.yaml

...

  numbers-web:
    image: diamol/ch08-numbers-web:v3
    restart: on-failure
    environment:
      - RngApi__Url=http://numbers-api/rng
    ports:
      - "8088:80"
    healthcheck:
      test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "150"]
      interval: 5s
      timeout: 1s
      retries: 2
      start_period: 10s
    networks:
      - app-net

...
  • test : Health check를 위해 실행하는 명령 (Image에서 Health check 정의 안해도 Compose 파일에서 가능하다.)
  • restart: "on-failure" 설정은 Container가 예기치 않게 종료되면 재시작한다.

web 컨테이너의 경우 api 컨테이너에 의존성을 갖도록 image가 만들어져 있지만

정작 Compose 파일에는 depends_on 설정을 하지 않아 Docker Compose가 컨테이너 실행 순서를 고려하지 않는다.

하지만 API 컨테이너가 실행되기도 전에 web 컨테이너가 먼저 실행되버리면 healthchecktest에 등록된 dependency check에 의해 종료된다.

그러나 웹 컨테이너의 경우 restart: always가 명시되어 있기에 종료되도 다시 실행된다.

결론적으로 API 컨테이너는 무사히 실행되며, Dependency check도 통과하여 Application이 정상적으로 동작할 것이다.

✒️ Compose 파일에 depends_on 설정을 하지 않은 이유

Docker compose가 dependency check를 할 수 있는 범위는 단일 서버에 국한된다.반면, 실제 운영 환경은 단일 서버에서 동작하지 않는다.

보통은 클러스터를 구축할 것이다.
이렇게 되면 운영 환경에서 Application이 실제 시작할 때 일어나는 상황을 예측하기가 힘들어진다.


‣ 실행해보기

docker-compose up -d --build

docker container ls

docker-compose logs numbers-web

image

  • Compose 파일에 의존관계를 설정하지 않아 web Container가 먼저 실행되었다.
  • API Container는 CREATED와 STATUS를 비교했을 때, 실행까지 약 3~4초가 소요되었다.
  • Web Container도 실행은 됐으나 10초 가량이 소요되었다.
  • Web Container 로그를 확인해보면 처음 HTTP 테스트가 응답 거부로 에러가 발생했었다.

Loading script...