Skip to Content
Infra & DevOpsTomcat 안전 셧다운 및 재시작 스크립트
☁️ Infra & DevOps2018년 11월 2일

Tomcat 안전 셧다운 및 재시작 스크립트

#tomcat#infra-devops#bash#deploy#script#automation

1. Tomcat 다운타임과 좀비 프로세스 이슈

과거 레거시 웹 개발 환경에서는 물리 장비 혹은 단일 VM에 여러 개의 톰캣(Tomcat) 인스턴스를 올려서 서비스하는 경우가 잦았습니다. 특히 데이터노드 측에서 초당 쏟아지는 메트릭 데이터를 플랫폼 서버가 화면에 뿌려주는 과정에서, 간혹 가비지 컬렉션(GC) 튜닝 실패나 메모리 단편화로 인해 톰캣 프로세스가 좀비(Zombie) 상태로 빠져 행업(Hang-up)되는 현상이 있었습니다.

Apache Tomcat 패키지에 내장된 기본 shutdown.sh을 실행할 경우, DB 커넥션 풀이 덜 닫혔거나 비동기 스레드가 돌고 있으면 프로세스가 죽지 않고 며칠이고 포트를 물고 늘어집니다.

이때 관리자가 단순히 /bin/shutdown.sh를 실행해도 톰캣은 정상 종료되지 않으며, 그 위에 곧바로 startup.sh를 또 실행하면 “해당 포트는 이미 사용중입니다” 라며 에러를 뿜게 됩니다. 이는 CI/CD 자동 배포 파이프라인이 빈번하게 실패하는 가장 큰 원인이었습니다.


2. 안전한 종료 메커니즘 (Graceful Shutdown)

안전한 톰캣 재시작의 3단계 룰은 다음과 같습니다.

  1. Graceful Shutdown: 일단 톰캣이 제공하는 기본 종료 스크립트를 호출하여 내부 애플리케이션 스레드가 정상 종료될 시간을 줍니다.
  2. 소프트 킬 (SIGTERM): 일정 시간(약 10초)이 지나도 데몬이 죽지 않았다면 kill 명령(15)으로 범용적인 프로세스 종료 시그널을 던집니다. (안전벨트)
  3. 하드 킬 (SIGKILL): 최후의 수단으로 kill -9를 전송하여 메모리에서 즉각 강제 삭제하고 좀비 스레드를 박멸합니다.

3. 안전 종료 래퍼 스크립트 구현

아래는 위 3단계 룰을 준수하면서, 다중 톰캣 구동 환경에서도 겹치지 않게 정확한 톰캣 인스턴스의 PID를 찾아내 순차적으로 안전한 종료를 수행하고 기동시키는 실무용 배포 자동화 .sh 파일입니다.

※ 주의: 하단 스크립트 내의 tomcat_pid 함수에서 grep /web 등 각자의 환경에 맞는 유니크한 디렉토리 혹은 옵션 키워드로 검색 값을 반드시 변경하여야 서로 다른 톰캣 프로세스를 엉뚱하게 건드리지 않습니다. 단순 grep java는 절대 금물입니다.

bash
#!/bin/bash ################################################################### # Script Name : Tomcat Restart # Description : 톰캣 프로세스를 자동으로 안전하게 킬하고 다시 띄운다. 먼저 SIGTERM 5회를 시도하고, 그래도 종료되지 않으면 SIGKILL (-9)을 보낸다. ################################################################### check_root() { # root 사용자로 톰캣을 띄우는 보안 취약점 방지 if [ "${UID}" -eq 0 ] then echo "You are root. Can not run tomcat as root user." exit 1 fi } tomcat_pid() { # 시스템에 여러 톰캣이 존재할 경우를 대비한 유니크 키 추출. (예시: /web 디렉토리를 포함한 프로세스 식별) echo `ps aux | grep org.apache.catalina.startup.Bootstrap | grep /web | grep -v grep | awk '{ print $2 }'` } start() { echo -e "\e[92mStarting Tomcat\e[0m" "$TOMCAT_PATH"/startup.sh >& /dev/null sleep 2 PID=$(tomcat_pid) echo -e "Tomcat is running with pid: \e[96m$PID\e[0m" } stop() { echo -e "\e[93mShutdown Tomcat\e[0m" "$TOMCAT_PATH"/shutdown.sh >& /dev/null sleep 2 # 기본 2초 대기 # 1차: 정상 종료 확인 (5회 반복) for i in {1..5} do PID=$(tomcat_pid) if [ -z "$PID" ] ; then echo -e "\e[91mShutdown completed.\e[0m" return else sleep 2 fi done # 2차: 프로세스가 안 죽었으면 일반 kill (SIGTERM) PID=$(tomcat_pid) if [ -n "$PID" ] ; then echo "Kill process..." for i in {1..2} do PID=$(tomcat_pid) if [ -z "$PID" ] ; then echo -e "\e[91mShutdown completed.\e[0m" return else killProc sleep 2 fi done fi # 3차: 그래도 살아있으면 강제 킬 (SIGKILL -9) if [ -n "$PID" ] ; then echo "Force kill process..." killProc 9 fi sleep 2 PID=$(tomcat_pid) if [ -z "$PID" ] ; then echo -e "\e[91mShutdown completed.\e[0m" return fi } # 프로세스 종료 래퍼 함수 killProc() { local signal="TERM" if [ "$#" = 1 ] ; then signal=$1 fi if [ ! -z ${PID} ] && [ ${PID} -gt 0 ] ; then echo "kill -${signal} ${PID}" kill -${signal} ${PID}; fi } # 메인 로직 시작 PID=$(tomcat_pid) TOMCAT_PATH="$(cd "$(dirname "$0")" && pwd)" if [ -n "$PID" ] ; then echo -e "Tomcat is running with pid: \e[96m$PID\e[0m" echo -e "Tomcat bin location is \e[94m$TOMCAT_PATH\e[0m" stop start else echo "Tomcat is not running" echo -e "Tomcat bin location is \e[94m$TOMCAT_PATH\e[0m" start fi

4. 무중단 배포 연동 회고

이 짧은 쉘 프로그래밍 스크립트 하나가 팀 내 인프라에 도입된 날, 배포 파이프라인에서 수시로 발생하던 행업 이슈와 데몬 좀비화 현상으로 인한 재배포/수작업 리스크가 완벽히 사라졌습니다.

데몬 프로세스의 수명 주기를 제어할 때는 무작정 매몰찬 킬 스크립트(kill -9)를 반복적으로 때려 넣는 것보다, 애플리케이션 스스로 안전하게 커넥션을 닫고 후처리를 할 수 있는 시간과 기회(Graceful period)를 철저히 보장해주는 것이 훌륭한 백엔드 엔지니어링의 기본임을 다시 한번 체감할 수 있었습니다.

(물론 이후 시대의 흐름에 따라 컨테이너 오케스트레이션 환경(K8s)으로 시스템이 넘어가게 되면서, 이 복잡한 스크립트는 쿠버네티스 고유의 컨테이너 라이프사이클 훅인 OOM Killed / Terminating 로직과 preStop 훅으로 전부 우아하게 흡수되었습니다.)

Last updated on