불사신 서버 만들기

김재연·2025년 10월 19일
10
post-thumbnail

배경

저는 YAPP이라는 IT 동아리에서 24기로 활동하며 Wespot이라는 서비스를 만들어냈습니다.

그간 저희 팀은 약 1년 정도, 때로는 빠른 속도로, 때로는 느린 속도로 천천히 서비스를 개발해왔습니다.

약 1년 4개월간의 준비 과정을 거쳐, 현재 마침내 실질적인 서비스 운영을 앞두고 있습니다.

이제 실제 사용자가 유입될 수 있으니 만반의 준비를 진행하고 있습니다.

서비스 QA 등, 예전보다 조금 더 자주 온·오프라인에서 회의를 진행하고 있습니다.

또한, 예전 애플리케이션 심사 때, 서버가 내려가 있어 Reject을 당했던 뼈아픈 추억이 있기에 실사용자가 유입되기 이전에 만반의 준비를 하고자, 서버가 죽더라도 다시 살아날 수 있는 불사신 서버를 만들어보고자 합니다.

Docker Restart

혼자 서버를 운영하고 있으며, 인프라도 모두 혼자 구성했습니다.

Redis, Application 등을 Docker를 통해 관리하고 있고, 이 과정에서 다음과 같은 Docker Compose 스크립트를 작성해두었습니다.

version: "3.8"

services:
  redis:
    image: redis
    container_name: redis
    ports:
      - "6379:6379"
    networks:
      - my-network
    environment:
      TZ: "Asia/Seoul"

  wespot-green:
    image: wespot0817/wespot
    container_name: wespot-green
    ports:
      - "8081:8080"
    platform: linux/amd64
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - redis
    networks:
      - my-network
    environment:
      TZ: "Asia/Seoul"
	  
  wespot-blue:
    image: wespot0817/wespot
    container_name: wespot-blue
    ports:
      - "8080:8080"
    platform: linux/amd64
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - redis
    networks:
      - my-network
    environment:
      TZ: "Asia/Seoul"

networks:
  my-network:
    driver: bridge

여기서 wespot-green, wespot-blue무중단 배포(Blue-Green)를 위해 애플리케이션 컨테이너를 포트 번호에 따라 분리해놓았습니다.

Redis는 서버가 배포되더라도 재배포될 필요성이 존재하지 않음으로 따로 분리하지 않았습니다.

만약 여기서 컨테이너가 죽었을 때, 컨테이너를 다시 살리기 위해 어떤 옵션을 추가하면 될까요?

바로 restart 옵션만 추가하면 됩니다.

version: "3.8"

services:
  redis:
    image: redis
    container_name: redis
    restart: always # 추가 
    ports:
      - "6379:6379"
    networks:
      - my-network
    environment:
      TZ: "Asia/Seoul"

  wespot-green:
    image: wespot0817/wespot
    container_name: wespot-green
    restart: always # 추가 
    ports:
      - "8081:8080"
    platform: linux/amd64
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - redis
    networks:
      - my-network
    environment:
      TZ: "Asia/Seoul"

  wespot-blue:
    image: wespot0817/wespot
    container_name: wespot-blue
    restart: always # 추가 
    ports:
      - "8080:8080"
    platform: linux/amd64
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - redis
    networks:
      - my-network
    environment:
      TZ: "Asia/Seoul"

networks:
  my-network:
    driver: bridge

굉장히 간단합니다.


Autoheal

그런데 만약 컨테이너 자체가 아닌 내부 애플리케이션만 죽는다면 어떻게 살릴 수 있을까요?

Docker 컨테이너 자체가 죽으면 restart가 정상적으로 실행되지만, 내부에 있는 애플리케이션만 죽는다면 이는 정상적으로 동작하지 않을 것입니다.

그렇기 때문에 지속적으로 Docker 컨테이너 내의 애플리케이션이 정상적으로 구동되고 있는지 확인해주는 HealthChecker가 필요합니다.

그리고 만약 애플리케이션이 죽었다는 것을 확인하면 이를 재구동시켜주는 프로세스가 필요할 것입니다.

이를 위해 찾은 것이 autoheal이라는 툴이었습니다.

version: "3.8"

services:
  ... 쭉쭉

  wespot-green:
	... 쭉쭉
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080"] 
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  wespot-blue:
  	... 쭉쭉
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  autoheal:
    image: willfarrell/autoheal
    container_name: autoheal
    restart: always
    environment:
      AUTOHEAL_CONTAINER_LABEL: all
      AUTOHEAL_INTERVAL: 10
      AUTOHEAL_START_PERIOD: 60
      AUTOHEAL_DEFAULT_STOP_TIMEOUT: 10
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - my-network

... 쭉쭉

Autoheal은 Docker 컨테이너를 지속적으로 헬스 체크하고, 헬스 체크 과정에서 Unhealthy 상태가 된 컨테이너가 있다면 이를 재시작시켜주는 유틸리티 컨테이너입니다.

