Published on

systemd 서비스 자동 재시작 원인 추적 가이드

Authors

서론

운영 중인 Linux 서버에서 “서비스가 자꾸 다시 살아난다”는 현상은 꽤 흔합니다. 문제는 재시작 자체가 원인이 아니라, 재시작을 유발한 **종료 이벤트(크래시/시그널/타임아웃/Watchdog/OOM 등)**와, 이를 **재시작하도록 만든 정책(systemd의 Restart=, 외부 오케스트레이터, 타이머, 소켓 활성화 등)**이 결합된 결과라는 점입니다.

이 글은 다음 두 질문을 분리해서 답합니다.

  1. 누가/무엇이 재시작을 트리거했는가? (systemd 정책? timer? socket? 외부 supervisor?)
  2. 프로세스는 왜 종료됐는가? (exit code? signal? OOM? timeout? watchdog?)

현장에서 바로 적용할 수 있도록, 확인 순서를 “증거 수집 → 재현/분리 → 원인 확정” 흐름으로 정리합니다.

> 무한 재시작 패턴 자체(대표 원인 7가지)를 먼저 훑고 싶다면: systemd 서비스가 무한 재시작되는 원인 7가지


1) 먼저 ‘재시작’인지 ‘재활성화’인지 구분하기

겉으로는 “restart”처럼 보이지만 실제로는 다음 케이스일 수 있습니다.

  • Restart= 정책으로 systemd가 재시작
  • socket activation으로 요청이 들어올 때마다 새로 기동(서비스가 죽어도 다시 뜸)
  • timer가 주기적으로 start를 호출(죽었다 살아나는 것처럼 보임)
  • 외부 도구(예: cron, supervisor, 배포 스크립트)가 반복 실행

1-1. unit 관계 확인

systemctl cat my.service
systemctl show my.service -p Restart -p RestartSec -p Type -p NotifyAccess
systemctl list-dependencies my.service
systemctl list-unit-files | grep my

특히 아래를 확인하세요.

  • Restart= 값(always, on-failure, on-abnormal, on-watchdog 등)
  • RestartSec= (재시작 간격)
  • StartLimitIntervalSec=, StartLimitBurst= (재시작 폭주 제한)

1-2. socket/timer가 붙어 있는지 확인

systemctl list-units --type=socket --all | grep my
systemctl list-units --type=timer  --all | grep my

# 있으면 상세 확인
systemctl status my.socket
systemctl status my.timer
systemctl cat my.socket
systemctl cat my.timer
  • my.socket이 존재하면, 서비스가 죽어도 다음 연결에서 다시 뜰 수 있습니다.
  • my.timer가 존재하면, 주기적으로 systemctl start my.service가 실행됩니다.

2) 재시작의 ‘직전 종료 원인’을 journal로 고정하기

원인 추적은 “마지막으로 죽은 시점”의 로그를 잡는 게 핵심입니다.

2-1. 최근 부팅 이후 해당 unit 로그만 보기

journalctl -u my.service -b --no-pager

2-2. ‘이번 실행 인스턴스’ 단위로 보기

systemd는 서비스 실행마다 Invocation ID를 남깁니다.

systemctl show my.service -p InvocationID
# 예: InvocationID=0f2f... 라면
journalctl _SYSTEMD_INVOCATION_ID=0f2f... --no-pager

이 방식은 “재시작 루프”에서 로그가 섞이는 문제를 크게 줄여줍니다.

2-3. 종료 코드/시그널을 systemd 관점에서 확인

systemctl status my.service
systemctl show my.service \
  -p ExecMainStatus -p ExecMainCode -p ExecMainPID \
  -p Result -p NRestarts -p StateChangeTimestamp

해석 포인트:

  • ExecMainCode=: exited(정상 종료), killed(시그널로 종료)
  • ExecMainStatus=: exit code 또는 signal number
  • Result=: exit-code, signal, timeout, watchdog, oom-kill

여기서 Result=oom-kill이면 거의 확정적으로 메모리 이슈입니다.


