Published on

systemd 서비스가 자동재시작 안 될 때 7가지 원인

Authors

서버에서 Restart=always 를 걸어놨는데도 서비스가 죽은 채로 멈춰 있으면, 대개 “systemd가 재시작을 못 하는” 게 아니라 “재시작 조건이 충족되지 않거나, 이미 재시작을 포기한 상태”인 경우가 많습니다. 이 글은 systemd가 자동 재시작을 하지 않는 대표 원인 7가지를 로그로 확인하는 방법unit 파일에서 고치는 방법 중심으로 정리합니다.

먼저 공통으로 확인할 기본 진단 명령부터 잡고 들어가겠습니다.

# 서비스 상태와 최근 종료 코드/원인
systemctl status myapp.service -l

# 저널 로그(부팅 이후)
journalctl -u myapp.service -b --no-pager

# 재시작/실패 카운터 포함한 상세 속성
systemctl show myapp.service \
  -p ActiveState -p SubState -p Result \
  -p ExecMainStatus -p ExecMainCode \
  -p NRestarts -p Restart -p RestartUSec \
  -p StartLimitIntervalUSec -p StartLimitBurst

# unit 파일과 drop-in 오버라이드 확인
systemctl cat myapp.service

# systemd가 실제로 어떻게 해석했는지(문법/경로/권한 등)
systemd-analyze verify /etc/systemd/system/myapp.service

1) Restart= 정책이 상황과 안 맞는다

가장 흔한 케이스는 Restart=on-failure 를 써놓고, 프로세스가 “실패”가 아닌 방식으로 종료되는 경우입니다.

자주 헷갈리는 종료 유형

  • 정상 종료: exit code 0 (systemd 관점에서 성공)
  • SIGTERM 등 신호로 종료: systemd 설정에 따라 성공/실패로 분류가 달라질 수 있음
  • 워치독/타임아웃/킬: Result=timeout 또는 Result=signal 등으로 찍힘

Restart=on-failure비정상 종료(실패) 에만 반응합니다. 애플리케이션이 스스로 exit 0 으로 내려가거나, SuccessExitStatus= 로 성공으로 간주되면 재시작이 안 됩니다.

해결

정말 “어떤 이유로든 내려가면 다시 올려야” 한다면 Restart=always 또는 Restart=on-abnormal 를 검토합니다.

[Service]
Restart=always
RestartSec=3

반대로, 특정 exit code는 정상 종료로 보고 재시작하지 않게 하고 싶다면:

[Service]
Restart=on-failure
SuccessExitStatus=0 143
# 143은 SIGTERM(15) 종료를 애플리케이션이 128+15로 반환하는 패턴에서 자주 보임

2) 서비스 타입이 안 맞아 systemd가 “이미 끝났다”고 판단한다

Type= 이 잘못되면 systemd가 프로세스를 추적하지 못해 재시작 조건이 꼬입니다.

대표 증상

  • Type=oneshot 인데 데몬처럼 계속 떠 있어야 하는 서비스
  • Type=forking 인데 실제로 fork 하지 않고 foreground로 도는 프로세스
  • Type=simple 인데 실제 실행은 백그라운드로 보내고 부모가 바로 종료되는 스크립트

예를 들어 ExecStart=/path/run.sh 가 내부에서 nohup ... & 로 백그라운드 실행 후 스크립트가 종료되면, systemd는 메인 프로세스가 끝났다고 보고 상태를 exited 로 둡니다. 이후 죽어도 감지 자체가 안 되니 재시작도 없습니다.

해결

  • 가능하면 애플리케이션을 foreground 로 실행
  • Type=simple 또는 필요 시 Type=exec 사용
[Service]
Type=simple
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
Restart=always

forking 데몬이라면 PIDFile을 정확히 제공해야 합니다.

[Service]
Type=forking
PIDFile=/run/mydaemon.pid
ExecStart=/usr/sbin/mydaemon --pidfile=/run/mydaemon.pid
Restart=on-failure

