Published on

systemd 서비스 무한 재시작 원인과 journalctl 추적

Authors

서버에서 특정 데몬이 갑자기 CPU를 잡아먹거나, 로그가 폭발적으로 쌓이거나, 서비스가 active (running)failed 를 빠르게 오가며 “살아있는 듯 죽어있는” 상태가 되면 가장 먼저 의심해야 할 것이 systemd 의 자동 재시작 루프입니다. 보통은 Restart=always 같은 설정이 원인처럼 보이지만, 실제로는 프로세스가 왜 종료되는지, 그리고 systemd가 어떤 조건에서 재시작을 트리거하는지를 함께 봐야 합니다.

이 글은 다음 순서로 진행합니다.

  • 무한 재시작이 발생하는 전형적인 원인 패턴
  • systemctl 로 상태를 빠르게 요약하는 법
  • journalctl 로 “첫 실패 지점”을 정확히 잡는 법
  • 서비스 유닛 파일에서 재시작 정책을 안전하게 조정하는 법
  • 운영에서 자주 놓치는 함정(환경변수, 권한, 의존성, 타임아웃)

1) 무한 재시작이란 무엇인가

systemd는 서비스가 종료되면 유닛 설정에 따라 재시작을 시도합니다. 이때 프로세스가 정상적으로 떠 있지 못하고 짧은 시간 안에 계속 종료되면, systemd는 다음과 같은 형태로 로그를 남기며 루프를 만듭니다.

  • Main process exited, code=exited, status=...
  • Failed with result 'exit-code'
  • Service RestartSec=... expired, scheduling restart
  • Start request repeated too quickly

특히 마지막 메시지(Start request repeated too quickly)는 systemd가 자체적으로 “너무 자주 죽는다”고 판단해 재시작을 멈추는 보호 장치가 발동된 상태입니다. 반대로, 보호 장치 임계값이 높거나 비활성화되어 있다면 재시작이 장시간 지속되며 장애를 키웁니다.

2) 가장 흔한 원인 8가지

무한 재시작은 “systemd 설정 문제”라기보다 “프로세스가 뜨지 못하는 이유”가 핵심입니다. 현장에서 자주 만나는 패턴을 8가지로 정리합니다.

2.1 실행 파일/인자 오류 (ExecStart 문제)

  • 바이너리 경로 오타
  • 실행 권한 미부여
  • 인자 변경으로 더 이상 유효하지 않은 플래그 사용

이 경우 status=203/EXEC 같은 형태가 흔합니다.

2.2 환경변수/설정 파일 누락

서비스는 쉘에서 수동 실행하면 잘 뜨는데 systemd로만 실패한다면, 대개 환경 차이입니다.

  • EnvironmentFile= 경로가 없거나 권한 문제
  • working directory가 달라 상대경로 설정을 못 찾음
  • .env 를 애플리케이션이 읽지 못함

2.3 권한/파일 접근 문제

User= 로 낮은 권한 사용자로 실행하면서 다음이 터집니다.

  • 로그 디렉터리 쓰기 실패
  • 소켓/포트 바인딩 실패(Permission denied)
  • 설정 디렉터리 읽기 실패

2.4 포트 충돌 또는 이미 떠 있는 프로세스

  • Address already in use
  • 오래된 프로세스가 포트를 점유
  • PIDFile= 이 stale 상태

2.5 의존 서비스 미기동(네트워크, DB, DNS)

애플리케이션이 시작 시점에 DB 연결을 강제하면, DB가 잠깐 늦게 뜨는 것만으로도 서비스가 종료하고 재시작을 반복합니다.

이런 유형은 쿠버네티스에서도 비슷한 형태로 나타납니다. 예를 들어 인그레스/네트워크 계층 문제가 애플리케이션의 스타트업 실패로 이어져 502 계열 장애로 확산되기도 합니다. 관련해서는 EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지도 함께 보면 “상위 계층 증상”과 “하위 계층 원인”을 연결하는 데 도움이 됩니다.

2.6 Type= 불일치로 인한 오판

  • Type=simple 인데 실제로는 포크 후 종료
  • Type=forking 인데 포크하지 않음
  • Type=notify 인데 sd_notify 를 호출하지 않음

systemd는 “서비스가 준비됨”을 잘못 판단해, 살아있는데도 실패로 처리하거나, 반대로 죽었는데도 살아있다고 생각할 수 있습니다.

