Published on

systemd 서비스가 반복 재시작될 때 원인 추적법

Authors

서버 운영 중 가장 성가신 장애 중 하나가 systemd 서비스가 살아있는 듯 보이는데 계속 재시작되는 현상입니다. 겉으로는 active (running)activating (auto-restart)가 번갈아 보이고, 로그는 순식간에 쌓이며, 원인 파악이 늦어지면 장애가 장기화됩니다.

이 글에서는 “왜 재시작되는가?”를 **systemd가 가진 신호(ExitCode, Restart 정책, StartLimit, watchdog, 타임아웃, 의존성)**로 분해해, 재현 없이도 원인을 좁히는 방법을 단계별로 정리합니다.

> 참고: 분산 환경(EKS 등)에서의 네트워크/엔드포인트/트래픽 문제를 10분 내 진단하는 글도 함께 보면 좋습니다. 예를 들어 Pod는 뜨는데 트래픽이 0인 상황은 systemd 레벨이 아닌 네트워크 정책/보안그룹으로 원인이 이동할 수 있습니다: EKS Pod는 뜨는데 트래픽 0 - NetPol·SG·CNI 10분 진단

1) “재시작”의 종류부터 구분하기

systemd에서 서비스가 다시 뜨는 이유는 크게 두 갈래입니다.

  1. 프로세스가 실제로 죽어서(exit, crash, OOM, signal) systemd가 Restart= 정책에 따라 재기동
  2. 프로세스는 살아있지만 systemd가 watchdog/timeout/health 조건 위반으로 강제 종료 후 재기동

따라서 첫 단계는 “프로세스가 왜 죽었는지” vs “systemd가 왜 죽였는지”를 가르는 것입니다.

2) 즉시 확인해야 할 3종 세트: status / show / journal

아래 3개 명령은 거의 모든 케이스에서 출발점입니다.

# 1) 현재 상태 + 직전 종료 원인 힌트
systemctl status myservice.service -l

# 2) systemd가 알고 있는 상세 필드(ExitCode, Signal, Restart 카운터 등)
systemctl show myservice.service \
  -p ActiveState -p SubState -p Result \
  -p ExecMainPID -p ExecMainStatus -p ExecMainCode \
  -p ExecMainStartTimestamp -p ExecMainExitTimestamp \
  -p NRestarts -p Restart -p RestartUSec \
  -p TimeoutStartUSec -p TimeoutStopUSec \
  -p WatchdogUSec -p StartLimitIntervalUSec -p StartLimitBurst

# 3) 해당 유닛 로그만, 최신부터(원인 메시지 찾기)
journalctl -u myservice.service -b -n 200 --no-pager

해석 포인트

  • ExecMainCode=exited + ExecMainStatus=1 같은 형태면 애플리케이션이 종료코드로 죽음
  • ExecMainCode=killed + ExecMainStatus=9(SIGKILL)면 강제 종료(OOM killer, systemd timeout, 관리자 kill 등) 가능성
  • Result=start-limit-hit이면 StartLimit에 걸려 systemd가 더 이상 재시작을 안 하거나(혹은 잠시 멈춤) 상태가 꼬여 보일 수 있음
  • SubState=auto-restart가 반복되면 Restart= 정책이 실제로 발동하고 있다는 뜻

3) “왜 죽었는지”를 로그에서 10초 안에 찾는 패턴

journalctl -u로 보이는 메시지는 대개 다음 패턴 중 하나로 분류됩니다.

3.1 애플리케이션 예외/패닉/세그폴트

  • 언어 런타임의 stack trace, panic, fatal error
  • segfault at ... 같은 커널 메시지(유닛 로그가 아니라 커널 로그에 찍히는 경우도 있음)

추가로 커널 로그를 같이 확인합니다.

journalctl -k -b -n 200 --no-pager | egrep -i 'oom|killed process|segfault|panic'

3.2 OOM(Out Of Memory)로 죽음

컨테이너가 아닌 VM/베어메탈에서도 OOM killer가 프로세스를 죽이면 systemd는 보통 SIGKILL로 관측합니다.

# OOM 흔적 확인
journalctl -k -b | egrep -i 'oom|out of memory|killed process'

# 서비스의 메모리 제한이 걸려 있는지(유닛/드롭인)
systemctl show myservice.service -p MemoryMax -p MemoryHigh -p TasksMax

유닛에 MemoryMax=가 설정되어 있거나, cgroup 제한이 작으면 “정상 트래픽에서만” 재시작되는 형태로 나타납니다.

3.3 systemd 타임아웃으로 죽음 (TimeoutStartSec/StopSec)

