Tomcat 안전 셧다운 및 재시작 스크립트
1. Tomcat 다운타임과 좀비 프로세스 이슈
과거 레거시 웹 개발 환경에서는 물리 장비 혹은 단일 VM에 여러 개의 톰캣(Tomcat) 인스턴스를 올려서 서비스하는 경우가 잦았습니다. 특히 데이터노드 측에서 초당 쏟아지는 메트릭 데이터를 플랫폼 서버가 화면에 뿌려주는 과정에서, 간혹 가비지 컬렉션(GC) 튜닝 실패나 메모리 단편화로 인해 톰캣 프로세스가 좀비(Zombie) 상태로 빠져 행업(Hang-up)되는 현상이 있었습니다.
Apache Tomcat 패키지에 내장된 기본 shutdown.sh을 실행할 경우, DB 커넥션 풀이 덜 닫혔거나 비동기 스레드가 돌고 있으면 프로세스가 죽지 않고 며칠이고 포트를 물고 늘어집니다.
이때 관리자가 단순히 /bin/shutdown.sh를 실행해도 톰캣은 정상 종료되지 않으며, 그 위에 곧바로 startup.sh를 또 실행하면 “해당 포트는 이미 사용중입니다” 라며 에러를 뿜게 됩니다. 이는 CI/CD 자동 배포 파이프라인이 빈번하게 실패하는 가장 큰 원인이었습니다.
2. 안전한 종료 메커니즘 (Graceful Shutdown)
안전한 톰캣 재시작의 3단계 룰은 다음과 같습니다.
- Graceful Shutdown: 일단 톰캣이 제공하는 기본 종료 스크립트를 호출하여 내부 애플리케이션 스레드가 정상 종료될 시간을 줍니다.
- 소프트 킬 (SIGTERM): 일정 시간(약 10초)이 지나도 데몬이 죽지 않았다면
kill명령(15)으로 범용적인 프로세스 종료 시그널을 던집니다. (안전벨트) - 하드 킬 (SIGKILL): 최후의 수단으로
kill -9를 전송하여 메모리에서 즉각 강제 삭제하고 좀비 스레드를 박멸합니다.
3. 안전 종료 래퍼 스크립트 구현
아래는 위 3단계 룰을 준수하면서, 다중 톰캣 구동 환경에서도 겹치지 않게 정확한 톰캣 인스턴스의 PID를 찾아내 순차적으로 안전한 종료를 수행하고 기동시키는 실무용 배포 자동화 .sh 파일입니다.
※ 주의: 하단 스크립트 내의
tomcat_pid함수에서grep /web등 각자의 환경에 맞는 유니크한 디렉토리 혹은 옵션 키워드로 검색 값을 반드시 변경하여야 서로 다른 톰캣 프로세스를 엉뚱하게 건드리지 않습니다. 단순grep java는 절대 금물입니다.
#!/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
fi4. 무중단 배포 연동 회고
이 짧은 쉘 프로그래밍 스크립트 하나가 팀 내 인프라에 도입된 날, 배포 파이프라인에서 수시로 발생하던 행업 이슈와 데몬 좀비화 현상으로 인한 재배포/수작업 리스크가 완벽히 사라졌습니다.
데몬 프로세스의 수명 주기를 제어할 때는 무작정 매몰찬 킬 스크립트(kill -9)를 반복적으로 때려 넣는 것보다, 애플리케이션 스스로 안전하게 커넥션을 닫고 후처리를 할 수 있는 시간과 기회(Graceful period)를 철저히 보장해주는 것이 훌륭한 백엔드 엔지니어링의 기본임을 다시 한번 체감할 수 있었습니다.
(물론 이후 시대의 흐름에 따라 컨테이너 오케스트레이션 환경(K8s)으로 시스템이 넘어가게 되면서, 이 복잡한 스크립트는 쿠버네티스 고유의 컨테이너 라이프사이클 훅인 OOM Killed / Terminating 로직과 preStop 훅으로 전부 우아하게 흡수되었습니다.)