Published on

systemd 자동 재시작 무한루프 끊는 실전 가이드

Authors

서버에서 서비스가 갑자기 죽고 systemd가 즉시 다시 살리기를 반복하면, 로그는 순식간에 밀리고 CPU/IO가 튀면서 장애가 커집니다. 특히 Restart=always 또는 Restart=on-failure 조합에서 애플리케이션이 즉시 종료되는 패턴이면, 재시작이 "보호"가 아니라 "증폭기"가 됩니다.

이 글은 무한루프를 안전하게 멈추고, 왜 죽는지 재현 가능한 형태로 확인한 뒤, systemd 레벨에서 재발을 막는 설정까지 다룹니다. 보다 넓은 진단 체크리스트가 필요하면 systemd 서비스가 자꾸 재시작될 때 7단계 진단도 함께 보세요.

1) 지금 당장 루프를 멈추는 방법(피해 최소화)

무한 재시작 중인 서비스는 먼저 "더 이상 재시작하지 못하게" 만들어야 합니다.

즉시 중단

sudo systemctl stop myservice

하지만 stop만으로는 운영 자동화(타이머, 다른 유닛 의존성, 외부 툴)가 다시 올릴 수 있습니다.

마스킹으로 강제 차단(가장 확실)

sudo systemctl mask --now myservice
  • mask는 유닛을 /dev/null로 연결해 어떤 방식으로도 시작되지 않게 합니다.
  • 원인 파악 후 복구할 때는:
sudo systemctl unmask myservice

StartLimit에 걸려서 “실은 멈춘 상태”인지 확인

무한루프처럼 보이지만 사실은 StartLimitBurst에 걸려 더 이상 재시작을 안 하는 경우도 있습니다.

systemctl status myservice

상태 출력에서 다음 같은 문구가 보이면 StartLimit에 의해 차단된 것입니다.

  • Start request repeated too quickly

2) 무한루프인지, 정상 재시도인지 구분하기

systemd 재시작은 크게 세 종류로 나뉩니다.

  • 앱이 즉시 종료됨(종료 코드 0 또는 비0)
  • 워치독/타임아웃으로 강제 종료됨
  • 의존 서비스/환경이 준비되지 않아 반복 실패함

우선 "얼마나 빨리" 죽는지부터 숫자로 확인합니다.

systemctl show myservice -p ExecMainStatus -p ExecMainCode -p NRestarts -p ActiveEnterTimestamp -p ActiveExitTimestamp
  • NRestarts가 계속 증가하면 루프입니다.
  • ExecMainStatus1, 2, 127 같은 값이면 실행 실패 가능성이 큽니다.

3) journald 로그를 “이번 부팅” 기준으로 깔끔하게 보기

재시작이 반복되면 로그가 섞여 원인 파악이 어렵습니다. 범위를 좁혀 봅니다.

journalctl -u myservice -b --no-pager

최근 몇 분만 보고 싶다면:

journalctl -u myservice -b --since "10 min ago" --no-pager

실시간으로 따라가며 죽는 순간을 잡으려면:

journalctl -u myservice -f

로그에 “실제 원인”이 안 보이는 경우

  • 앱이 stdout/stderr로 안 찍고 파일로만 남길 수 있음
  • 권한 문제로 로그 파일조차 못 쓰고 즉시 종료할 수 있음
  • ExecStart가 셸 스크립트인데 set -e 등으로 조용히 죽을 수 있음

이 경우 다음 섹션의 "수동 실행"이 빠른 해답입니다.

4) systemd 밖에서 동일 조건으로 수동 실행해보기

systemd 환경과 SSH 쉘 환경은 다릅니다. 유닛에 정의된 사용자/작업 디렉터리/환경변수로 실행해 재현해야 합니다.

유닛 설정 확인

systemctl cat myservice

특히 다음 항목을 확인하세요.

  • User, Group
  • WorkingDirectory
  • Environment, EnvironmentFile
  • ExecStart 경로, 인자

동일 사용자로 실행

sudo -u myuser -H -- /opt/myapp/bin/myapp --config /etc/myapp/config.yml

WorkingDirectory가 중요하면:

sudo -u myuser -H -- bash -lc 'cd /opt/myapp && ./bin/myapp --config /etc/myapp/config.yml'

이 단계에서 자주 나오는 원인:

  • 설정 파일 경로 오타, 권한 부족
  • 상대 경로 의존(작업 디렉터리가 달라 실패)
  • 필요한 환경변수 미설정
  • 바이너리/스크립트의 실행 권한 누락
  • 동적 라이브러리 누락(예: libssl)

5) 가장 흔한 “즉시 재시작” 원인 6가지와 확인법

5-1) ExecStart 경로/권한 문제

ls -al /opt/myapp/bin/myapp
file /opt/myapp/bin/myapp
  • 실행 비트가 없으면 chmod +x 필요
  • 스크립트라면 shebang이 유효해야 함(예: #!/usr/bin/env bash)

5-2) EnvironmentFile 로딩 실패

유닛에 이런 설정이 있을 때:

EnvironmentFile=/etc/myapp/myapp.env

파일이 없으면 서비스가 시작도 못 하고 실패할 수 있습니다. 존재/권한을 확인하세요.

sudo ls -al /etc/myapp/myapp.env

필수가 아니라면 - 접두사를 붙여 "없어도 진행"하게 만들 수 있습니다.

EnvironmentFile=-/etc/myapp/myapp.env

5-3) Type 불일치로 인한 오판정

앱이 포그라운드로 떠 있어야 하는데 Type=forking이면 systemd가 메인 PID를 잘못 추적해 재시작 루프를 만들기도 합니다.