각 컬럼의 의미는 다음과 같습니다.

  • healthcheck.test : 헬스 체크를 진행하는 방법입니다.
  • healthcheck.interval : 헬스 체크를 몇 초 주기로 진행할지 결정합니다.
  • healthcheck.timeout : 헬스 체크의 timeout 설정입니다.
  • healthcheck.retries : 컨테이너의 상태를 Unhealthy로 만들기까지 몇 번 시도해야 하는지 나타냅니다. (예: 3이면 3번 연속 실패 시 Unhealthy)
  • healthcheck.start_period : Docker가 시작한 뒤 몇 초 후에 헬스 체크를 시작할지 명시합니다.
  • environment.AUTOHEAL_CONTAINER_LABEL : all로 설정하면 라벨이 붙어있지 않은 컨테이너도 Unhealthy 상태일 경우 다시 띄워줍니다.
  • environment.AUTOHEAL_INTERVAL : Unhealthy 상태의 컨테이너가 있는지를 몇 초 간격으로 확인할지 결정합니다.
  • environment.AUTOHEAL_START_PERIOD : 프로세스가 시작하고 얼마나 지난 후에 이 과정을 시작할지 결정합니다.
  • environment.AUTOHEAL_DEFAULT_STOP_TIMEOUT : 프로세스가 정상 종료할 시간을 얼마나 줄지 결정하는 값입니다. 예를 들어 10으로 설정하면, Docker가 재시작할 때 10초 이상 종료되지 않으면 강제로 kill한 뒤 다시 띄웁니다.

이렇게 설정했으니 이제 테스트만 해보면 되겠죠?


실험

로컬에서 컨테이너 3개를 띄우고 테스트를 진행했습니다.

docker compose up -d redis wespot-blue autoheal 명령어를 통해 Docker 컨테이너 3개를 띄웠습니다.

먼저 docker exec -it wespot-blue /bin/bash 명령어로 컨테이너 내부에 들어가서 애플리케이션 프로세스를 죽이려고 했습니다.

그런데 일반적으로 Spring Application 프로세스를 찾아내기 위해 필요한 lsof와 같은 명령어가 전혀 동작하지 않았고, Chatgpt와의 사투 끝에 애플리케이션의 Process ID를 알아낼 수 있었고, 그 때 사용한 코드는 다음과 같습니다.

for p in /proc/[0-9]*; do
  pid=$(basename "$p")
  # /proc/<pid>/cmdline에 "java" 포함되면 출력
  if [ -r "$p/cmdline" ] && grep -a -q "java" "$p/cmdline" 2>/dev/null; then
    echo "$pid"
  fi
done

PID가 1번이었습니다.

그리고 PID 1번 말고는 아무것도 존재하지 않았습니다.

암튼 애플리케이션의 PID가 1번이라는 것을 알게 되었으니 kill 1을 진행하였는데, 바로 컨테이너가 사망했습니다.

docker logs를 활용해 로그를 살펴보았지만, 애플리케이션은 이미 새롭게 뜨고 있었습니다. 참으로 기이했습니다.

조금 더 찾아보니, Docker의 기본 패러다임을 더 깊이 이해할 수 있었습니다.


Docker 컨테이너 내에 떠 있는 프로세스가 왜 1개밖에 없을까?

저는 지금까지 Docker를 띄우면 하나의 새로운 컴퓨터가 뜬다고 생각했습니다.

물론 개념적으로는 진짜 Virtual Machine과는 다르다는 것을 인지하고는 있었지만, 크게 체감하고 있지는 못했던 것이 까닭입니다.

Docker는 호스트 커널 위에서 동작하는 하나의 격리된 프로세스 그룹입니다.

그렇기 때문에 호스트 OS의 많은 자원을 공유해 기존 Virtual Machine과 비교했을 때 훨씬 가벼운 리소스로 환경을 구성할 수 있는 것입니다.

Docker는 컨테이너를 실행할 때 새로운 PID namespace를 만들고, 맨 처음 실행되는 프로세스가 바로 PID 1이 됩니다.

즉, 컨테이너 내부에서는 그 프로세스가 init 프로세스 역할을 하는 것입니다.

컨테이너는 기본적으로 단일 프로세스 실행 단위로 설계되어 있으며, 컨테이너 내부에서 추가로 프로세스를 띄우지 않았다면 PID 1만 존재하는 것입니다.

Docker 컨테이너 내부에는 하나의 프로세스만 떠 있고, 이는 루트 프로세스이기에 이를 죽이면 Docker 컨테이너 자체가 내려가 버리는 것입니다.


이로써 알 수 있던 사실

이를 실제로 시도해보기 전까지는 “Docker 내에서 애플리케이션이 죽으면 어떻게 하지?” 라는 막연한 생각이 있었고, 이를 해결하기 위해 autoheal이라는 유틸리티 컨테이너를 적용했습니다.

하지만 일반적으로 제가 사용하는 방식의 Docker는 애플리케이션 프로세스 하나만 컨테이너 내에 존재하고, 이가 죽으면 컨테이너 자체가 내려가기에 restart 옵션만으로도 충분히 불사신 서버를 만들 수 있다는 것을 알게 되었습니다.

뭔가 쓸데없는 짓을 한 것 같기도 하지만, 이로써 배운 점이 있기 때문에 결코 의미 없는 행위들이었다고 할 수는 없을 것 같습니다.

profile
끊임없이 '성장'하는 개발자 김재연입니다.

2개의 댓글

comment-user-thumbnail
2025년 10월 19일

탕 탕 탕 🔫 💥

1개의 답글