- Published on
systemd 서비스 재시작 루프 10분 진단 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 systemctl status를 봤는데 Active: activating (auto-restart)가 반복되고, 로그는 짧게 끊기며, 서비스는 살아나지 않는 상황은 흔합니다. 이 글은 “왜 재시작하는지”를 10분 내에 좁히기 위한 순서형 체크리스트입니다. 핵심은 (1) systemd가 재시작을 유발한 직접 원인(Exit code/Signal/OOM 등)을 먼저 잡고, (2) unit 파일의 재시작 정책과 런타임 환경(권한/경로/의존성/리소스)을 확인하는 것입니다.
0) 먼저 알아야 할 systemd 재시작 루프의 패턴
systemd에서 재시작이 반복되는 대표 패턴은 다음 중 하나입니다.
- 프로세스가 즉시 종료: 설정/환경/파일 경로/권한 오류로
ExecStart가 곧장 실패 - 신호로 종료:
SIGSEGV(크래시),SIGKILL(OOM 또는 강제 kill),SIGABRT등 - 타임아웃:
TimeoutStartSec/TimeoutStopSec초과 - Type 설정 불일치:
Type=forking/notify/simple가 실제 동작과 안 맞아 systemd가 “실패”로 판단 - StartLimit: 짧은 시간에 너무 자주 실패해
Start request repeated too quickly로 막힘
이제 10분 진단 루틴으로 들어갑니다.
1분: 현재 상태와 마지막 실패 이유를 한 번에 확인
가장 먼저 “systemd가 왜 실패로 판단했는지”를 확인합니다.
sudo systemctl status -l --no-pager myservice.service
sudo systemctl show myservice.service \
-p ActiveState -p SubState -p Result \
-p ExecMainPID -p ExecMainStatus -p ExecMainCode -p ExecMainStartTimestamp
ExecMainCode=exited+ExecMainStatus=1같은 형태면 프로그램이 에러 코드로 종료한 겁니다.ExecMainCode=killed면 시그널로 종료된 겁니다. 특히status=9/KILL이면 OOM 또는 외부 kill 가능성이 큽니다.Result=exit-code,Result=signal,Result=timeout같은 결과는 다음 단계(로그/타임아웃/리소스)로 가는 분기점입니다.
2~3분: journalctl로 “한 번의 실패”를 정확히 재현해 보기
재시작 루프에서 흔한 함정은 로그가 계속 덮여서 원인이 안 보이는 겁니다. 부팅 이후 전체가 아니라, 서비스 단위로 최근 실패만 봅니다.
# 최근 200줄
sudo journalctl -u myservice.service -n 200 --no-pager
# 이번 부팅에서의 로그(재부팅 후 혼선 방지)
sudo journalctl -u myservice.service -b --no-pager
# 실패한 시점 주변을 시간으로 좁히기
sudo journalctl -u myservice.service --since "10 min ago" --no-pager
여기서 확인할 포인트:
No such file or directory→ 경로/WorkingDirectory/환경변수/상대경로 문제Permission denied→ User/Group, 파일 권한, SELinux/AppArmor, Capability 문제Address already in use→ 포트 충돌(다른 프로세스가 이미 바인딩)Exec format error→ 잘못된 아키텍처 바이너리 또는 shebang 문제Killed process ... out of memory또는oom-kill→ OOM
OOM이 의심되면 컨테이너 환경에서의 OOMKilled 진단 흐름과도 유사합니다. 원리(커널이 메모리 부족으로 프로세스를 kill)는 같기 때문에, 메모리/누수 관점은 이 글도 참고가 됩니다: Kubernetes OOMKilled 진단과 메모리 누수 추적 실전
3~4분: unit 파일과 drop-in override를 “실제로 적용된 값” 기준으로 확인
/etc/systemd/system/...만 보고 판단하면 틀릴 수 있습니다. drop-in override(/etc/systemd/system/myservice.service.d/*.conf)가 우선 적용되기 때문입니다. 반드시 최종 렌더링된 설정을 확인합니다.
# 최종 적용된 unit 내용 확인
sudo systemctl cat myservice.service
# 런타임 속성 전체 덤프(필터링해서 보는 걸 추천)
sudo systemctl show myservice.service | less
특히 다음 항목은 재시작 루프와 직결됩니다.
ExecStart=경로가 실제로 존재하는가?WorkingDirectory=가 맞는가? (상대경로 사용 시 치명적)User=/Group=가 접근해야 하는 파일/소켓/포트 권한을 가지는가?Environment=/EnvironmentFile=경로가 존재하는가?Restart=정책이 과도하지 않은가? (always로 해두면 즉시 실패도 무한 반복)StartLimitIntervalSec=/StartLimitBurst=로 인해 차단된 건 아닌가?
4~6분: “재시작을 멈추고” 단발 실행으로 원인을 드러내기
무한 재시작은 원인 분석을 방해합니다. 잠깐 재시작을 멈추고, foreground 실행/단발 실행으로 에러를 노출시키는 게 빠릅니다.
방법 A: 임시로 Restart 정책 끄기(드롭인)
sudo systemctl edit myservice.service
아래를 입력:
[Service]
Restart=no
적용:
sudo systemctl daemon-reload
sudo systemctl reset-failed myservice.service
sudo systemctl start myservice.service
sudo systemctl status -l myservice.service
방법 B: systemd-run으로 동일 환경에 가깝게 실행
서비스가 User=...로 실행된다면 그 사용자로 실행해 봐야 합니다.
sudo systemd-run --unit=myservice-debug \
--property=User=myuser \
--property=WorkingDirectory=/opt/myapp \
/opt/myapp/bin/myservice --config /etc/myservice/config.yml
sudo journalctl -u myservice-debug -n 200 --no-pager
이 방식은 “셸에서 잘 되는데 서비스로는 실패” 같은 케이스를 빠르게 잡는 데 유효합니다(환경변수/작업 디렉터리/권한 차이).
6~8분: 흔한 원인 Top 6 빠른 체크
여기서부터는 로그에서 단서가 약할 때 빠르게 훑는 체크입니다.
1) Type 불일치로 인한 오판
- 데몬이 포그라운드로 도는 프로그램인데
Type=forking이면 systemd가 PID 추적을 실패할 수 있습니다. - 반대로 백그라운드로 포크하는데
Type=simple이면 “시작 직후 종료”처럼 보일 수 있습니다.
검증:
sudo systemctl show myservice.service -p Type -p MainPID -p ExecMainPID
2) 포트 충돌
sudo ss -lntp | grep -E ":(80|443|8080|9000)\b"
# 또는
sudo lsof -iTCP -sTCP:LISTEN -P | grep 8080
3) 권한/소유권/런타임 디렉터리
특히 PIDFile=, RuntimeDirectory=, 소켓/로그 디렉터리, /var/lib/... 접근이 흔한 원인입니다.
# 서비스 실행 사용자 확인
sudo systemctl show myservice.service -p User -p Group
# 문제 파일/디렉터리 권한 확인
namei -l /var/lib/myservice
ls -al /var/lib/myservice
4) 환경변수/EnvironmentFile 누락
sudo systemctl show myservice.service -p Environment -p EnvironmentFiles
sudo systemctl cat myservice.service | sed -n '1,200p'
5) 의존 서비스(DB/Redis 등) 미기동
After=는 “순서”일 뿐 “필수”가 아닐 수 있습니다. 실제로는 연결 실패로 즉시 종료할 수 있습니다.
sudo systemctl list-dependencies myservice.service
sudo systemctl status postgresql redis
6) 리소스 제한(파일 디스크립터/메모리)
Too many open files→LimitNOFILE필요- OOM →
MemoryMax/cgroup 제한/누수
sudo systemctl show myservice.service -p LimitNOFILE -p MemoryMax -p TasksMax
# 커널 메시지에서 OOM 흔적
sudo dmesg -T | grep -i -E "oom|killed process"
8~9분: StartLimit에 걸렸다면 즉시 복구
재시작이 너무 빨리 반복되면 systemd가 보호 차원에서 시작을 막습니다.
증상:
Start request repeated too quickly.Failed with result 'start-limit-hit'.
해결(원인 수정 전이라도 일단 재시도 가능하게):
sudo systemctl reset-failed myservice.service
sudo systemctl start myservice.service
근본적으로는 실패 간격을 늘리거나(RestartSec=), StartLimit 정책을 조정합니다.
[Unit]
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=3
9~10분: 재발 방지를 위한 “관측 가능성” 보강
원인을 잡았더라도, 다음 장애 때 10분 진단이 가능하려면 로그와 종료 코드를 잘 남겨야 합니다.
ExecStartPre로 사전 조건을 검증
예: 설정 파일 존재/권한, 포트 사용 여부 등을 시작 전에 검사해 명확한 실패 메시지를 남깁니다.
[Service]
ExecStartPre=/usr/bin/test -r /etc/myservice/config.yml
ExecStartPre=/usr/bin/bash -c '! ss -lnt | grep -q ":8080"'
ExecStart=/opt/myapp/bin/myservice --config /etc/myservice/config.yml
Restart=on-failure
RestartSec=2
StandardOutput/StandardError를 journal로 통일
대부분 기본값이지만, 애매하면 명시해 로그 유실을 줄입니다.
[Service]
StandardOutput=journal
StandardError=journal
애플리케이션 런타임(특히 Python) 경로 꼬임도 의심
서비스 환경에서는 PATH, VIRTUAL_ENV, 작업 디렉터리가 달라서 “터미널에서는 되는데 서비스로는 ModuleNotFoundError”가 자주 납니다. Python 계열이라면 아래 체크리스트도 같은 결의 10분 진단에 도움이 됩니다: pip install은 성공인데 실행하면 ModuleNotFoundError가 뜰 때 venv poetry conda 혼용으로 꼬인 인터프리터와 site-packages를 10분 만에 진단하고 확실히 고치는 체크리스트
결론: 10분 진단의 핵심은 “Exit 원인 → unit 최종값 → 단발 실행”
재시작 루프는 복잡해 보이지만, 실제로는 (1) status/show로 Exit 코드/시그널을 확인하고, (2) journalctl로 실패 로그를 좁히고, (3) systemctl cat으로 최종 unit 설정을 확인한 뒤, (4) 재시작을 잠깐 끄고 단발 실행으로 재현하면 대부분 10분 안에 원인 범위를 크게 줄일 수 있습니다.
마지막으로, 원인을 고친 뒤에는 Restart=on-failure, RestartSec, StartLimit*을 합리적으로 조정하고, 사전 조건 검증(ExecStartPre)과 로그 일원화를 해두면 같은 문제가 다시 와도 훨씬 빨리 끝납니다.