systemd 를 쓸 수 없는 상황이 있다. root 권한이 없어 유닛을 등록하지 못하거나, 여러 인스턴스를 한 계정으로 돌려야 하거나, 컨테이너 안이거나, 배포 규칙이 그렇게 정해져 있는 경우다. 이때는 PID 파일을 직접 관리하는 제어 스크립트를 쓴다.
권한이 있으면 systemd 를 쓴다. 재시작 정책 · 자원 제한 · 로그 수집 · 의존 관계를 스크립트로 다시 만들 이유가 없다.
#!/usr/bin/env bash
set -uo pipefail
APP_NAME=prometheus
APP_HOME=/home/services/prometheus
BIN="${APP_HOME}/bin/prometheus"
CONF="${APP_HOME}/conf/prometheus.yml"
DATA=/xvdb/prometheus/tsdb
LOG="${APP_HOME}/logs/${APP_NAME}.log"
PIDFILE="${APP_HOME}/run/${APP_NAME}.pid"
ARGS=(
"--config.file=${CONF}"
"--storage.tsdb.path=${DATA}"
"--storage.tsdb.retention.time=90d"
"--web.listen-address=:10011"
"--web.enable-lifecycle"
)
mkdir -p "$(dirname "$PIDFILE")" "$(dirname "$LOG")"
running() {
[ -f "$PIDFILE" ] || return 1
local pid; pid=$(cat "$PIDFILE" 2>/dev/null) || return 1
[ -n "$pid" ] || return 1
kill -0 "$pid" 2>/dev/null || return 1
# PID 재사용을 걸러낸다
grep -q "$APP_NAME" "/proc/${pid}/cmdline" 2>/dev/null
}
start() {
if running; then
echo "$APP_NAME 이미 실행 중 (pid $(cat "$PIDFILE"))"
return 0
fi
rm -f "$PIDFILE"
nohup "$BIN" "${ARGS[@]}" >> "$LOG" 2>&1 &
echo $! > "$PIDFILE"
sleep 1
if running; then
echo "$APP_NAME 시작 (pid $(cat "$PIDFILE"))"
else
echo "$APP_NAME 시작 실패. 로그를 확인한다: $LOG" >&2
rm -f "$PIDFILE"
return 1
fi
}
stop() {
if ! running; then
echo "$APP_NAME 실행 중이 아니다"
rm -f "$PIDFILE"
return 0
fi
local pid; pid=$(cat "$PIDFILE")
kill "$pid"
for _ in $(seq 1 30); do
running || break
sleep 1
done
if running; then
echo "정상 종료되지 않아 강제 종료한다 (pid $pid)" >&2
kill -9 "$pid"
sleep 1
fi
rm -f "$PIDFILE"
echo "$APP_NAME 종료"
}
status() {
if running; then
echo "$APP_NAME 실행 중 (pid $(cat "$PIDFILE"))"
else
echo "$APP_NAME 정지"
return 3
fi
}
case "${1:-}" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
*) echo "사용법: $0 {start|stop|restart|status}" >&2; exit 1 ;;
esac
PID 파일만 믿지 않는다. 프로세스가 비정상 종료하면 PID 파일이 남고, 그 번호를 다른 프로세스가 물려받을 수 있다. kill -0 으로 존재를 확인한 뒤 /proc/<pid>/cmdline 으로 그것이 정말 우리 프로세스인지 본다.
pkill -f 로 찾아 죽이지 않는다. 같은 문자열이 들어간 다른 프로세스나 심지어 스크립트 자신을 죽인다. 굳이 써야 한다면 무엇이 잡히는지 먼저 본다.
pgrep -af prometheus
kill -9 를 처음부터 쓰지 않는다. 기본 kill(SIGTERM)은 프로세스가 정리할 기회를 준다. Prometheus 같은 저장 엔진은 강제 종료하면 WAL 재생에 시간이 걸리거나 데이터가 상한다. 기다린 뒤에도 남아 있을 때만 -9 를 쓴다.
로그를 덮어쓰지 않는다. > 가 아니라 >> 로 이어 붙이고, 커지면 logrotate 로 관리한다.
기동 직후 상태를 확인한다. nohup 은 실행 실패도 배경으로 밀어 버린다. 잠깐 기다렸다가 살아 있는지 보고 결과를 알려야 운영에서 쓸 수 있다.
ps 에 Z 또는 defunct 로 보이는 것은 이미 끝났는데 부모가 종료 상태를 거두지 않은 프로세스다. 자원을 쓰지 않으므로 그 자체를 죽일 수는 없다.
ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/ {print}'
해결은 부모를 고치는 것이다. 부모가 wait 를 호출하지 않는 코드가 원인이고, 부모를 종료하면 좀비가 init 으로 넘어가 정리된다. 컨테이너 안에서 좀비가 쌓이면 PID 1 이 자식을 거두지 않는 것이므로 --init 을 쓰거나 tini 같은 초기화 프로세스를 넣는다.