Published on

systemd 서비스 자동 재시작 무한루프 진단 가이드

Authors

서버에서 특정 데몬이 “살아있는 듯 보이는데” 실제로는 계속 죽었다가 살아나는 상황이 있습니다. systemctl status에서는 초록색으로 잠깐 active (running)이 찍히지만, 몇 초 뒤 다시 재시작… 이를 반복하면 CPU/로그가 폭증하고, 의존 서비스까지 연쇄 장애가 날 수 있습니다.

이 글은 systemd 서비스가 자동 재시작 무한루프에 빠졌을 때, 감으로 찍지 않고 증거 기반으로 원인을 좁히는 진단 루틴을 제공합니다. (대부분은 “프로세스가 왜 죽는지” + “systemd가 왜 계속 살리려 하는지” 두 축으로 정리됩니다.)

관련해서 장애 추적/복구 관점의 글이 필요하다면 Azure VM 부팅 불가? Boot Diagnostics로 복구도 함께 참고하면 좋습니다.

1) 우선: 무한루프인지, 의도된 재시작인지 구분

무한루프처럼 보이지만, 실제로는 정상 동작(예: Type=oneshot 작업이 끝나고 종료)일 수 있습니다. 먼저 서비스 타입과 기대 수명을 확인하세요.

systemctl cat myservice.service
systemctl show -p Type,Restart,RestartSec,StartLimitIntervalUSec,StartLimitBurst myservice.service
  • Type=simple/notify: 프로세스가 계속 떠 있어야 정상
  • Type=oneshot: 작업 후 종료가 정상(하지만 Restart=always면 재실행 루프가 됨)

빠른 체크 포인트

  • Type=oneshot인데 Restart=always/on-failure가 걸려 있지 않은가?
  • ExecStartforeground로 실행되는가? (daemonize/fork하면 systemd가 “끝났다”고 판단)

2) 상태/로그로 “왜 죽는지”부터 잡기

2-1) status에서 핵심만 읽기

systemctl status myservice.service --no-pager

여기서 봐야 할 것:

  • Main PID가 바뀌는지(재시작 반복 증거)
  • code=exited, status=... 혹은 code=killed, signal=...
  • Result: exit-code / Result: signal / Result: watchdog

2-2) journalctl로 재시작 구간만 뽑기

journalctl -u myservice.service -b --no-pager
# 최근 5분만
journalctl -u myservice.service --since "5 min ago" --no-pager
# 재시작 관련 메시지 필터링
journalctl -u myservice.service -b | egrep -i "failed|exit|killed|restart|start-limit|watchdog"

status=1/FAILURE 같은 애매한 종료코드는 결국 애플리케이션 로그/스택트레이스를 봐야 합니다. 반면 signal=SEGV, OOMKilled, watchdog는 systemd/커널 레벨 단서가 강합니다.

3) systemd가 “계속 살리는” 원인: Restart/StartLimit 이해

무한루프의 80%는 아래 조합입니다.

  • Restart=always 또는 Restart=on-failure
  • RestartSec=0 또는 너무 짧음
  • StartLimit*이 충분히 크거나(혹은 기본값으로) 제한이 안 걸림

3-1) StartLimit에 걸리는지 확인

systemctl status myservice.service --no-pager
# 또는
systemctl show myservice.service -p NRestarts,ExecMainStatus,ExecMainCode,Result

StartLimit에 걸리면 보통 이런 메시지가 보입니다.

  • Start request repeated too quickly.
  • Failed with result 'start-limit-hit'.

3-2) 진단 중에는 잠시 “루프를 멈추는” 게 이득

원인 분석 전에 재시작 폭주부터 끊으세요.

# 즉시 중지
sudo systemctl stop myservice.service

# 재시작 방지(마스킹)
sudo systemctl mask myservice.service
# 해제
sudo systemctl unmask myservice.service

또는 임시로 drop-in으로 Restart를 꺼도 됩니다.

sudo systemctl edit myservice.service
# /etc/systemd/system/myservice.service.d/override.conf
[Service]
Restart=no