2.7 타임아웃(TimeoutStartSec)과 워밍업

애플리케이션이 초기 마이그레이션/캐시 워밍업 등으로 오래 걸리면 TimeoutStartSec 에 걸려 강제 종료되고 재시작합니다.

2.8 재시작 정책이 과격함

  • Restart=always
  • RestartSec=0
  • StartLimitIntervalSec=0 와 같이 사실상 무제한

이 조합은 “장애를 빨리 드러내는” 대신 “장애를 증폭시키는” 쪽으로 작동할 수 있습니다.

3) 1분 요약: systemctl로 상태부터 잡기

먼저 서비스의 현재 상태와 마지막 종료 코드를 확인합니다.

systemctl status myapp.service --no-pager

여기서 체크할 포인트는 다음입니다.

  • Active:activating 에서 오래 멈추는지
  • Main PID: 가 계속 바뀌는지(재시작 중)
  • Process: 라인에 code=exited, status=... 가 무엇인지
  • Result:exit-code 인지 timeout 인지 signal 인지

재시작 카운트를 바로 보고 싶다면:

systemctl show myapp.service -p NRestarts

유닛 설정(재시작 정책 포함)을 확인합니다.

systemctl cat myapp.service
systemctl show myapp.service \
  -p Restart -p RestartSec -p StartLimitBurst -p StartLimitIntervalSec -p TimeoutStartSec -p Type

4) journalctl로 “첫 실패 원인”을 잡는 추적법

무한 재시작에서 가장 중요한 것은 가장 처음 실패한 순간의 로그입니다. 재시작이 반복되면 최신 로그는 비슷한 메시지로 덮여서 원인이 흐려집니다.

4.1 서비스 단위로 로그 보기

journalctl -u myapp.service --no-pager

최근 로그만 보려면:

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

4.2 시간 범위를 좁혀 “첫 실패” 찾기

재시작 루프가 시작된 시각 전후로 좁혀 봅니다.

journalctl -u myapp.service --since "2026-02-25 09:30:00" --until "2026-02-25 09:40:00" --no-pager

4.3 부팅 단위로 격리하기

재부팅 이후에만 발생했다면 부팅 ID로 분리하는 게 빠릅니다.

journalctl --list-boots --no-pager
journalctl -u myapp.service -b 0 --no-pager

4.4 로그를 따라가며 재시작 루프 관찰

journalctl -u myapp.service -f

여기서 systemd 가 남기는 핵심 라인을 같이 봅니다.

  • Scheduled restart job, restart counter is ...
  • Stopped ...
  • Starting ...

애플리케이션 로그와 systemd 로그가 섞여 보이므로, 종료 직전 애플리케이션이 남긴 마지막 에러를 찾는 것이 목표입니다.

4.5 우선순위/필드로 더 정밀하게

에러 레벨 중심으로 보고 싶다면:

journalctl -u myapp.service -p err..alert --no-pager

systemd가 기록한 종료 코드만 빨리 훑고 싶다면:

journalctl -u myapp.service --no-pager | grep -E "Main process exited|Failed with result|Start request repeated"

5) 재현 가능한 최소 예제로 이해하기

다음은 일부러 바로 종료하는 스크립트를 서비스로 등록해 무한 재시작을 재현하는 예시입니다.

5.1 테스트 스크립트

sudo tee /usr/local/bin/crash-loop.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

echo "starting and failing..."
exit 1
EOF

sudo chmod +x /usr/local/bin/crash-loop.sh

5.2 유닛 파일

/etc/systemd/system/crash-loop.service

[Unit]
Description=Crash Loop Demo
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/crash-loop.sh
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

적용:

sudo systemctl daemon-reload
sudo systemctl enable --now crash-loop.service
systemctl status crash-loop.service --no-pager
journalctl -u crash-loop.service -n 50 --no-pager

이 예제에서 보듯, Restart=always 는 “원인이 해결될 때까지 계속 시도”하는 장점이 있지만, 원인이 영구적이면 로그와 부하만 늘립니다.

6) 유닛 설정으로 루프를 안전하게 제어하기

원인을 고치는 것이 1순위지만, 운영 안정성을 위해 “루프가 무한히 돌지 않게” 방어하는 것도 중요합니다.

6.1 Restart=on-failure 로 바꾸기