서비스가 시작은 하는데 준비가 늦거나, 종료가 오래 걸리면 systemd가 kill합니다.

  • 로그에 Start operation timed out / stop-sigterm timed out. Killing. 같은 메시지
systemctl show myservice.service -p TimeoutStartUSec -p TimeoutStopUSec -p KillSignal -p SendSIGKILL
journalctl -u myservice.service -b | egrep -i 'timed out|timeout|killing'

대응은 두 방향입니다.

  • 애플리케이션의 준비 시간을 줄이거나(예: DB 연결 재시도 전략 개선)
  • systemd 타임아웃을 현실적으로 늘리기
# /etc/systemd/system/myservice.service.d/override.conf
[Service]
TimeoutStartSec=120
TimeoutStopSec=60

적용:

systemctl daemon-reload
systemctl restart myservice.service

3.4 Watchdog에 걸려 systemd가 재시작

WatchdogSec=가 켜져 있고, 서비스가 sd_notify(WATCHDOG=1)을 주기적으로 보내지 않으면 systemd가 “응답 없음”으로 판단해 재시작합니다.

systemctl show myservice.service -p WatchdogUSec -p NotifyAccess -p Type
journalctl -u myservice.service -b | egrep -i 'watchdog|notify'
  • Type=notify인데 앱이 notify를 안 보내면 반복 재시작 가능
  • WatchdogSec를 끄거나(0), 애플리케이션에서 watchdog notify를 구현해야 합니다.

3.5 잘못된 Restart 조건(정상 종료도 재시작)

가끔은 서비스가 정상 종료(Exit 0) 했는데도 재시작됩니다. 보통 다음 설정 때문입니다.

  • Restart=always
  • Restart=on-success (성공해도 재시작)
systemctl show myservice.service -p Restart -p RestartPreventExitStatus -p RestartForceExitStatus

정상적으로 “한 번 실행 후 종료”가 의도라면 Type=oneshot + RemainAfterExit=yes 등을 고려해야 합니다.

4) StartLimit에 걸리는지 확인하고, 원인-결과를 분리하기

서비스가 너무 빨리 죽으면 systemd는 재시작 폭주를 막기 위해 제한을 겁니다.

systemctl show myservice.service -p StartLimitIntervalUSec -p StartLimitBurst -p Result -p NRestarts
  • Result=start-limit-hit이면 “원인은 죽는 것”이고 “결과는 systemd가 더 이상 재시작하지 않음”입니다.
  • 제한을 완화하는 것은 임시방편일 수 있습니다. 다만 원인 분석 중 로그를 더 확보해야 한다면 유용합니다.
# /etc/systemd/system/myservice.service.d/override.conf
[Unit]
StartLimitIntervalSec=0

[Service]
Restart=on-failure
RestartSec=2

StartLimitIntervalSec=0은 제한을 사실상 끄는 효과가 있어, 진단 단계에서만 권장합니다.

5) ExecStart/WorkingDirectory/User/권한 문제로 즉시 종료되는 케이스

재시작 루프의 흔한 원인이 “실행 파일 경로/권한/환경 변수/작업 디렉토리”입니다.

체크리스트:

  • ExecStart= 경로가 올바른가?
  • User=로 실행할 때 바이너리/설정/로그 디렉토리 권한이 맞는가?
  • WorkingDirectory=가 존재하는가?
  • .envEnvironmentFile= 경로가 맞는가?
systemctl cat myservice.service
systemctl show myservice.service -p ExecStart -p WorkingDirectory -p User -p Group -p Environment -p EnvironmentFiles

# 해당 유저로 직접 실행해보기(가장 확실)
sudo -u myuser -H -- /usr/local/bin/myservice --config /etc/myservice/config.yaml

특히 EnvironmentFile=-/etc/default/myservice처럼 -가 붙어 있으면 파일이 없어도 무시합니다. “환경 변수가 없어서 앱이 즉시 종료”하는 경우가 있으니 주의하세요.

6) 의존성(네트워크/DB/파일시스템) 문제: systemd 관점에서 좁히기

서비스가 외부 의존성(DB, Redis, API, 마운트된 디스크)에 의해 죽는 경우, 애플리케이션 로그에는 “연결 실패”만 남고 systemd는 재시작만 반복합니다.

이때는 의존성 실패를 systemd가 자연스럽게 흡수하도록 유닛을 설계하는 것이 핵심입니다.

6.1 네트워크 준비 전 기동 문제

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

6.2 DB/외부 서비스 준비 대기(ExecStartPre 활용)