3) StartLimit* 에 걸려 “재시작 포기” 상태다

systemd는 짧은 시간에 너무 자주 실패하면 재시작을 멈춥니다. 이때 상태는 보통 Start request repeated too quickly 로 찍힙니다.

확인

systemctl status myapp.service -l
systemctl show myapp.service -p StartLimitIntervalUSec -p StartLimitBurst -p NRestarts
journalctl -u myapp.service -b --no-pager | tail -n 200

해결

  • 실패 루프를 끊고(원인 해결) 다시 올리기: systemctl reset-failed myapp.service
  • 정책 조정: StartLimitIntervalSecStartLimitBurst 변경
[Unit]
StartLimitIntervalSec=60
StartLimitBurst=10

[Service]
Restart=on-failure
RestartSec=5

운영에서 무작정 burst를 크게 올리면 “장애를 더 빨리 증폭”시킬 수 있습니다. DB 장애나 외부 의존성 장애로 계속 재시작하면서 커넥션/스레드가 폭증하는 패턴도 흔합니다. 비슷한 장애 전개는 다음 글의 진단 방식이 참고됩니다.

4) RestartPreventExitStatus= 또는 RestartForceExitStatus= 가 의도치 않게 작동한다

systemd는 exit code나 시그널에 따라 “재시작을 막거나/강제”할 수 있습니다. 이전에 누군가 튜닝해둔 값이 의도치 않게 재시작을 차단하는 경우가 있습니다.

확인

systemctl show myapp.service \
  -p RestartPreventExitStatus -p RestartForceExitStatus \
  -p SuccessExitStatus

예시: 특정 종료 코드는 재시작하지 않게 막기

[Service]
Restart=on-failure
RestartPreventExitStatus=2

애플리케이션이 예외 상황에서 exit code 2 를 반환하도록 되어 있다면, 이 설정 때문에 재시작이 안 됩니다.

해결

  • exit code 정책을 문서화하고 서비스 코드와 합의
  • 불필요한 RestartPreventExitStatus= 제거

5) Watchdog/Timeout 설정으로 “죽긴 죽는데 재시작 안 하는 것처럼” 보인다

TimeoutStartSec / TimeoutStopSec / WatchdogSec 는 잘못 설정하면 다음처럼 보일 수 있습니다.

  • 시작이 느린데 TimeoutStartSec 이 너무 짧아 timeout으로 kill
  • stop이 느린데 TimeoutStopSec 이 짧아 SIGKILL로 종료
  • WatchdogSec 을 켰는데 애플리케이션이 notify를 안 해서 watchdog timeout

이 경우 재시작은 시도되지만, 계속 timeout으로 죽어서 StartLimit에 걸려 “어느 순간부터” 재시작이 멈춘 것처럼 보입니다.

확인 포인트

journalctl 에서 timed out , watchdog , killed 키워드를 찾습니다.

journalctl -u myapp.service -b --no-pager | egrep -i 'timeout|watchdog|killed' \
  | tail -n 100

해결 예시

[Service]
TimeoutStartSec=120
TimeoutStopSec=60
Restart=on-failure
RestartSec=5

Type=notify 와 watchdog를 쓸 거면 애플리케이션이 sd_notify 를 호출해야 합니다. Java라면 별도 라이브러리/래퍼가 필요하고, 미구현 상태에서 Type=notify 만 켜면 시작 판정부터 꼬일 수 있습니다.

6) 실제로는 재시작 중인데, 의존성/환경 문제로 즉시 실패한다

재시작이 “안 되는” 게 아니라 “되자마자 또 죽는” 경우입니다. 특히 다음 유형이 흔합니다.

  • 포트가 이미 점유됨: 이전 프로세스가 좀비처럼 남았거나, 다른 프로세스가 사용
  • 파일 디스크립터/메모리 제한: LimitNOFILE , cgroup 메모리 제한
  • 런타임 환경 누락: WorkingDirectory , EnvironmentFile 경로 오류
  • 권한 문제: User= 로 실행 시 접근 불가

