Published on

systemd 서비스 재시작 루프, 10분 디버깅

Authors

서버에서 systemctl status 를 보면 서비스가 activating (auto-restart) 로 깜빡이고, CPU는 튀고, 로그는 쏟아지는 상황이 있습니다. 이때 중요한 건 “원인을 전부 다 보려 하지 말고, 10분 안에 재시작 트리거를 하나씩 제거하며 범위를 좁히는 것”입니다.

이 글은 systemd 서비스가 반복 재시작될 때, 최소한의 명령으로 빠르게 원인을 특정하는 디버깅 루틴을 제공합니다. 더 깊은 원인 추적 프레임워크는 systemd 서비스가 반복 재시작될 때 원인 추적법 도 함께 참고하면 좋습니다.

0분: 먼저 “재시작 루프”인지 확인

재시작 루프는 보통 다음 2가지로 나타납니다.

  • 프로세스가 즉시 종료되며 systemd 가 다시 띄움
  • 프로세스는 살아있는데 WatchdogSec 또는 헬스체크 실패로 systemd 가 죽이고 다시 띄움

가장 먼저 상태를 한 줄로 요약해봅니다.

systemctl status myapp.service -n 50 --no-pager

여기서 바로 봐야 할 포인트는 다음입니다.

  • Active: 줄에 auto-restart 가 있는지
  • Main PID: 가 계속 바뀌는지
  • Process: 또는 Main PID 옆에 code=exited, status=... 가 찍히는지

1~2분: 최근 실패 원인만 “정확히” 뽑기

journalctl 을 길게 보면 오히려 시간을 잃습니다. 최근 부팅에서 해당 유닛의 에러만 뽑습니다.

journalctl -u myapp.service -b --no-pager -n 200

재시작이 너무 빠르면 시간 범위를 좁히는 게 더 좋습니다.

journalctl -u myapp.service --since "10 min ago" --no-pager

여기서 찾는 건 딱 3가지입니다.

  • “앱이 남긴 마지막 로그 1줄”
  • systemd 가 찍는 status=... 숫자
  • Killed 또는 Watchdog 같은 강제 종료 흔적

자주 보이는 종료 코드 패턴

  • status=1/FAILURE : 앱이 일반 에러로 종료
  • status=127 : 실행 파일/명령을 못 찾음
  • status=203/EXEC : ExecStart 실행 자체가 실패 (권한, 경로, shebang 등)
  • status=217/USER : User= 계정 문제(존재하지 않음, 권한 부족)
  • status=143 : SIGTERM 으로 종료(외부에서 죽이거나 systemd 가 종료)
  • status=137 : SIGKILL 가능성(메모리 부족 OOM, 강제 kill)

중요: 숫자만 보지 말고 systemctl status 에서 code=status= 를 함께 읽어야 합니다.

2~4분: 유닛 파일에서 “재시작을 유발하는 설정” 확인

서비스가 계속 재시작되는 이유는 앱 버그도 있지만, systemd 설정이 원인을 확대하는 경우가 많습니다.

systemctl cat myapp.service
systemctl show myapp.service -p Restart -p RestartSec -p StartLimitIntervalSec -p StartLimitBurst

1) Restart=always 가 디버깅을 방해하는 경우

장애 상황에서 Restart=always 는 로그를 폭주시켜 원인을 가립니다. 원인 파악을 위해 잠깐 재시작을 끌 수 있습니다.

  • 즉시 멈추기
systemctl stop myapp.service
  • 재시작 비활성화(임시)
systemctl edit myapp.service

아래 내용을 드롭인으로 추가합니다.

[Service]
Restart=no

적용 후:

systemctl daemon-reload
systemctl start myapp.service

이렇게 하면 “한 번만 실행되고 왜 죽는지”를 또렷하게 볼 수 있습니다.

2) StartLimit 에 막혀서 실패처럼 보이는 경우

재시작이 너무 잦으면 systemd 가 스스로 포기합니다.

  • Start request repeated too quickly.

이 메시지가 보이면, 재시작 루프의 원인은 여전히 남아있지만 systemd 가 제한을 건 상태입니다. 이때는 제한을 풀기보다 “왜 즉시 죽는지”를 먼저 잡아야 합니다.

4~6분: ExecStart 를 그대로 복사해 “수동 실행”

systemd 환경과 셸 환경은 다릅니다. 가장 빠른 방법은 ExecStart 를 그대로 실행해보는 겁니다.

  1. ExecStart 확인
systemctl show myapp.service -p ExecStart
  1. 동일 사용자로 실행(유닛에 User= 가 있다면 그 계정으로)
sudo -u myapp /usr/local/bin/myapp --config /etc/myapp/config.yaml

여기서 바로 터지는 대표 원인:

  • 상대 경로 사용(working directory 차이)
  • 환경변수 누락(PATH, NODE_ENV, DATABASE_URL 등)
  • 권한 부족(로그 디렉터리, 소켓, PID 파일)
  • 설정 파일 경로 오타