[Service]
ExecStartPre=/usr/bin/bash -lc 'for i in {1..30}; do nc -z db.example.com 5432 && exit 0; sleep 1; done; exit 1'
ExecStart=/usr/local/bin/myservice
Restart=on-failure
RestartSec=3

ExecStartPre가 실패하면 서비스는 시작 실패로 처리되고, Restart=on-failure라면 재시작됩니다. 이때 로그에 “어디에서 실패했는지”가 명확히 남습니다.

6.3 파일시스템/마운트 의존

마운트가 늦거나 실패하면 앱이 설정/데이터를 못 읽고 즉시 종료할 수 있습니다.

[Unit]
RequiresMountsFor=/var/lib/myservice
After=local-fs.target

쿠버네티스/EKS 환경에서는 볼륨 마운트 실패가 Pod 레벨에서 더 자주 보이지만, VM에서도 동일한 유형의 원인이 발생합니다. 스토리지 마운트 실패 유형을 빠르게 분류하는 관점은 이 글도 참고할 만합니다: EKS PV MountFailed - wrong fs type 해결법

7) 로그가 부족할 때: systemd/journald 로그 품질 올리기

재시작 루프에서는 “죽기 직전 로그가 없다”가 흔합니다. 아래 설정/방법으로 증거를 남길 수 있습니다.

7.1 StandardOutput/StandardError를 journal로 강제

[Service]
StandardOutput=journal
StandardError=journal

7.2 애플리케이션이 버퍼링 때문에 로그를 못 남길 때

Python/Node/Go 등에서 stdout 버퍼링으로 마지막 로그가 유실될 수 있습니다.

  • Python: PYTHONUNBUFFERED=1
[Service]
Environment=PYTHONUNBUFFERED=1

7.3 core dump 활성화(크래시 분석)

세그폴트/abort가 의심되면 코어덤프를 남기고 coredumpctl로 확인합니다.

coredumpctl list myservice
coredumpctl info myservice
coredumpctl gdb myservice

8) 자주 나오는 “재시작 루프” 원인 TOP 10 요약

  1. 애플리케이션 예외/패닉/세그폴트
  2. OOM killer 또는 MemoryMax 등 cgroup 제한
  3. TimeoutStartSec/TimeoutStopSec에 의한 강제 kill
  4. WatchdogSec + notify 미구현
  5. Restart=always로 정상 종료도 재시작
  6. ExecStart 경로/권한/WorkingDirectory 오류
  7. EnvironmentFile 누락/환경 변수 미설정으로 즉시 종료
  8. 외부 의존성(DB/Redis/API) 연결 실패 후 즉시 종료
  9. 마운트/디스크 의존성 문제
  10. StartLimit에 걸려 상태가 더 악화(원인 은폐)

9) 실전용 미니 런북: 5분 안에 좁히는 순서

S=myservice.service

# 1) 상태/원인 필드
systemctl status $S -l
systemctl show $S -p Result -p ExecMainCode -p ExecMainStatus -p NRestarts -p Restart

# 2) 유닛 로그(최근 200줄)
journalctl -u $S -b -n 200 --no-pager

# 3) 커널 로그(OOM/segfault)
journalctl -k -b -n 200 --no-pager | egrep -i 'oom|killed process|segfault'

# 4) 유닛 정의/드롭인 확인
systemctl cat $S

여기서도 원인이 안 보이면, 다음을 추가합니다.

  • sudo -u <User>로 직접 실행해 동일 증상 재현
  • coredumpctl로 크래시 덤프 확인
  • Timeout*/Watchdog/Restart 값을 현실화(진단 중에는 StartLimit 완화)

10) 마무리: “재시작”은 증상이고, systemd는 단서다

systemd 서비스 재시작 루프는 대부분 **정책(Restart/Timeout/Watchdog/StartLimit)**과 **실제 종료 원인(ExitCode/Signal/OOM/권한/의존성)**을 분리하면 빠르게 해결됩니다. 핵심은 감으로 설정을 바꾸기보다, systemctl show의 필드와 journalctl 로그를 근거로 “누가 죽였는지(앱 vs systemd vs 커널)”를 먼저 확정하는 것입니다.

운영 환경이 쿠버네티스/EKS로 확장되면, 프로세스 재시작이 아니라 네트워크/엔드포인트/스토리지 같은 계층에서 동일한 ‘반복 실패’가 나타나기도 합니다. 그런 경우에는 위의 systemd 런북으로 호스트 레벨을 정리한 뒤, 클러스터 레벨 진단으로 넘어가면 전체 MTTR을 크게 줄일 수 있습니다.