3) systemd가 재시작한 것인지, 외부가 start한 것인지 판별

3-1. systemd 로그에서 “Scheduled restart job” 찾기

journalctl -b -u my.service --no-pager | egrep -i "Scheduled restart|Restarting|Start request repeated"
  • Scheduled restart job, restart counter is ...가 보이면 Restart= 정책으로 재시작된 것입니다.

3-2. 누가 start/stop을 호출했는지(가능한 범위에서) 추적

관리자가 systemctl start를 치거나 배포 도구가 호출한 경우, systemd는 종종 호출자 정보를 남깁니다.

journalctl -b --no-pager | egrep -i "Starting my\.service|Started my\.service|Stopping my\.service|Stopped my\.service"

추가로 auditd가 있다면 훨씬 명확해집니다.

# auditd 설치/활성화되어 있다는 가정
ausearch -m USER_CMD -ts today | grep systemctl

4) 자주 놓치는 종료 원인 6가지와 증거 수집법

4-1. OOM Kill(메모리 부족)로 죽고 재시작

증상:

  • systemctl show에서 Result=oom-kill
  • 커널 로그에 OOM killer 기록
journalctl -k -b --no-pager | egrep -i "oom|killed process|out of memory"

# cgroup 메모리 제한이 있는지
systemctl show my.service -p MemoryMax -p MemoryHigh -p ManagedOOMMemoryPressure

컨테이너/제한 환경이라면 MemoryMax= 또는 상위 slice 제한도 확인하세요.

4-2. Watchdog 타임아웃(헬스 신호 미전송)

WatchdogSec=를 설정했는데 서비스가 sd_notify(WATCHDOG=1)를 보내지 않으면 systemd가 죽이고 재시작할 수 있습니다.

systemctl cat my.service | sed -n '1,200p'
systemctl show my.service -p WatchdogUSec -p Type -p NotifyAccess
journalctl -b -u my.service --no-pager | egrep -i "watchdog|sd_notify"

Type=notify를 쓰는 서비스(예: 일부 Go/Rust 데몬)에서 설정 불일치가 자주 납니다.

4-3. Start/Stop 타임아웃(TimeoutStartSec/TimeoutStopSec)

프로세스가 기동/종료 시 멈추면 systemd가 강제 종료 후 재시작할 수 있습니다.

systemctl show my.service -p TimeoutStartUSec -p TimeoutStopUSec
journalctl -b -u my.service --no-pager | egrep -i "timeout|Timed out"

기동 시 DB 연결을 무한 대기하거나, 종료 시 graceful shutdown이 끝나지 않는 패턴이 흔합니다.

4-4. SIGKILL/SIGTERM 등 외부 시그널

ExecMainCode=killed로 보이고 ExecMainStatus=9(SIGKILL) 같은 값이 나옵니다.

systemctl show my.service -p ExecMainCode -p ExecMainStatus
journalctl -b -u my.service --no-pager | egrep -i "killed|signal|SIG"

원인은 운영자의 kill, 배포 스크립트, OOM killer, 혹은 systemd의 강제 종료(TimeoutStopSec 초과)일 수 있어 “누가 죽였는지”를 추가로 좁혀야 합니다.

4-5. 프로세스는 살아있는데 systemd가 죽었다고 판단(Type=) 불일치

예: Type=forking인데 실제로는 포그라운드로 실행하거나, PIDFile이 잘못되어 systemd가 메인 PID를 못 잡는 경우.

systemctl show my.service -p Type -p PIDFile -p MainPID
ps -ef | grep my-binary

이 경우 재시작이 반복되며 로그는 “Started”가 찍히지만 곧 “Main process exited”가 뒤따르는 형태가 많습니다.

4-6. 애플리케이션 자체 크래시(세그폴트/패닉/언핸들드 예외)

가장 정직한 케이스입니다. systemd는 결과만 보여주고, 실제 크래시 원인은 coredump/스택트레이스가 필요합니다.

