저는 YAPP이라는 IT 동아리에서 24기로 활동하며 Wespot이라는 서비스를 만들어냈습니다.
그간 저희 팀은 약 1년 정도, 때로는 빠른 속도로, 때로는 느린 속도로 천천히 서비스를 개발해왔습니다.
약 1년 4개월간의 준비 과정을 거쳐, 현재 마침내 실질적인 서비스 운영을 앞두고 있습니다.
이제 실제 사용자가 유입될 수 있으니 만반의 준비를 진행하고 있습니다.
서비스 QA 등, 예전보다 조금 더 자주 온·오프라인에서 회의를 진행하고 있습니다.
또한, 예전 애플리케이션 심사 때, 서버가 내려가 있어 Reject을 당했던 뼈아픈 추억이 있기에 실사용자가 유입되기 이전에 만반의 준비를 하고자, 서버가 죽더라도 다시 살아날 수 있는 불사신 서버를 만들어보고자 합니다.
혼자 서버를 운영하고 있으며, 인프라도 모두 혼자 구성했습니다.
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
굉장히 간단합니다.
그런데 만약 컨테이너 자체가 아닌 내부 애플리케이션만 죽는다면 어떻게 살릴 수 있을까요?
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 상태가 된 컨테이너가 있다면 이를 재시작시켜주는 유틸리티 컨테이너입니다.
각 컬럼의 의미는 다음과 같습니다.
all로 설정하면 라벨이 붙어있지 않은 컨테이너도 Unhealthy 상태일 경우 다시 띄워줍니다.이렇게 설정했으니 이제 테스트만 해보면 되겠죠?
로컬에서 컨테이너 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를 띄우면 하나의 새로운 컴퓨터가 뜬다고 생각했습니다.
물론 개념적으로는 진짜 Virtual Machine과는 다르다는 것을 인지하고는 있었지만, 크게 체감하고 있지는 못했던 것이 까닭입니다.
Docker는 호스트 커널 위에서 동작하는 하나의 격리된 프로세스 그룹입니다.
그렇기 때문에 호스트 OS의 많은 자원을 공유해 기존 Virtual Machine과 비교했을 때 훨씬 가벼운 리소스로 환경을 구성할 수 있는 것입니다.
Docker는 컨테이너를 실행할 때 새로운 PID namespace를 만들고, 맨 처음 실행되는 프로세스가 바로 PID 1이 됩니다.
즉, 컨테이너 내부에서는 그 프로세스가 init 프로세스 역할을 하는 것입니다.
컨테이너는 기본적으로 단일 프로세스 실행 단위로 설계되어 있으며, 컨테이너 내부에서 추가로 프로세스를 띄우지 않았다면 PID 1만 존재하는 것입니다.
Docker 컨테이너 내부에는 하나의 프로세스만 떠 있고, 이는 루트 프로세스이기에 이를 죽이면 Docker 컨테이너 자체가 내려가 버리는 것입니다.
이를 실제로 시도해보기 전까지는 “Docker 내에서 애플리케이션이 죽으면 어떻게 하지?” 라는 막연한 생각이 있었고, 이를 해결하기 위해 autoheal이라는 유틸리티 컨테이너를 적용했습니다.
하지만 일반적으로 제가 사용하는 방식의 Docker는 애플리케이션 프로세스 하나만 컨테이너 내에 존재하고, 이가 죽으면 컨테이너 자체가 내려가기에 restart 옵션만으로도 충분히 불사신 서버를 만들 수 있다는 것을 알게 되었습니다.
뭔가 쓸데없는 짓을 한 것 같기도 하지만, 이로써 배운 점이 있기 때문에 결코 의미 없는 행위들이었다고 할 수는 없을 것 같습니다.
탕 탕 탕 🔫 💥