Search
🔵

docker compose를 이용한 컨테이너를 재시작하고 프로세스 자동 복구하기

프로젝트
🚀 prev note
🚀 next note
♻️ next note
15 more properties
서비스 운영 중 가장 중요한 요소 중 하나는 안정성이다. 특히 Docker 컨테이너 환경에서는 서비스가 예상치 못한 이유로 응답하지 않는 상황이 발생할 수 있다. 실제로 운영 환경에서는 메모리 누수나 자원 고갈로 인해 컨테이너가 먹통이 되는 경우가 생각보다 빈번하게 발생한다. 이런 상황을 자동으로 감지하고 복구하는 시스템을 구축해보자.

문제 상황 이해하기

평소에는 잘 동작하지만, 부하가 심하거나 특정 조건에서 응답이 지연되거나 중단되는 FastAPI 웹 서비스가 있다고 가정해보자. 프로젝트의 디렉토리 구조는 다음과 같다:
my_fastapi_service/ ├── app.py # FastAPI 애플리케이션 코드 ├── entrypoint.sh # 컨테이너 시작 및 모니터링 스크립트 ├── Dockerfile # Docker 이미지 정의 ├── docker-compose.yml # Docker Compose 설정 └── logs/ # 스레드 덤프가 저장될 디렉토리
Plain Text
복사
Docker Compose는 컨테이너의 상태를 확인하는 health check 기능은 제공하지만, 건강 상태에 따라 특별한 작업을 수행하는 기능은 제공하지 않는다. Docker Compose에서 health check를 설정하는 방법은 다음과 같다:
version: '3' services: api: build: . ports: - "8000:8000" volumes: - ./logs:/ws/logs restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 5s
YAML
복사
위의 healthcheck 설정은 Docker에게 주기적으로 컨테이너의 상태를 확인하도록 지시하지만, 상태가 비정상이라고 해서 Docker가 자동으로 컨테이너를 재시작하거나 다른 조치를 취하지는 않는다. 단, 컨테이너가 종료되면 restart: unless-stopped 설정에 따라 자동으로 재시작한다.
이러한 상황에서 우리는 entrypoint.sh 스크립트를 활용하여 건강 상태 모니터링과 문제 발생 시 컨테이너를 종료하는 로직을 구현할 수 있다. 컨테이너가 종료되면 Docker Compose가 이를 자동으로 재시작하게 됨으로써 서비스 복구 메커니즘을 완성할 수 있다.

FastAPI 서비스 설계

먼저 문제 상황을 시뮬레이션하기 위한 FastAPI 서비스를 만들어보자. 이 서비스는 /health 엔드포인트를 제공하며, 요청을 받을 때마다 대기 시간을 점진적으로 늘려 결국 응답이 지연되는 상황을 모방한다.
from fastapi import FastAPI import time app = FastAPI() # 요청 횟수를 카운트하는 변수 cnt = 0 @app.get("/health") async def health_check(): global cnt # 요청이 들어올 때마다 카운트 증가 cnt += 1 # 카운트가 증가할수록 대기 시간도 늘어남 time.sleep(cnt) return {"status": "healthy"}
Python
복사
이 코드는 /health 엔드포인트에 요청이 들어올 때마다 cnt 변수를 1씩 증가시키고, 그에 비례하여 응답 시간을 늘린다. 결국 요청이 많아질수록 서비스는 점점 더 느려지게 된다. 이는 실제 운영 환경에서 자주 발생하는 메모리 누수나 자원 고갈 문제를 단순화하여 모방한 것이다.

상태 모니터링 스크립트 작성

