- Published on
systemd 서비스가 자꾸 재시작될 때 진단 방법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 리눅스 서버에서 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 quicklyWatchdog 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가 너무 짧아 크래시 루프가 서버 부하를 키움StartLimitBurst와StartLimitIntervalSec가 기본값이라, 반복 실패 시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.targetWants=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를 쓰더라도 “왜 실패했는지”를 남기는 에러 핸들링- 크래시 루프 시 서버를 과부하시키지 않도록
RestartSec와StartLimit...를 적절히 설정
예시로 실패 시 별도 유닛을 호출하는 형태(개념 예시):
[Unit]
OnFailure=your-service-failure@%n.service
이렇게 해두면 “재시작 자체”보다 “첫 실패의 원인”을 놓치지 않게 됩니다.
마무리
systemd 서비스 재시작 문제는 대개 다음 중 하나로 수렴합니다.
- 애플리케이션이 에러로 종료(
status=1등) - OOM/리소스 제한으로 강제 종료(
status=9등) Restart=정책, watchdog, timeout 같은systemd제어 설정 문제- 의존성 준비 전 기동으로 실패 반복
가장 중요한 요령은 systemctl show로 종료 코드와 재시작 정책을 수치로 확인하고, journalctl에서 “systemd가 기록한 실패 이유”와 “애플리케이션 로그”를 같은 시간축으로 맞춰 보는 것입니다. 이 두 축이 맞물리는 순간, 재시작 루프는 대부분 빠르게 끝납니다.