Published on

systemd 서비스가 자꾸 재시작될 때 진단 방법

Authors

운영 중인 리눅스 서버에서 systemd 서비스가 갑자기 죽고 다시 살아나는 현상은 흔하지만, 원인은 매우 다양합니다. 애플리케이션 예외로 즉시 종료될 수도 있고, systemd 설정(Restart=) 때문에 “죽으면 무조건 살린다” 정책이 과하게 적용된 것일 수도 있습니다. 또는 OOM(메모리 부족)으로 커널이 프로세스를 강제 종료했는데, 겉으로는 단순 재시작처럼 보이기도 합니다.

이 글은 “왜 재시작되는지”를 가장 짧은 시간에 좁혀가는 진단 순서를 제공합니다. 단순히 로그 한 줄 보는 수준이 아니라, systemd가 판단한 종료 원인, 종료 코드, 시그널, 재시작 정책, 리소스 한도, 의존성, 헬스체크 설계까지 함께 점검합니다.

문제 상황이 유사하다면, 더 확장된 체크리스트는 다음 글도 함께 참고할 수 있습니다.

1) 먼저 “재시작”인지 “재기동”인지 구분하기

systemd가 서비스를 재시작하는 경우에도 형태가 다릅니다.

  • 프로세스가 크래시하거나 정상 종료했는데 Restart= 정책으로 다시 띄움
  • WatchdogSec= 타임아웃으로 systemd가 죽이고 다시 띄움
  • 외부(관리자, 배포 스크립트, 오토스케일러)가 systemctl restart를 반복 실행
  • OOM Killer가 프로세스를 죽임(서비스는 죽었으니 systemd가 다시 띄움)

가장 먼저 아래로 “최근 재시작 흔적”을 확인합니다.

systemctl status -l --no-pager your.service
systemctl show your.service -p NRestarts -p ExecMainStatus -p ExecMainCode -p ExecMainPID
  • NRestarts가 빠르게 증가하면 systemd가 반복 재시작 중일 확률이 큽니다.
  • ExecMainCode는 종료 유형(예: exited, killed, dumped)을 보여줍니다.
  • ExecMainStatus는 종료 코드 또는 시그널 번호를 보여줍니다.

자주 보는 패턴

  • code=exited, status=1/FAILURE: 애플리케이션이 에러로 종료
  • code=killed, status=9/KILL: 누군가 SIGKILL로 강제 종료했거나, systemd가 타임아웃으로 죽였거나, OOM 상황일 수 있음
  • status=143: 보통 SIGTERM(15)로 종료된 경우가 많음(예: 128+15)

2) journalctl로 “systemd 관점”과 “프로세스 관점” 로그를 함께 보기

서비스 단위 로그를 시간 역순으로 확인합니다.

journalctl -u your.service --no-pager -n 200

부팅 이후 전체에서 해당 유닛만 따라가며, 재시작이 시작된 시점을 찾습니다.

journalctl -u your.service --since "1 hour ago" --no-pager

핵심은 다음 문구들입니다.

  • Main process exited, code=..., status=...
  • Scheduled restart job, restart counter is ...
  • Start request repeated too quickly
  • Watchdog timeout

systemd가 “왜 다시 시작했는지”를 보려면, 유닛 로그뿐 아니라 PID 1(systemd) 로그도 함께 보는 게 좋습니다.

journalctl -t systemd --since "1 hour ago" --no-pager

여기서 타임아웃, watchdog, 시작 제한(StartLimitIntervalSec, StartLimitBurst) 관련 메시지가 더 명확히 보이는 경우가 많습니다.

3) unit 파일에서 Restart 정책과 타이밍을 확인하기

재시작 루프는 애플리케이션 문제가 아니라 systemd 정책 문제인 경우도 꽤 있습니다. 현재 적용 중인 설정을 확인합니다.

systemctl cat your.service
systemctl show your.service \
  -p Restart -p RestartSec -p StartLimitIntervalSec -p StartLimitBurst \
  -p TimeoutStartSec -p TimeoutStopSec -p WatchdogSec

Restart 관련 자주 발생하는 함정

  • Restart=always인데 프로세스가 “정상 종료”해도 다시 뜸
  • Restart=on-failure인데 애플리케이션이 종료 코드를 0이 아닌 값으로 반환
  • RestartSec가 너무 짧아 크래시 루프가 서버 부하를 키움
  • StartLimitBurstStartLimitIntervalSec가 기본값이라, 반복 실패 시 Start request repeated too quickly로 아예 멈춤