적용:

sudo systemctl daemon-reload
sudo systemctl start myservice.service

4) Exit code로 원인 분류(가장 빠른 분기점)

4-1) ExecMainStatus/Code 확인

systemctl show myservice.service -p ExecMainStatus,ExecMainCode,ExecMainStartTimestamp,ExecMainExitTimestamp
  • ExecMainCode=exited + ExecMainStatus=1 : 앱이 1로 종료(설정/인자/환경 문제 가능성)
  • ExecMainCode=killed + Status=9 : SIGKILL(대개 OOM/관리자가 kill)

4-2) OOMKilled 여부 확인

journalctl -u myservice.service -b | egrep -i "oom|killed process|out of memory"
# cgroup 관점
systemctl show myservice.service -p MemoryCurrent,MemoryMax,CPUUsageNSec

컨테이너처럼 MemoryMax가 걸려 있으면, 서비스는 정상인데 메모리 제한으로 커널이 죽이고 systemd가 다시 살리는 패턴이 됩니다.

5) 흔한 설정 실수: Type/daemonize/forking 불일치

5-1) 백그라운드로 떠버리는 프로그램

프로그램이 자체적으로 daemonize하면 systemd는 ExecStart가 끝난 것으로 판단해 종료/재시작 루프가 납니다.

해결 방향:

  • 가능하면 foreground 옵션 사용
  • 또는 Type=forking + PIDFile= 정확히 지정

예: Nginx는 보통 daemon off;로 foreground 실행.

[Service]
Type=simple
ExecStart=/usr/sbin/nginx -g 'daemon off;'
Restart=on-failure

5-2) notify 타입인데 READY 신호가 안 옴

Type=notify는 프로세스가 sd_notify(READY=1)를 보내야 합니다. 못 보내면 systemd가 타임아웃으로 실패 처리 후 재시작할 수 있습니다.

systemctl show myservice.service -p Type,TimeoutStartUSec

의심되면 일단 Type=simple로 바꿔 원인 분리(애플리케이션 vs systemd 연동)를 하기도 합니다.

6) ExecStart 명령/인자/경로 문제(특히 상대경로)

systemd 유닛은 기본 작업 디렉터리가 /인 경우가 많습니다. 로컬에서 잘 되던 상대경로가 서비스에서는 깨집니다.

6-1) WorkingDirectory 지정

[Service]
WorkingDirectory=/opt/myservice
ExecStart=/opt/myservice/bin/myservice --config ./config.yaml

6-2) PATH/환경변수 차이

인터랙티브 쉘에서 되던 명령이 systemd에서는 PATH가 달라 실패합니다. 절대경로를 쓰거나 Environment=를 명시하세요.

[Service]
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
EnvironmentFile=-/etc/default/myservice
ExecStart=/usr/local/bin/myservice

진단 팁: ExecStartPre=/usr/bin/env를 잠깐 넣어 환경을 로그로 찍어볼 수 있습니다.

[Service]
ExecStartPre=/usr/bin/env
ExecStart=/usr/local/bin/myservice

7) 권한/파일/포트 바인딩 문제: User/Group과 Capability

서비스가 루트가 아닌 사용자로 실행될 때 가장 흔한 실패가 다음입니다.

  • 설정 파일/로그 디렉터리 쓰기 권한 없음
  • 1024 미만 포트 바인딩 실패
  • 소켓/디바이스 접근 불가

7-1) 권한 문제 빠르게 재현

# 유닛의 실행 사용자 확인
systemctl show myservice.service -p User,Group

# 그 사용자로 직접 실행
sudo -u myuser -s
/opt/myservice/bin/myservice --config /etc/myservice/config.yaml

7-2) 80/443 같은 privileged port 바인딩

루트로 띄우지 않으려면 capability 부여를 고려합니다.

sudo setcap 'cap_net_bind_service=+ep' /opt/myservice/bin/myservice
getcap /opt/myservice/bin/myservice

또는 systemd에서:

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