# 코어덤프가 systemd-coredump로 수집되는 환경
coredumpctl list my-binary
coredumpctl info my-binary
coredumpctl gdb  my-binary

Node.js, Rust, Java 등 런타임별로 크래시 로그가 다르게 남습니다. 런타임 예외를 다루는 방식은 다른 장애 분석 글과도 접근이 유사합니다(“에러 코드 → 재현 → 원인 고정”). 예를 들어 API 요청 오류를 증거 기반으로 좁혀가는 흐름은 다음 글과도 닮았습니다: OpenAI Responses API 400 invalid_request_error 원인과 해결


5) 재시작 정책을 ‘의도대로’ 만들고, 추적 가능하게 만들기

원인을 잡는 동안에도 서비스가 폭주 재시작하면 로그가 밀리고 서버가 불안정해집니다. 진단 단계에서 다음을 권장합니다.

5-1. 임시로 재시작을 꺼서 원인 로그를 확보

sudo systemctl edit my.service

오버라이드 파일에:

[Service]
Restart=no

적용:

sudo systemctl daemon-reload
sudo systemctl restart my.service

원인 확인 후 원복하세요.

5-2. StartLimit으로 폭주 방지

[Unit]
StartLimitIntervalSec=60
StartLimitBurst=3

[Service]
Restart=on-failure
RestartSec=5
  • “실패하면 재시작”은 유지하되, 1분에 3번 이상 실패하면 멈추게 해서 장애 전파를 줄입니다.

5-3. 로그에 컨텍스트를 남기는 ExecStartPre/ExecStopPost

재시작 원인 추적을 돕기 위해, 기동 직전/종료 직후에 상태를 남길 수 있습니다.

[Service]
ExecStartPre=/bin/sh -c 'echo "[pre] $(date -Is) env=$(env | wc -l)"'
ExecStopPost=/bin/sh -c 'echo "[post] $(date -Is) last_exit=$SERVICE_RESULT"'

$SERVICE_RESULT 같은 environment는 systemd 버전에 따라 제공 범위가 다를 수 있으니, 실제로는 systemctl show와 journal을 함께 쓰는 쪽이 더 안정적입니다.


6) 실전 체크리스트: 10분 안에 결론 내는 순서

운영 장애에서 시간을 아끼려면 아래 순서가 효율적입니다.

  1. systemctl status my.serviceResult / exit code / restart counter 확인
  2. journalctl -u my.service -b죽기 직전 로그 확인
  3. systemctl show ... ExecMainCode/Status/Result종료 형태 확정
  4. journalctl -k -b | grep -i oom으로 OOM 여부 먼저 배제
  5. systemctl list-units --type=timer/socket | grep mytimer/socket 재활성화 배제
  6. coredumpctl info크래시 증거 확보
  7. 필요 시 Restart=no로 잠깐 멈춰 단일 실행 로그 확보

이 과정을 거치면 “systemd가 자꾸 재시작한다”는 모호한 상태에서, 최소한 아래 중 하나로 수렴합니다.

  • 애플리케이션이 특정 exit code로 종료한다(버그/설정/의존성 문제)
  • OOM/Watchdog/Timeout 등 시스템 조건으로 강제 종료된다(리소스/설정 문제)
  • timer/socket/외부 호출로 start가 반복된다(운영 구성 문제)

결론

systemd 서비스 자동 재시작은 ‘현상’이고, 원인은 **종료 원인(Exit/Signal/OOM/Timeout/Watchdog)**과 **재기동 트리거(Restart 정책/Timer/Socket/외부 호출)**의 조합입니다.

가장 중요한 습관은 두 가지입니다.

  • 로그를 섞지 않도록 Invocation ID 단위로 journal을 고정할 것
  • systemctl showResult/ExecMainCode/ExecMainStatus로 종료 형태를 먼저 확정할 것

이 두 가지만 지켜도, 재시작 루프의 “감”이 아니라 “증거”로 원인을 빠르게 좁힐 수 있습니다.