운영에서 흔히 쓰는 완충 설정 예시는 다음과 같습니다.

[Service]
Restart=on-failure
RestartSec=3
StartLimitIntervalSec=60
StartLimitBurst=10

재시작을 “덜 하게” 만드는 것이 목표가 아니라, 원인 파악 중에 서버가 과부하로 무너지는 것을 막는 안전장치로도 유용합니다.

4) 종료 원인이 OOM Killer인지 확인하기

서비스가 status=9/KILL로 죽는다면 OOM을 의심해야 합니다. 커널 로그에서 OOM 흔적을 찾습니다.

journalctl -k --since "1 hour ago" --no-pager | grep -i -E "oom|killed process"

또는 다음처럼 전체 로그에서 OOM만 좁힙니다.

journalctl --since "1 hour ago" --no-pager | grep -i "oom"

OOM이면 애플리케이션 메모리 누수, 과도한 캐시, 컨테이너/서비스 제한(MemoryMax=) 설정, 동시성 급증 등이 원인일 수 있습니다. systemd 리소스 제한을 쓰고 있다면 다음도 확인합니다.

systemctl show your.service -p MemoryMax -p MemoryHigh -p TasksMax -p CPUQuota

5) 파일 디스크립터/프로세스 수 제한(ulimit)으로 죽는지 확인하기

재시작 루프의 흔한 원인 중 하나가 FD 고갈입니다. 로그에 Too many open files가 보이거나, 특정 시점부터 네트워크/파일 IO가 실패하면서 종료되는 경우가 많습니다.

이 경우 아래 글도 함께 보면 원인 분석이 빨라집니다.

systemd 서비스의 FD 제한은 LimitNOFILE로 결정될 수 있습니다.

systemctl show your.service -p LimitNOFILE

unit 파일에서 상향하는 예시:

[Service]
LimitNOFILE=1048576

주의할 점은, 값만 올려도 애플리케이션(예: DB 커넥션 풀, 워커 수)이 폭증하면 근본 해결이 아닙니다. “왜 FD가 늘었는지”를 함께 봐야 합니다.

6) ExecStart 경로/권한/환경변수 문제로 즉시 종료하는지 확인하기

서비스가 시작하자마자 바로 종료된다면, 아래를 우선 점검합니다.

  • ExecStart= 경로 오타
  • 실행 권한 부족
  • User=로 실행하는데 해당 사용자가 파일 접근 권한이 없음
  • WorkingDirectory=가 존재하지 않음
  • EnvironmentFile=이 누락되었거나 파싱 실패

systemd는 종종 “실행은 되었는데 즉시 종료” 상황을 재시작으로 보이게 만듭니다. 다음 명령으로 실행 커맨드를 정확히 확인하세요.

systemctl show your.service -p ExecStart -p User -p Group -p WorkingDirectory -p Environment -p EnvironmentFiles

그리고 동일한 사용자로 직접 실행해 즉시 재현하는 것이 가장 빠릅니다.

sudo -u youruser -H bash -lc '/path/to/your-binary --your-args'

7) Type=forking, PIDFile, notify 설정 불일치 점검

systemd 타입 설정이 실제 프로세스 동작과 맞지 않으면, 서비스가 “죽었다고 오판”되어 재시작될 수 있습니다.

  • Type=simple: 기본값. 포그라운드 프로세스가 계속 살아있어야 함
  • Type=forking: 데몬이 포크 후 부모가 종료되는 형태. PIDFile=이 정확해야 함
  • Type=notify: 애플리케이션이 sd_notify로 준비 완료를 알려야 함

Type=notify인데 애플리케이션이 notify를 보내지 않으면, TimeoutStartSec 이후 실패로 처리되어 재시작 루프가 생길 수 있습니다.

확인:

systemctl show your.service -p Type -p PIDFile -p NotifyAccess -p TimeoutStartUSec

8) WatchdogSec으로 인해 systemd가 주기적으로 죽이는지 확인

WatchdogSec을 설정해두고 애플리케이션이 watchdog ping을 보내지 않으면, systemd가 프로세스를 강제 종료하고 재시작합니다. 로그에는 보통 Watchdog timeout이 찍힙니다.

systemctl show your.service -p WatchdogSec
journalctl -u your.service --since "2 hours ago" --no-pager | grep -i watchdog

