- Published on
systemd 서비스 재시작 무한루프 - 원인과 차단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 systemctl status 를 보는데 서비스가 activating (auto-restart) 를 반복하고, CPU가 튀거나 로그가 폭발하는 상황은 생각보다 흔합니다. 특히 Restart=always 같은 설정이 들어간 유닛은 “죽는 즉시 다시 살린다”는 의도는 좋지만, 근본 원인이 해결되지 않으면 재시작 폭풍으로 이어집니다.
이 글은 systemd 서비스가 재시작 무한루프일 때
journalctl로 “첫 실패 원인”을 정확히 잡는 법Restart=정책을 상황에 맞게 바꾸는 기준StartLimit*로 재시작 폭주를 차단하는 방법- 자주 놓치는 권한, 환경변수, 작업 디렉터리, 의존성 문제
를 한 번에 정리합니다.
1) 증상 확인: 무한루프인지, 정상 재시도인지
먼저 상태에서 “언제부터 얼마나 자주” 재시작하는지 확인합니다.
systemctl status myapp.service
systemctl show myapp.service -p ActiveState -p SubState -p NRestarts -p ExecMainStatus -p ExecMainCode
NRestarts가 빠르게 증가하면 거의 확실히 루프입니다.ExecMainStatus가1이나127같은 값이면 프로세스가 즉시 실패하고 있다는 뜻입니다.
다음으로 최근 실패 이벤트만 뽑아 패턴을 봅니다.
journalctl -u myapp.service --since "10 min ago" --no-pager
2) 핵심: “마지막 로그”가 아니라 “첫 실패 로그”를 잡아라
재시작 루프에서는 마지막 실패가 원인을 가리는 경우가 많습니다. 예를 들어 DB 연결 실패로 종료되는데, 재시작이 너무 빠르면 뒤쪽에는 “시작함” 로그만 반복될 수 있습니다.
다음 옵션 조합이 유용합니다.
# 최신부터 역순으로
journalctl -u myapp.service -r --no-pager
# 부팅 이후로 제한
journalctl -u myapp.service -b --no-pager
# 특정 우선순위 이상만
journalctl -u myapp.service -p warning..alert --no-pager
또한 systemd는 종료 원인을 상태로도 남깁니다.
systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p ExecMainStartTimestamp -p ExecMainExitTimestamp
ExecMainCode=exited와ExecMainStatus=127이면 보통 실행 파일을 못 찾는 케이스가 많습니다.ExecMainCode=killed이면 OOM, 시그널 종료, watchdog 등 외부 요인을 의심합니다.
3) 흔한 원인 1: 실행 파일 경로, 권한, 작업 디렉터리
3-1) ExecStart 경로 문제
가장 흔한 실수는 ExecStart=/usr/bin/myapp 같은 경로가 실제로는 없거나, 환경이 달라 실행이 안 되는 경우입니다.
systemctl cat myapp.service
ls -al /usr/bin/myapp
ExecStart 는 셸이 아니라 systemd가 직접 실행합니다. 따라서 다음이 통하지 않습니다.
ExecStart=cd /app && ./run.sh같은 형태ExecStart=source /etc/profile && myapp같은 형태
이럴 때는 WorkingDirectory= 와 절대 경로를 사용하거나, 셸을 명시해야 합니다.
[Service]
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp
또는 정말 셸이 필요하면 다음처럼 명시합니다.
[Service]
ExecStart=/bin/bash -lc 'cd /opt/myapp && ./run.sh'
단, 셸을 끼우면 quoting 문제와 환경 의존성이 늘어나므로 가능한 피하는 편이 좋습니다.
3-2) 권한 및 사용자 문제
User= 를 지정했는데 파일 권한이나 바인딩 포트 권한 때문에 죽는 경우도 많습니다.
[Service]
User=myapp
Group=myapp
점검 포인트:
- 로그 디렉터리, 소켓, PID 파일 경로에 쓰기 권한이 있는지
WorkingDirectory에 접근 가능한지1024미만 포트 바인딩이면CAP_NET_BIND_SERVICE필요
권한 디버깅은 서비스 계정으로 직접 실행해보면 빠릅니다.
sudo -u myapp /opt/myapp/bin/myapp --config /etc/myapp/config.yaml
4) 흔한 원인 2: 환경변수 누락과 EnvironmentFile=
터미널에서는 잘 뜨는데 systemd로만 죽는다면 환경변수 차이를 의심해야 합니다.
유닛에 환경변수를 명시합니다.
[Service]
Environment="APP_ENV=prod"
Environment="DATABASE_URL=postgres://..."
환경 파일을 쓰는 패턴도 흔합니다.
[Service]
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/opt/myapp/bin/myapp
파일이 없을 수도 있으면 앞에 - 를 붙여 “없어도 진행”하게 할 수 있습니다.
EnvironmentFile=-/etc/myapp/myapp.env
환경변수 적용 여부는 다음으로 확인합니다.
systemctl show myapp.service -p Environment
5) Restart= 제대로 이해하기: 무한루프를 만드는 주범
Restart= 는 “어떤 종료에 대해 재시작할지”를 결정합니다.
no: 재시작 안 함on-failure: 비정상 종료에만 재시작always: 정상 종료 포함 무조건 재시작
서비스가 “작업을 하고 정상 종료”하는 타입인데 Restart=always 를 써버리면, 정상 종료도 루프로 보일 수 있습니다. 배치성 작업이면 Type=oneshot 과 함께 설계해야 합니다.
[Service]
Type=oneshot
ExecStart=/opt/myapp/bin/job
RemainAfterExit=yes
반대로 웹 서버처럼 상시 구동이라면 보통은 다음 조합이 안전합니다.
[Service]
Restart=on-failure
RestartSec=3s
RestartSec가 없으면 너무 빠르게 재시작하면서 로그 폭발과 부하를 유발할 수 있습니다.
6) 재시작 폭주 차단: StartLimitIntervalSec 와 StartLimitBurst
systemd는 일정 횟수 이상 재시작하면 “포기”하도록 제한을 걸 수 있습니다. 이 제한이 없거나 너무 크면 무한루프가 오래 지속됩니다.
유닛에 다음을 추가합니다.
[Unit]
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=3s
의미:
- 60초 동안 5번 이상 재시작하면 제한에 걸려 더 이상 시작하지 않음
제한에 걸리면 상태에 이런 메시지가 보일 수 있습니다.
Start request repeated too quickly
이때는 원인을 해결한 뒤 카운터를 리셋하고 다시 시작합니다.
systemctl reset-failed myapp.service
systemctl start myapp.service
7) “의존성 준비 전 시작” 문제: DB, 네트워크, 마운트
재시작 루프의 또 다른 흔한 원인은 의존 서비스가 준비되기 전에 시작하는 것입니다.
7-1) 네트워크 준비
[Unit]
After=network-online.target
Wants=network-online.target
7-2) 특정 마운트나 디렉터리
예를 들어 /data 마운트가 늦게 올라오면 애플리케이션이 바로 죽을 수 있습니다.
[Unit]
RequiresMountsFor=/data
After=local-fs.target
7-3) DB 준비
DB가 뜨기 전에 앱이 시작되면 연결 실패로 즉시 종료하고, systemd가 다시 살리고, 또 실패하는 루프가 됩니다. 이 경우는 애플리케이션 레벨 재시도도 같이 설계해야 합니다.
HTTP 클라이언트 재시도 설계 관점은 다음 글도 참고할 만합니다.
8) 종료 코드로 원인 분류하기: RestartPreventExitStatus
어떤 실패는 “재시작해봤자 의미 없음”입니다. 예를 들어 설정 파일 파싱 실패, 마이그레이션 필요, 필수 환경변수 누락 같은 케이스는 재시작보다 운영자 개입이 필요합니다.
이때 종료 코드를 정하고, 해당 코드에서는 재시작을 막을 수 있습니다.
[Service]
Restart=on-failure
RestartPreventExitStatus=2
애플리케이션에서 “치명적 설정 오류”를 exit 2 로 종료하도록 맞추면, systemd가 불필요한 재시작을 하지 않습니다.
9) 디버깅을 쉽게 만드는 유닛 옵션들
9-1) 로그를 저널에 확실히 남기기
표준 출력이 버퍼링되어 마지막 로그가 안 찍히는 문제가 있습니다.
[Service]
StandardOutput=journal
StandardError=journal
9-2) 타임아웃으로 “걸린 상태”도 실패로 만들기
프로세스가 기동 중에 멈추면 재시작이 안 되고 그냥 매달릴 수 있습니다.
[Service]
TimeoutStartSec=30
TimeoutStopSec=30
9-3) 실제 실행 커맨드 확인
드롭인 설정까지 포함해 최종 유닛을 확인합니다.
systemctl cat myapp.service
systemctl show myapp.service -p FragmentPath -p DropInPaths
10) 실전 예시: 무한루프를 “안전한 재시도”로 바꾸는 템플릿
아래는 웹 서비스 계열에서 자주 쓰는 기본 템플릿입니다.
[Unit]
Description=MyApp API
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure
RestartSec=3s
# 설정 오류 등은 재시작하지 않도록 애플리케이션 exit code로 제어
RestartPreventExitStatus=2
TimeoutStartSec=30
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
적용 후에는 반드시 데몬 리로드를 합니다.
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service
11) systemd 루프와 쿠버네티스 CrashLoopBackOff의 공통점
재시작 루프는 systemd만의 문제가 아닙니다. 쿠버네티스에서도 동일한 현상이 CrashLoopBackOff 로 나타납니다. 공통된 핵심은 “재시작 정책이 원인을 해결해주지 않는다”는 점입니다. 로그로 첫 실패를 잡고, 재시도 간격과 상한을 두고, 재시도해도 의미 없는 실패는 빠르게 멈추게 해야 합니다.
쿠버네티스 관점의 유사한 문제 해결 흐름은 다음 글도 참고할 수 있습니다.
12) 체크리스트: 10분 안에 보는 우선순위
systemctl show ... -p NRestarts -p ExecMainStatus -p ExecMainCode로 실패 형태 파악journalctl -u ... -b로 부팅 이후 첫 실패 로그 확인ExecStart절대 경로,WorkingDirectory, 실행 권한 확인User권한과 파일 쓰기 권한 확인- 환경변수는
EnvironmentFile로 명시하고systemctl show -p Environment로 검증 Restart=on-failure와RestartSec로 과도한 재시작 방지StartLimitIntervalSec와StartLimitBurst로 폭주 차단- 설정 오류는
RestartPreventExitStatus로 재시작 금지
재시작 무한루프는 “systemd가 나쁘다”기보다, 실패 원인을 드러내지 못하는 로그 구조와 과한 재시작 정책이 합쳐져서 생기는 운영 문제입니다. journalctl 로 첫 실패를 정확히 잡고, Restart= 와 StartLimit* 를 보수적으로 설계하면 장애 확산을 크게 줄일 수 있습니다.