- Published on
systemd 서비스 재시작 루프, 10분 디버깅
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 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 를 그대로 실행해보는 겁니다.
ExecStart확인
systemctl show myapp.service -p ExecStart
- 동일 사용자로 실행(유닛에
User=가 있다면 그 계정으로)
sudo -u myapp /usr/local/bin/myapp --config /etc/myapp/config.yaml
여기서 바로 터지는 대표 원인:
- 상대 경로 사용(working directory 차이)
- 환경변수 누락(
PATH,NODE_ENV,DATABASE_URL등) - 권한 부족(로그 디렉터리, 소켓, PID 파일)
- 설정 파일 경로 오타
WorkingDirectory 와 Environment 를 반드시 확인
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 (체크리스트)
ExecStart경로 오타 또는 실행 권한 없음(status=203/EXEC)User=계정/권한 문제(status=217/USER)- 환경변수 누락(
EnvironmentFile경로, 권한, 포맷) WorkingDirectory차이로 상대 경로 실패- 포트 바인딩 실패(이미 사용 중, 권한 부족)
- 의존 서비스 레이스(DB/네트워크/마운트)
- OOM,
MemoryMax제한, 또는 워치독/타임아웃
마무리: 10분 안에 “한 가지 축”으로 좁혀라
systemd 재시작 루프 디버깅은 결국 다음 질문 하나로 수렴합니다.
- 앱이 스스로 죽는가, systemd 가 죽이는가
이 구분만 되면, 필요한 건 보통 1) 마지막 로그 1줄, 2) 종료 코드, 3) 유닛의 재시작 정책입니다. 그 다음은 수동 실행으로 환경 차이를 확인하고, 의존성 레이스나 리소스 제한을 제거하면 됩니다.
더 체계적인 케이스 분류와 심화 예시는 systemd 서비스가 반복 재시작될 때 원인 추적법 에서 확장해서 정리해두었습니다.