8) 의존성/네트워크 준비 전 기동: After/Wants/Requires

재시작 루프가 “앱이 잘못”이 아니라 부팅 타이밍 문제인 경우가 많습니다. 예: DB가 아직 안 떴는데 앱이 바로 접속하다 실패→종료→재시작.

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

# DB가 같은 호스트의 systemd 서비스라면
Requires=postgresql.service
After=postgresql.service

네트워크가 준비되기 전에 뜨는 문제는 클라우드/쿠버네티스에서도 자주 보이는 패턴인데, 네트워크/로드밸런서 레벨 추적 관점은 EKS에서 Pod egress만 502? Envoy/NLB 추적기 같은 글의 접근법(관측 지점 늘리기)이 systemd에서도 그대로 통합니다.

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

원인을 고쳤더라도, 재발 시 피해를 줄이려면 재시작 정책을 안전하게 잡아두는 게 좋습니다.

9-1) 권장 패턴 예시

[Service]
Restart=on-failure
RestartSec=3s

# 폭주 방지
StartLimitIntervalSec=60
StartLimitBurst=5

# 종료 시그널/타임아웃
TimeoutStopSec=20
KillSignal=SIGTERM
  • Restart=always는 정말 필요한 경우에만(프로세스가 “정상 종료”해도 다시 떠야 하는 워커 등)
  • RestartSec를 0으로 두면 장애 시 로그/CPU 폭주가 빨라집니다.

9-2) Watchdog 사용 시

워치독은 유용하지만, 설정이 어긋나면 “정상인데도 watchdog timeout으로 죽고 재시작”이 됩니다.

systemctl show myservice.service -p WatchdogUSec
journalctl -u myservice.service -b | egrep -i "watchdog"

10) 실전 체크리스트: 10분 안에 원인 좁히기

  1. systemctl cat으로 Type/Restart/ExecStart 확인
  2. systemctl show -p NRestarts,ExecMainStatus,ExecMainCode,Result로 분기
  3. journalctl -u -b에서 첫 실패 시점 로그 확보
  4. OOM/Signal/Timeout 여부 확인
  5. WorkingDirectory, 절대경로, Environment 차이 제거
  6. User/Group 권한으로 직접 실행해 재현
  7. 포트/파일 권한/SELinux/AppArmor 확인(환경에 따라)
  8. 의존성(네트워크/DB) 타이밍 조정
  9. 재시작 폭주 방지(StartLimit/RestartSec) 적용
  10. 수정 후 systemctl daemon-reload && systemctl restart 및 재발 여부 관찰

부록) 최소 재현용 디버그 유닛 템플릿

문제를 분리하기 위해, 애플리케이션 대신 쉘로 환경/경로를 출력하고 일부러 실패시키는 유닛을 만들어 “systemd 레벨 동작”을 확인할 수 있습니다.

# /etc/systemd/system/myservice-debug.service
[Unit]
Description=MyService Debug Unit
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myuser
WorkingDirectory=/opt/myservice
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
ExecStart=/bin/bash -lc 'echo "PWD=$PWD"; whoami; env | sort; /opt/myservice/bin/myservice --config ./config.yaml'
Restart=on-failure
RestartSec=2
StartLimitIntervalSec=30
StartLimitBurst=3

[Install]
WantedBy=multi-user.target

적용/확인:

sudo systemctl daemon-reload
sudo systemctl start myservice-debug.service
sudo journalctl -u myservice-debug.service -b --no-pager

이렇게 “systemd가 제공하는 실행 환경”을 눈으로 확인하면, 로컬에서 되던 것이 서비스에서만 실패하는 이유(경로/권한/환경변수)가 대부분 드러납니다.


무한 재시작 루프는 결국 (1) 프로세스가 죽는 직접 원인(2) systemd 재시작 정책/타이밍의 조합입니다. 위 절차대로 ExecMainStatus, Result, journal 로그를 기준으로 분기하면, 감으로 설정을 바꾸는 대신 짧은 시간 안에 원인을 수렴할 수 있습니다.