해결은 둘 중 하나입니다.

  • 애플리케이션이 sd_notify 기반 watchdog ping을 보내도록 구현
  • watchdog 기능이 필요 없다면 WatchdogSec=0 또는 설정 제거

9) TimeoutStartSec, TimeoutStopSec로 인해 강제 종료되는지 확인

애플리케이션이 느리게 뜨거나, 종료가 오래 걸리면 systemd가 타임아웃으로 죽일 수 있습니다. 이 경우 재시작처럼 보입니다.

systemctl show your.service -p TimeoutStartUSec -p TimeoutStopUSec
journalctl -u your.service --since "1 hour ago" --no-pager | grep -i -E "timeout|killed"

예를 들어 초기 마이그레이션, 캐시 워밍, 외부 의존성 대기 등으로 시작이 오래 걸린다면 TimeoutStartSec을 늘리거나, 준비 완료 시점을 더 정확히 설계(Type=notify)하는 게 좋습니다.

10) 의존성 문제: 네트워크/DB 준비 전 기동으로 실패 반복

재시작 루프의 현실적인 원인 중 하나는 “의존 서비스가 아직 준비되지 않았는데 먼저 뜨려다 실패”입니다. 특히 부팅 직후나 배포 직후에 자주 발생합니다.

unit 파일에서 다음을 검토합니다.

  • After=network-online.target
  • Wants=network-online.target
  • DB나 메시지 큐 등은 systemd 타겟만으로 보장되지 않으므로, 애플리케이션 레벨 재시도(backoff)가 필요

간단한 예:

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

[Service]
Restart=on-failure
RestartSec=5

핵심은 systemd 의존성만으로 “외부 시스템이 준비됨”을 보장하기 어렵다는 점입니다. 애플리케이션에 지수 백오프 재시도, 회로 차단기, 준비 상태 체크를 넣는 편이 더 견고합니다.

11) 빠르게 원인을 좁히는 “현장용” 명령 모음

아래 순서대로 실행하면 대부분의 케이스에서 원인이 빠르게 드러납니다.

# 1) 현재 상태와 최근 실패 이유
systemctl status -l --no-pager your.service

# 2) 종료 코드/시그널과 재시작 횟수
systemctl show your.service -p NRestarts -p ExecMainCode -p ExecMainStatus -p Result

# 3) 유닛 로그
journalctl -u your.service --since "30 min ago" --no-pager

# 4) 커널(OOM 등)
journalctl -k --since "30 min ago" --no-pager | grep -i -E "oom|killed process"

# 5) unit 설정(재시작/타임아웃/워치독)
systemctl show your.service -p Restart -p RestartSec -p TimeoutStartUSec -p TimeoutStopUSec -p WatchdogSec

12) 재발 방지: 실패를 “조용히 재시작”하지 말고 관측 가능하게 만들기

서비스가 계속 재시작되는데도 한동안 아무도 모르는 경우가 많습니다. 다음을 권장합니다.

  • OnFailure=로 실패 시 알림/덤프 수집 유닛 실행
  • 로그에 종료 직전 핵심 상태(메모리 사용량, 연결 수, 큐 길이)를 남기기
  • Restart=on-failure를 쓰더라도 “왜 실패했는지”를 남기는 에러 핸들링
  • 크래시 루프 시 서버를 과부하시키지 않도록 RestartSecStartLimit...를 적절히 설정

예시로 실패 시 별도 유닛을 호출하는 형태(개념 예시):

[Unit]
OnFailure=your-service-failure@%n.service

이렇게 해두면 “재시작 자체”보다 “첫 실패의 원인”을 놓치지 않게 됩니다.

마무리

systemd 서비스 재시작 문제는 대개 다음 중 하나로 수렴합니다.

  • 애플리케이션이 에러로 종료(status=1 등)
  • OOM/리소스 제한으로 강제 종료(status=9 등)
  • Restart= 정책, watchdog, timeout 같은 systemd 제어 설정 문제
  • 의존성 준비 전 기동으로 실패 반복

가장 중요한 요령은 systemctl show로 종료 코드와 재시작 정책을 수치로 확인하고, journalctl에서 “systemd가 기록한 실패 이유”와 “애플리케이션 로그”를 같은 시간축으로 맞춰 보는 것입니다. 이 두 축이 맞물리는 순간, 재시작 루프는 대부분 빠르게 끝납니다.