- Published on
systemd 서비스 자동 재시작 - 죽었다 깨도 복구
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 리눅스 서비스가 한 번 죽으면, 새벽에 알람을 보고 수동으로 systemctl restart 하는 패턴은 오래 못 갑니다. 특히 JVM, Node.js, Python 같은 런타임 기반 서비스는 메모리 단편화, 외부 의존성 타임아웃, 커넥션 누수, 로그 적재 지연 등으로 “가끔” 죽거나 멈추는 일이 반복됩니다.
이 글은 “죽었다 깨도 다시 살아나는” 수준을 목표로, systemd만으로 만들 수 있는 자동 재시작 구성을 단계별로 정리합니다.
- 단순 재시작이 아니라, 비정상 종료, 시그널 종료, 행(hang) 상태까지 다룹니다.
- 무한 재시작 루프를 막기 위해 rate limit 과 백오프(backoff) 를 설계합니다.
- 재시작이 “성공”인지 판단하기 위해 헬스체크 기반 재기동도 함께 넣습니다.
관련해서 장애가 커넥션 고갈이나 누수에서 시작되는 경우가 많으니, 원인 분석은 아래 글도 같이 보면 좋습니다.
systemd 자동 재시작의 핵심: Restart, StartLimit, Timeout
systemd에서 자동 재시작은 유닛 파일의 Service 섹션에서 결정됩니다. 가장 중요한 옵션은 아래 4가지입니다.
Restart=: 어떤 상황에서 재시작할지RestartSec=: 재시작 전 대기 시간(간단한 백오프)StartLimitIntervalSec=/StartLimitBurst=: 일정 시간 내 재시작 횟수 제한(무한 루프 방지)TimeoutStartSec=/TimeoutStopSec=: 시작/종료가 멈췄을 때 강제 처리
Restart= 값은 아래를 많이 씁니다.
on-failure: 비정상 종료(프로세스 exit code 비정상, watchdog timeout, signal 등)일 때만 재시작always: 정상 종료라도 무조건 재시작
대부분의 API 서버나 워커는 정상 종료가 “의도된 종료”가 아닌 경우가 많아서 on-failure 로 시작하고, 정말 절대 죽으면 안 되는 데몬이면 always 를 고려합니다.
기본형: 가장 많이 쓰는 “죽으면 재시작” 유닛
예시로 myapp 이라는 서비스를 만든다고 가정합니다.
/etc/systemd/system/myapp.service
[Unit]
Description=MyApp API Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# 실제 실행 커맨드
ExecStart=/opt/myapp/bin/myapp
# 장애 시 자동 재시작
Restart=on-failure
RestartSec=3s
# 시작/종료가 멈췄을 때 처리
TimeoutStartSec=30s
TimeoutStopSec=20s
# 너무 빨리 죽는 경우(크래시 루프) 차단
StartLimitIntervalSec=60
StartLimitBurst=5
# 로그는 journalctl 로 모으는 구성이 가장 단순
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
적용/실행:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service
여기까지면 “프로세스가 죽으면 다시 띄움”은 됩니다. 하지만 운영에서 자주 마주치는 진짜 문제는 아래 두 가지입니다.
- 프로세스가 죽지 않고 멈춘다 (스레드 데드락, 이벤트 루프 정지, 외부 의존성 영구 대기)
- 죽자마자 바로 재시작을 반복하며 장애 루프가 된다 (설정 오류, DB 다운, 포트 충돌)
이걸 systemd 방식으로 탄탄하게 만들려면 다음 단계가 필요합니다.
“죽진 않았는데 멈춤” 대응 1: watchdog(권장)
systemd는 서비스가 주기적으로 “나 살아있음” 신호를 보내도록 만들 수 있습니다. 이를 WatchdogSec= 로 켜고, 서비스가 제때 ping을 못 하면 systemd가 강제 재시작합니다.
유닛 설정:
[Service]
WatchdogSec=30s
Restart=on-failure
애플리케이션에서 systemd notify를 보내야 합니다.
예시: Bash로 systemd watchdog ping 보내기
애플리케이션이 watchdog를 직접 지원하지 않는다면, 래퍼 스크립트로 우회할 수 있습니다.
/opt/myapp/bin/run-with-watchdog.sh
#!/usr/bin/env bash
set -euo pipefail
# systemd-notify 가 없으면 watchdog는 동작하지 않음
command -v systemd-notify >/dev/null 2>&1 || {
echo "systemd-notify not found" >&2
exec /opt/myapp/bin/myapp
}
# 실제 프로세스 실행(백그라운드)
/opt/myapp/bin/myapp &
APP_PID=$!
# systemd에 준비 완료 신호(선택)
systemd-notify --ready || true
# watchdog ping 루프
while kill -0 "$APP_PID" 2>/dev/null; do
systemd-notify WATCHDOG=1 || true
sleep 10
done
# 앱이 종료되면 exit code 전달
wait "$APP_PID"
유닛에서 ExecStart= 를 이 스크립트로 바꿉니다.
ExecStart=/opt/myapp/bin/run-with-watchdog.sh
WatchdogSec=30s
Restart=on-failure
이 방식은 “프로세스가 살아있기만 하면 ping을 보낸다”는 한계가 있습니다. 즉, 앱이 논리적으로 멈췄는데 프로세스만 살아있으면 ping도 계속 나가서 감지가 안 됩니다. 그래서 다음 방법이 더 실전적입니다.
“죽진 않았는데 멈춤” 대응 2: 헬스체크 실패 시 강제 재시작(실전)
서비스가 HTTP 헬스 엔드포인트를 제공한다면, 외부에서 주기적으로 체크하고 실패 시 systemctl restart 를 실행하는 것이 가장 확실합니다.
핵심은 두 가지입니다.
- 헬스체크는
curl로http://127.0.0.1:PORT/health를 두드린다 - 실패가 몇 번 연속될 때만 재시작한다(일시적 네트워크/GC 튐 방지)
헬스체크 스크립트
/usr/local/bin/myapp-health-restart.sh
#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="myapp.service"
URL="http://127.0.0.1:8080/health"
TIMEOUT_SEC=2
FAIL_THRESHOLD=3
STATE_FILE="/run/myapp-health-fails"
fails=0
if [[ -f "$STATE_FILE" ]]; then
fails=$(cat "$STATE_FILE" || echo 0)
fi
if curl -fsS --max-time "$TIMEOUT_SEC" "$URL" >/dev/null; then
echo 0 > "$STATE_FILE"
exit 0
fi
fails=$((fails+1))
echo "$fails" > "$STATE_FILE"
if [[ "$fails" -ge "$FAIL_THRESHOLD" ]]; then
logger -t myapp-health "healthcheck failed ${fails} times, restarting ${SERVICE_NAME}"
echo 0 > "$STATE_FILE"
systemctl restart "$SERVICE_NAME"
fi
권한 설정:
sudo chmod 755 /usr/local/bin/myapp-health-restart.sh
systemd timer로 10초마다 실행
/etc/systemd/system/myapp-health.timer
[Unit]
Description=MyApp health check timer
[Timer]
OnBootSec=30s
OnUnitActiveSec=10s
AccuracySec=1s
Unit=myapp-health.service
[Install]
WantedBy=timers.target
/etc/systemd/system/myapp-health.service
[Unit]
Description=MyApp health check and restart
[Service]
Type=oneshot
ExecStart=/usr/local/bin/myapp-health-restart.sh
적용:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-health.timer
systemctl list-timers | grep myapp-health
이 구성이 좋은 이유는 명확합니다.
- 프로세스가 살아있어도 헬스가 죽으면 재시작된다
- 헬스 실패가 연속될 때만 재시작해서 오탐을 줄인다
- cron 대신 systemd timer를 쓰니 로그/상태 추적이 쉽다
크래시 루프 방지: StartLimit과 “대기 시간 늘리기”
RestartSec=3s 만으로는 DB 장애 같은 상황에서 재시작 폭풍이 생길 수 있습니다. systemd는 기본적으로 StartLimitIntervalSec 와 StartLimitBurst 로 일정 횟수 초과 시 더 이상 시작을 시도하지 않고 실패 상태로 둡니다.
실전 권장값 예시:
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=300
StartLimitBurst=10
- 5분 동안 10번 이상 죽으면 “지금은 뭔가 근본적으로 잘못됨”으로 보고 멈추게 합니다.
- 멈춘 상태에서 원인(설정, 의존성, 인증서, 포트 충돌)을 해결한 뒤
systemctl reset-failed myapp.service와systemctl start myapp.service로 복구합니다.
sudo systemctl reset-failed myapp.service
sudo systemctl start myapp.service
지수 백오프가 필요하면?
systemd 자체는 지수 백오프를 직접 제공하지 않습니다. 대신 아래 중 하나로 해결합니다.
- 애플리케이션이 의존성이 준비될 때까지 내부적으로 재시도(backoff)하도록 구현
ExecStartPre=에서 의존성 체크 후 실패 시 exit code로 빠져 systemd 재시작에 맡기되,RestartSec를 크게 잡기- 별도 래퍼 스크립트에서 sleep 시간을 늘리는 방식(운영 난이도 증가)
대부분은 RestartSec 와 StartLimit* 조합으로 충분합니다.
종료 신호와 정리: KillSignal, ExecStop, KillMode
서비스가 죽을 때 “깔끔하게” 내려가야 다음 기동이 안전합니다. 예를 들어 포트가 남아있거나, 락 파일이 남거나, 임시 파일이 쌓이면 재시작이 실패합니다.
자주 쓰는 옵션:
KillSignal=SIGTERM
ExecStop=/bin/kill -s SIGTERM $MAINPID
KillMode=mixed
TimeoutStopSec=20s
KillMode=mixed는 메인 프로세스는 정상 종료를 기다리고, 나머지 자식 프로세스는 필요 시 강제 종료하는 쪽으로 동작합니다.- 애플리케이션이 SIGTERM을 받으면 정상 종료하도록 구현돼 있어야 합니다.
재시작이 “성공”인지 판단: ExecStartPost로 검증
프로세스가 떠도, 실제로는 포트 바인딩 실패 등으로 즉시 죽을 수 있습니다. ExecStartPost= 로 기동 직후 헬스 검증을 넣으면 “떴다”가 아니라 “쓸 수 있다”를 기준으로 재시작 루프를 제어할 수 있습니다.
ExecStartPost=/usr/bin/curl -fsS --max-time 2 http://127.0.0.1:8080/health
이 검증이 실패하면 유닛은 실패로 간주되고, Restart=on-failure 에 의해 다시 시도합니다.
관측과 트러블슈팅: journalctl로 재시작 원인 추적
자동 재시작을 걸어두면, “왜 죽었는지”를 놓치기 쉽습니다. 재시작 횟수와 직전 로그를 빠르게 보는 커맨드만 습관화해도 운영 품질이 올라갑니다.
# 최근 로그
journalctl -u myapp.service -n 200 --no-pager
# 오늘 발생한 로그만
journalctl -u myapp.service --since today --no-pager
# 재시작/실패 관련 메시지 필터링
journalctl -u myapp.service | grep -E "Failed|restart|Start request repeated" -n
만약 서비스가 DB 병목이나 락으로 멈추는 계열이라면, 단순 재시작으로 숨기기보다 원인 제거가 먼저입니다. 예를 들어 MySQL 데드락이 원인이라면 아래 글처럼 쿼리 추적부터 들어가는 것이 맞습니다.
운영용 추천 조합(템플릿)
API 서버/워커처럼 “상시 떠 있어야 하는” 서비스 기준으로, 가장 무난한 조합은 아래입니다.
Restart=on-failureRestartSec=5sStartLimitIntervalSec=300,StartLimitBurst=10TimeoutStartSec=30s,TimeoutStopSec=20s- 헬스체크 timer로 10초 내외 주기 점검, 연속 실패 시 재시작
최종 예시 유닛(요약):
[Unit]
Description=MyApp API Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=300
StartLimitBurst=10
TimeoutStartSec=30s
TimeoutStopSec=20s
KillMode=mixed
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
마무리
systemd의 자동 재시작은 단순히 Restart=always 한 줄로 끝내면, 언젠가 “조용히 망가진 채로 계속 재시작만 하는 서버”를 만들기 쉽습니다. 운영에서 진짜 강한 구성은 다음을 동시에 만족해야 합니다.
- 죽으면 올라온다(비정상 종료 대응)
- 안 죽고 멈춰도 복구된다(헬스체크 또는 watchdog)
- 무한 루프는 막는다(StartLimit, 적절한 RestartSec)
- 왜 죽는지는 남긴다(journalctl 기반 관측)
위 템플릿을 기반으로 서비스 특성에 맞게 헬스 기준과 제한 값을 조정하면, “죽었다 깨도 복구되는” 운영 자동화의 최소 단위를 만들 수 있습니다.