이제 Docker 컨테이너가 시작될 때 실행될 entrypoint.sh 스크립트를 작성해보자. 이 스크립트는 서비스의 건강 상태를 주기적으로 확인하고, 문제가 발생하면 프로세스를 종료하고 스크립트 자체도 종료한다. Docker의 특성상 entrypoint 스크립트가 종료되면 컨테이너도 함께 종료되며, Docker Compose의 restart 정책에 따라 컨테이너가 자동으로 재시작된다.
#!/bin/bash # 상태 확인 관련 설정값 RETRIES=3 # 재시도 횟수 START_PERIOD=5 # 초기 대기 시간(초) INTERVAL=10 # 확인 간격(초) TIMEOUT=5 # 요청 타임아웃(초) HEALTHCHECK_URL="http://localhost:8000/health" # 상태 확인 URL # 애플리케이션 시작 python app.py & APP_PID=$! # 애플리케이션이 시작될 때까지 대기 echo "서비스 시작 대기 중... ${START_PERIOD}초" sleep $START_PERIOD # 연속 실패 횟수 카운터 fail_count=0 # 상태 확인 및 처리 루프 while true; do # curl로 상태 확인, -m은 최대 대기 시간(타임아웃)을 설정 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m $TIMEOUT $HEALTHCHECK_URL) # HTTP 상태 코드 확인 (200이면 정상) if [ $HTTP_CODE -ge 200 ] && [ $HTTP_CODE -lt 300 ]; then # 정상 응답이면 카운터 초기화 fail_count=0 echo "$(date): 서비스 상태 정상" else # 비정상 응답이면 카운터 증가 fail_count=$((fail_count + 1)) echo "$(date): 서비스 상태 이상 감지. 실패 횟수: $fail_count/$RETRIES" # 실패 횟수가 임계값에 도달하면 조치 취함 if [ $fail_count -ge $RETRIES ]; then echo "$(date): 서비스 복구 실패. 스레드 덤프 생성 중..." # pystack을 사용하여 스레드 덤프 생성 mkdir -p /ws/logs DUMP_FILE="/ws/logs/thread_dump_$(date +%Y%m%d_%H%M%S).txt" pystack dump $APP_PID > $DUMP_FILE echo "스레드 덤프 저장 완료: $DUMP_FILE" echo "서비스 재시작 중..." # 프로세스 종료 kill -9 $APP_PID # 종료 코드 반환 (Docker가 컨테이너를 재시작하도록) exit 1 fi fi # 다음 검사까지 대기 sleep $INTERVAL done
Shell
복사
이 스크립트는 다음과 같은 역할을 한다:
1.
초기 설정값을 정의한다(재시도 횟수, 확인 주기 등).
2.
FastAPI 애플리케이션을 백그라운드로 시작한다.
3.
/health 엔드포인트를 주기적으로 확인하여 서비스의 상태를 모니터링한다.
4.
비정상 응답이 감지되면 실패 카운터를 증가시키고, 정상 응답이 오면 카운터를 초기화한다.
5.
실패 카운터가 설정된 재시도 횟수에 도달하면 pystack을 사용하여 스레드 덤프를 생성한다.
6.
덤프를 저장한 후 프로세스를 강제 종료하고 스크립트를 종료한다.
스크립트가 exit 코드 1로 종료되면 Docker의 특성상 컨테이너도 함께 종료된다. 그 후 Docker Compose의 restart: unless-stopped 설정에 따라 컨테이너가 자동으로 재시작된다. 이것이 바로 우리 자동 복구 메커니즘의 핵심이다.

Docker 구성하기

이제 Docker 환경을 구성해 보자. 먼저 Dockerfile부터 작성한다.
FROM python:3.9 WORKDIR /ws # 필요한 패키지 설치 RUN pip install fastapi uvicorn pystack # 애플리케이션 코드 복사 COPY app.py . COPY entrypoint.sh . # 실행 권한 부여 RUN chmod +x entrypoint.sh # 엔트리포인트 설정 ENTRYPOINT ["./entrypoint.sh"]
Docker
복사
다음으로 Docker Compose 파일을 작성하여 로그 디렉토리를 마운트한다. 이를 통해 컨테이너가 재시작되더라도 스레드 덤프 파일이 유지되어 문제 분석에 활용할 수 있다.
version: '3' services: api: build: . ports: - "8000:8000" volumes: - ./logs:/ws/logs restart: unless-stopped
YAML
복사
이 설정에서 중요한 부분은 restart: unless-stopped 옵션이다. 이 옵션은 컨테이너가 비정상 종료될 경우 자동으로 재시작하도록 설정한다. entrypoint.sh 스크립트가 문제를 감지하고 exit 코드로 종료하면, 이 설정에 의해 컨테이너가 자동으로 재시작되어 서비스가 복구된다.