대부분의 현대 서비스는 다음이 안전합니다.

Type=simple

반대로 앱이 sd_notify를 지원한다면:

Type=notify
NotifyAccess=main

5-4) 시작 타임아웃(TimeoutStartSec) 또는 워치독

초기화가 오래 걸리는데 타임아웃이 짧으면 systemd가 죽이고 재시작합니다.

systemctl show myservice -p TimeoutStartUSec -p WatchdogUSec

로그에 start operation timed out가 보이면 타임아웃을 늘리거나 초기화 과정을 개선해야 합니다.

5-5) OOM 또는 커널에 의해 종료

앱이 메모리를 터뜨리면 systemd는 “갑자기 죽음”으로 보고 재시작합니다.

journalctl -k -b --no-pager | grep -i -E 'oom|killed process'

컨테이너 환경의 OOM 진단은 접근 방식이 다르니, 쿠버네티스라면 EKS에서 Pod /dev/shm 부족으로 OOM 해결하기도 참고할 만합니다.

5-6) 의존성 서비스 미기동(네트워크/DB/마운트)

예: DB가 아직 안 떴는데 앱이 즉시 종료하고 재시작을 반복.

해결은 두 갈래입니다.

  • systemd 의존성으로 "기다리게" 만들기
  • 앱 자체를 "재시도" 설계로 바꾸기

systemd에서는 예를 들어 네트워크 이후 시작:

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

스토리지 마운트 의존이 있으면:

RequiresMountsFor=/data

6) 무한루프 방지용 systemd 설정(재발 방지 핵심)

원인을 고쳤더라도, 다음 설정을 넣어두면 "다시 터졌을 때 피해"가 줄어듭니다.

6-1) 재시작 정책을 “무조건”에서 “조건부”로

Restart=on-failure
RestartSec=5s
  • Restart=always는 정상 종료(코드 0)도 재시작합니다.
  • 배치성 작업이나 마이그레이션 유닛은 특히 위험합니다.

6-2) StartLimit로 폭주 차단

StartLimitIntervalSec=60
StartLimitBurst=3
  • 60초 동안 3번 이상 실패하면 systemd가 더 이상 재시작하지 않습니다.
  • 장애 시 로그/알림을 확인하고 사람이 개입할 시간을 벌어줍니다.

6-3) 종료 코드에 따라 재시작하지 않기

예를 들어 설정 오류는 재시작해도 의미가 없으니 즉시 멈추게 만들 수 있습니다.

Restart=on-failure
RestartPreventExitStatus=2 64 78
  • 앱에서 "설정 오류"를 특정 종료 코드로 통일해두면 매우 효과적입니다.

6-4) 백오프(점진적 대기)는 systemd만으로는 제한적

systemd의 RestartSec는 고정 대기입니다. 지수 백오프가 필요하면:

  • 앱 내부 재시도에 백오프 적용
  • 또는 ExecStartPre에서 간단한 sleep 전략을 넣되, 과용은 피하기

7) 디버깅을 쉽게 만드는 유닛 템플릿 예시

아래는 무한루프를 줄이고, 로그/환경을 명확히 남기며, 의존성을 어느 정도 보강한 예시입니다.

[Unit]
Description=MyApp Service
After=network-online.target
Wants=network-online.target

StartLimitIntervalSec=60
StartLimitBurst=3

[Service]
Type=simple
User=myuser
Group=myuser
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/myapp.env

ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yml

Restart=on-failure
RestartSec=5s
RestartPreventExitStatus=2 64 78

# 디버깅에 도움되는 로그 정책(환경에 맞게 조정)
StandardOutput=journal
StandardError=journal

# 필요 시 타임아웃 조정
TimeoutStartSec=60s

[Install]
WantedBy=multi-user.target

변경 후에는 반드시 데몬 리로드를 해야 합니다.

sudo systemctl daemon-reload
sudo systemctl restart myservice
sudo systemctl status myservice

8) “고쳤는데도 가끔 루프”라면: 운영 관점 체크

무한루프는 단순 설정 실수 외에도 운영 이슈가 섞여 재발합니다.

  • 디스크 가득 참으로 로그/DB 쓰기 실패
  • 인증서 만료로 TLS 핸드셰이크 실패
  • DNS 장애로 외부 의존 호출 실패
  • 배포 직후 마이그레이션 경쟁 조건(여러 인스턴스 동시에 실행)

이런 경우 systemd 설정만 만지기보다, 애플리케이션이 실패를 어떻게 분류하고 종료 코드를 어떻게 내보내는지(재시작 가치가 있는 실패인지)까지 포함해 설계를 정리해야 합니다.

재부팅 이후에만 문제가 재현된다면, 부팅 타이밍/의존성 문제일 가능성이 높습니다. 그 경우 systemd 서비스가 재부팅 후 안 뜰 때 9단계 진단도 함께 보면 원인 좁히기가 빨라집니다.

9) 빠른 체크리스트(현장용)

  • systemctl mask --now로 폭주 중단
  • systemctl show로 종료 코드와 재시작 횟수 확인
  • journalctl -u -b로 이번 부팅 로그만 확인
  • systemctl catUser, WorkingDirectory, EnvironmentFile, ExecStart 점검
  • 동일 사용자로 수동 실행해 재현
  • Restart=on-failure, StartLimit*, RestartPreventExitStatus로 재발 방지

무한 재시작은 “서비스가 죽는다”보다 “죽는 방식이 통제되지 않는다”가 더 큰 문제입니다. 루프를 끊는 장치(StartLimit, 조건부 Restart, 의미 있는 종료 코드)를 넣어두면, 같은 장애가 와도 장애 반경이 훨씬 작아집니다.