정상 종료(exit 0)까지도 재시작하는 always 대신, 실패에만 반응하도록 바꿉니다.

Restart=on-failure
RestartSec=3

6.2 StartLimit으로 폭주 방지

StartLimitIntervalSec=60
StartLimitBurst=5

의미는 “60초 동안 5번 이상 시작 요청이 반복되면 멈춘다”입니다.

6.3 타임아웃 조정

초기 로딩이 오래 걸리는 서비스라면:

TimeoutStartSec=120

반대로, 빨리 실패해야 하는 서비스라면 타임아웃을 줄이고 실패를 명확히 드러내는 것도 방법입니다.

6.4 WorkingDirectoryEnvironmentFile 명시

수동 실행과 systemd 실행의 차이를 없애려면 다음을 명시합니다.

WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env

환경 파일 권한 문제를 피하려면:

sudo chown root:myapp /etc/myapp/myapp.env
sudo chmod 640 /etc/myapp/myapp.env

6.5 의존성 준비 전 실행 방지

네트워크가 확실히 준비된 뒤 시작하도록:

After=network-online.target
Wants=network-online.target

DB 같은 외부 의존성이 있다면 애플리케이션 레벨에서 재시도(backoff)를 넣는 것이 정석입니다. 인프라 측면 장애가 서비스 기동 실패로 번지는 패턴은 쿠버네티스 노드/네트워크 이슈에서도 흔합니다. 예를 들어 CNI/ENI 문제로 네트워크가 불안정하면 상위 서비스가 연쇄적으로 실패할 수 있는데, 이런 케이스는 EKS Node NotReady - CNI ENI 할당 실패 해결 가이드 같은 글의 진단 흐름이 참고가 됩니다.

7) “원인별” journalctl에서 보이는 신호

실전에서는 로그 패턴을 보면 원인을 빠르게 좁힐 수 있습니다.

7.1 status=203/EXEC

  • ExecStart 경로, 실행 권한, shebang 확인

체크:

ls -al /usr/local/bin/myapp
head -n 1 /usr/local/bin/myapp

7.2 status=1/FAILURE 와 애플리케이션 예외

  • 애플리케이션이 남긴 스택 트레이스가 원인
  • 설정 누락, DB 연결 실패, 마이그레이션 실패가 많음

7.3 Failed with result 'timeout'

  • TimeoutStartSec 또는 애플리케이션의 준비 신호 문제
  • Type=notify 인데 notify를 안 하는 경우도 포함

7.4 Permission denied

  • User= 로 실행 시 파일/디렉터리 권한
  • 1024 미만 포트 바인딩 권한

7.5 Address already in use

  • 포트 충돌
  • 이전 프로세스가 남아있음

체크:

sudo ss -lntp | grep -E ":8080\b"

8) 장애 대응 체크리스트(현장용)

  1. systemctl status ... 로 종료 코드/결과 확인
  2. systemctl show ... -p NRestarts 로 폭주 여부 확인
  3. journalctl -u ... -b 0 --since ... 로 첫 실패 로그 찾기
  4. ExecStart 경로/권한, User 권한, WorkingDirectory, EnvironmentFile 점검
  5. 외부 의존성(DB, DNS, 네트워크) 실패 여부 확인
  6. 재시작 정책을 on-failure 로 완화하고 StartLimit 로 폭주 방지
  7. 원인 수정 후 daemon-reload 와 재기동

원인 수정 후 반영:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service --no-pager

9) 마무리

systemd 서비스 무한 재시작은 “재시작을 끄면 해결”되는 문제가 아니라, 프로세스가 왜 종료되는지journalctl 로 정확히 잡아내는 것이 핵심입니다. 특히 반복 재시작 상황에서는 최신 로그보다 “첫 실패 시점”을 좁혀 보는 습관이 문제 해결 시간을 극적으로 줄입니다.

추가로, 서비스가 외부 의존성(네트워크, 로드밸런서, DNS)에 민감하다면 애플리케이션 레벨의 재시도/백오프와 systemd의 재시작 정책을 함께 설계해야 장애가 증폭되지 않습니다. 이런 관점에서 인프라 계층의 장애 신호를 함께 읽는 연습도 도움이 되며, 앞서 소개한 EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지 같은 글이 “증상에서 원인을 역추적”하는 사고에 보탬이 됩니다.