- Published on
systemd 서비스 자동 재시작 무한루프 진단 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 특정 데몬이 “살아있는 듯 보이는데” 실제로는 계속 죽었다가 살아나는 상황이 있습니다. 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가 걸려 있지 않은가?ExecStart가 foreground로 실행되는가? (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-failureRestartSec=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분 안에 원인 좁히기
systemctl cat으로Type/Restart/ExecStart확인systemctl show -p NRestarts,ExecMainStatus,ExecMainCode,Result로 분기journalctl -u -b에서 첫 실패 시점 로그 확보- OOM/Signal/Timeout 여부 확인
WorkingDirectory, 절대경로,Environment차이 제거User/Group권한으로 직접 실행해 재현- 포트/파일 권한/SELinux/AppArmor 확인(환경에 따라)
- 의존성(네트워크/DB) 타이밍 조정
- 재시작 폭주 방지(StartLimit/RestartSec) 적용
- 수정 후
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 로그를 기준으로 분기하면, 감으로 설정을 바꾸는 대신 짧은 시간 안에 원인을 수렴할 수 있습니다.