WorkingDirectoryEnvironment 를 반드시 확인

systemctl show myapp.service -p WorkingDirectory -p Environment -p EnvironmentFile

환경변수는 셸에서 잘 되는데 서비스에서만 실패하는 1순위 원인입니다.

6~8분: “systemd 가 죽이는” 케이스(워치독, 타임아웃, OOM) 구분

앱이 스스로 종료하는지, systemd 가 죽이는지 구분이 핵심입니다.

1) Timeout 으로 죽는 경우

  • TimeoutStartSec 동안 Type=notify 준비 신호를 못 보내거나
  • ExecStartPre 가 오래 걸리거나
  • 초기화가 느린데 기본 타임아웃에 걸리는 경우

확인:

systemctl show myapp.service -p Type -p TimeoutStartUSec -p TimeoutStopUSec

로그에서 Start operation timed out 류 메시지를 찾습니다.

2) Watchdog 으로 죽는 경우

WatchdogSec 을 켰는데 앱이 sd_notify 를 안 보내면 systemd 가 주기적으로 죽입니다.

확인:

systemctl show myapp.service -p WatchdogUSec
journalctl -u myapp.service -b --no-pager | grep -i watchdog

3) OOM 또는 메모리 압박으로 죽는 경우

커널 OOM killer 가 죽이면 exit code 가 137 근처로 보이기도 합니다.

확인:

dmesg -T | grep -i -E "killed process|oom"

컨테이너 환경이라면 cgroup 제한도 확인해야 합니다.

systemctl show myapp.service -p MemoryMax -p MemoryHigh -p CPUQuota

8~9분: 의존성(네트워크, DB, 파일) 레이스 컨디션 제거

서비스가 부팅 직후에만 죽고, 재시작 몇 번 후엔 살아나는 패턴이면 의존성 레이스를 의심합니다.

  • DB 가 아직 안 뜸
  • 네트워크/라우팅 준비 전
  • 마운트가 늦음

유닛 파일에서 다음을 점검합니다.

  • After=
  • Wants=
  • Requires=

예: 네트워크가 준비된 뒤 시작하고 싶다면

[Unit]
After=network-online.target
Wants=network-online.target

단, 이것만으로 “DB 준비 완료”까지 보장되진 않습니다. DB 연결은 애플리케이션 레벨에서 재시도(backoff)하는 것이 더 안전합니다. 이런 재시도 설계는 API/외부 의존성에서도 동일하게 적용되며, 패턴 자체는 Claude API 529·429 재시도 전략과 구현 패턴 글의 백오프/재시도 설계를 참고해도 도움이 됩니다.

9~10분: 재현 가능한 “최소 로그 + 최소 설정”으로 고정

원인을 찾았으면, 다음 장애 대응을 위해 재현 가능한 형태로 남겨야 합니다.

1) 로그를 더 또렷하게 남기기

systemd 저널로만 보지 말고, 표준 출력 로그 레벨을 올려서 원인 1줄을 남깁니다.

예시(드롭인):

[Service]
Environment=LOG_LEVEL=debug

또는 표준 출력이 저널로 들어가도록 확인:

systemctl show myapp.service -p StandardOutput -p StandardError

2) 재시작 정책을 “장애 친화적”으로 조정

무조건 재시작은 장애를 숨길 수 있습니다. 일반적으로는 아래 조합을 권합니다.

[Service]
Restart=on-failure
RestartSec=2s
StartLimitIntervalSec=60
StartLimitBurst=5
  • on-failure 로 정상 종료는 재시작하지 않음
  • RestartSec 로 재시작 폭주 완화
  • StartLimit 로 무한 루프 방지

자주 터지는 원인 Top 7 (체크리스트)

  1. ExecStart 경로 오타 또는 실행 권한 없음(status=203/EXEC)
  2. User= 계정/권한 문제(status=217/USER)
  3. 환경변수 누락(EnvironmentFile 경로, 권한, 포맷)
  4. WorkingDirectory 차이로 상대 경로 실패
  5. 포트 바인딩 실패(이미 사용 중, 권한 부족)
  6. 의존 서비스 레이스(DB/네트워크/마운트)
  7. OOM, MemoryMax 제한, 또는 워치독/타임아웃

마무리: 10분 안에 “한 가지 축”으로 좁혀라

systemd 재시작 루프 디버깅은 결국 다음 질문 하나로 수렴합니다.

  • 앱이 스스로 죽는가, systemd 가 죽이는가

이 구분만 되면, 필요한 건 보통 1) 마지막 로그 1줄, 2) 종료 코드, 3) 유닛의 재시작 정책입니다. 그 다음은 수동 실행으로 환경 차이를 확인하고, 의존성 레이스나 리소스 제한을 제거하면 됩니다.

더 체계적인 케이스 분류와 심화 예시는 systemd 서비스가 반복 재시작될 때 원인 추적법 에서 확장해서 정리해두었습니다.