작동 원리 이해하기

이 시스템의 핵심은 entrypoint.sh 스크립트와 Docker Compose의 자동 재시작 기능이 결합된 형태이다. entrypoint.sh는 문제를 감지하고 컨테이너를 의도적으로 종료하며, Docker Compose는 이를 다시 시작한다.
서비스가 응답하지 않으면 스크립트는 실패 카운터를 증가시키고, 설정된 재시도 횟수에 도달하면 문제 해결을 위한 조치를 취한다. 이는 일시적인 부하로 인한 지연과 지속적인 문제를 구별하기 위함이다.
문제가 지속되면 pystack을 사용하여 스레드 덤프를 생성한다. pystack은 Python 프로세스의 현재 실행 상태를 캡처하여 스레드별로 어떤 코드가 실행 중인지, 어디서 블록되어 있는지 등을 보여준다. 이 정보는 서비스가 응답하지 않는 원인을 분석하는 데 매우 유용하다. 특히 알 수 없는 이유로 서비스가 먹통이 되는 경우가 실제 운영 환경에서는 생각보다 자주 발생하는데, 이 때 스레드 덤프는 문제 해결을 위한 중요한 단서를 제공한다.
스레드 덤프가 생성된 후에는 프로세스를 강제 종료하고 스크립트를 exit 코드 1로 종료한다. Docker의 특성상 entrypoint 스크립트가 종료되면 컨테이너도 함께 종료된다. 그 후 Docker Compose의 재시작 설정에 의해 컨테이너는 자동으로 재시작되며, 새로운 프로세스가 시작된다.
로그 디렉토리를 볼륨으로 마운트한 덕분에, 컨테이너가 재시작되더라도 스레드 덤프 파일은 호스트 시스템에 유지된다. 이를 통해 문제가 발생한 시점의, 여러 시점의 스레드 덤프를 수집하고 비교하여 패턴을 파악할 수 있다.

한계점과 고려사항

우리가 구현한 방식은 컨테이너 내에서 특정 프로세스의 상태만 모니터링하기 때문에 아래와 같은 상황에서는 효과적이지 않을 수 있다. 컨테이너 자체에 문제가 생기는 경우에는 대응하기 어렵다. 예를 들어, 모니터링 스크립트가 실행되는 쉘 프로세스나 OS 수준의 문제가 발생하면 모니터링 자체가 실패할 수 있다.
이러한 상황에서는 컨테이너 외부에서의 모니터링이 필요할 수 있다. 예를 들어, Kubernetes와 같은 오케스트레이션 도구를 사용하거나, 외부 모니터링 시스템을 구축하여 컨테이너의 상태를 감시하는 방법이 있다.
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
1.
None
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
1.
앞의 글은 쿠버네티스를 이용해 컨테이너가 항상 건강한 상태를 유지하도록 만들 수 있음을 내포하지만, 컨테이너를 되살리는 것과 같은 아주 기본적이지만 핵심적인 로직은 쿠버네티스를 이용하지 않더라도 쉽게 구현이 가능하다.
supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.
1.
None
opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.
1.
None
to : 이 문서에 작성된 생각이 어떤 생각으로 발전되거나 이어지는지를 작성하는 영역입니다.
1.
None
ref : 생각에 참고한 자료입니다.
1.
None