확인

# 최근 N회 재시작 흔적
systemctl show myapp.service -p NRestarts

# 종료 코드가 반복되는지
systemctl status myapp.service -l

# 실행 환경/경로
systemctl show myapp.service -p User -p Group -p WorkingDirectory -p EnvironmentFiles

# 리소스 제한
systemctl show myapp.service -p LimitNOFILE -p MemoryMax -p TasksMax

해결 예시: 환경 파일/작업 디렉터리 명시

[Service]
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=on-failure
RestartSec=3

외부 의존성 장애(예: Redis, DB, gRPC)로 인해 애플리케이션이 부팅 직후 종료하는 경우도 많습니다. 이때는 systemd 재시작만으로는 해결이 안 되고, 애플리케이션 레벨에서 backoff/retry, readiness 분리, 타임아웃 정책 정리가 필요합니다.

7) unit 파일 변경 후 daemon-reload 누락, drop-in 충돌, 혹은 다른 매니저가 덮어쓴다

설정을 바꿨는데도 동작이 그대로라면, systemd가 변경을 반영하지 못했거나 다른 설정이 우선 적용된 경우가 있습니다.

흔한 패턴

  • /etc/systemd/system/myapp.service 수정 후 systemctl daemon-reload 안 함
  • systemctl edit myapp.service 로 만든 drop-in 이 원본을 덮어씀
  • 배포 도구/컨테이너/프로세스 매니저가 별도로 기동하여 systemd 서비스와 충돌

확인

# systemd가 보는 최종 unit 구성(원본+drop-in)
systemctl cat myapp.service

# unit 파일 로드 시각/경로 추적
systemctl show myapp.service -p FragmentPath -p DropInPaths

해결 절차(안전한 순서)

# 1) 변경 반영
sudo systemctl daemon-reload

# 2) 실패 상태 초기화(StartLimit에 걸린 경우 특히 중요)
sudo systemctl reset-failed myapp.service

# 3) 재시작
sudo systemctl restart myapp.service

# 4) 재부팅 후에도 자동 시작 확인
sudo systemctl enable myapp.service

실전 점검 체크리스트(한 번에 훑기)

아래 항목만 순서대로 확인해도, 대부분의 “자동 재시작이 안 됨” 이슈는 빠르게 좁혀집니다.

# 1) 재시작 정책과 실패 판정
systemctl show myapp.service -p Restart -p SuccessExitStatus

# 2) 최근 실패 원인
systemctl status myapp.service -l
journalctl -u myapp.service -b --no-pager | tail -n 200

# 3) StartLimit에 걸렸는지
systemctl show myapp.service -p StartLimitIntervalUSec -p StartLimitBurst -p NRestarts

# 4) 타입/추적 문제
systemctl show myapp.service -p Type -p MainPID -p ExecMainStatus -p ExecMainCode

# 5) 최종 unit 구성(오버라이드 포함)
systemctl cat myapp.service

권장 unit 템플릿(웹/백엔드 서비스용)

서비스가 foreground로 실행되고, 실패 시 재시작하며, 과도한 재시작은 완화하는 “무난한” 예시입니다.

[Unit]
Description=MyApp Service
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=6

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
Restart=on-failure
RestartSec=5
TimeoutStartSec=120
TimeoutStopSec=60

# 필요 시 리소스 제한(환경에 맞게 조정)
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

마무리

systemd 자동 재시작이 “안 된다”는 현상은 대부분 다음 중 하나로 귀결됩니다: 재시작 조건 불일치, 프로세스 추적 실패(Type), StartLimit에 의한 포기, 혹은 재시작은 되지만 즉시 같은 이유로 재실패. systemctl showjournalctl종료 코드/Result/재시작 카운터를 먼저 확보하면, 감으로 설정을 바꾸는 시간을 크게 줄일 수 있습니다.