systemd 자동 재시작 완벽 가이드: Restart=always vs on-failure와 start-limit-hit 해결
프로세스가 죽었는데 아무도 안 살려준다
새벽 3시, 서비스가 조용히 죽었습니다. 모니터링 알람이 울리고 나서야 SSH로 들어가 systemctl start를 친 경험, 운영자라면 한 번쯤 있을 겁니다. 쿠버네티스 같은 오케스트레이터가 없는 단일 VM·베어메탈 환경에서는 이 "죽으면 알아서 살리기"를 누군가가 책임져야 하는데, 요즘은 supervisord나 pm2를 따로 올리지 않고 systemd로 일원화하는 흐름이 사실상 표준입니다. 배포 도구가 이미 깔려 있고, 부팅 자동 시작·로그 수집(journald)·의존성 관리까지 한 번에 되니까요.
그런데 막상 Restart=always만 붙였다가 "start request repeated too quickly" 에러로 서비스가 영영 안 올라오는 함정에 빠지는 분이 많습니다. 이 글은 Restart 옵션의 정확한 차이부터, 그 무한 재시작 함정을 진단하고 푸는 명령어, 그리고 복붙 가능한 .service 전체 샘플까지 한 번에 정리합니다.
Restart 옵션 4종 + RestartSec 완전 비교
가장 헷갈리는 게 always와 on-failure의 차이입니다. 핵심은 "정상 종료(exit 0)도 재시작 대상으로 볼 것이냐" 입니다.
| Restart 값 | exit 0 (정상) | exit 0 아님 (실패) | 시그널 종료(SIGSEGV 등) | watchdog timeout | 시작/종료 timeout |
|---|---|---|---|---|---|
no (기본값) | ❌ | ❌ | ❌ | ❌ | ❌ |
on-success | ✅ | ❌ | ❌ | ❌ | ❌ |
on-failure | ❌ | ✅ | ✅ | ✅ | ✅ |
on-abnormal | ❌ | ❌ | ✅ | ✅ | ✅ |
always | ✅ | ✅ | ✅ | ✅ | ✅ |
SIGTERM처럼 "깔끔하게 종료해달라"는 시그널은 기본적으로 "클린 종료"로 간주되어on-failure에서는 재시작하지 않습니다.SIGKILL,SIGSEGV,SIGABRT등은 비정상으로 보고 재시작합니다.
언제 무엇을 쓰나?
- 웹 서버·API·데몬 등 항상 떠 있어야 하는 상주 서비스 →
on-failure가 정석입니다. 운영자가systemctl stop으로 의도적으로 내렸을 때(exit 0) 다시 살아나지 않으니 안전합니다. - 무조건 떠 있어야 하고, 정상 종료가 곧 비정상인 서비스 →
always. 단,systemctl stop을 해도 다시 올라올 수 있으니 운영 중 혼란에 주의하세요. - 배치성 잡 →
on-failure또는no.
RestartSec: 재시작 전에 숨 한 번 쉬기
RestartSec=5s재시작 전 대기 시간입니다. 기본값은 100ms인데, 이걸 그냥 두면 DB가 죽었거나 포트가 막힌 상황에서 서비스가 초당 수십 번 부팅을 시도하며 CPU 폭주·DB 연결 폭주를 일으킵니다. 실무에서는 RestartSec=5(초) 정도를 권장합니다. 외부 의존성(DB, 메시지큐) 복구를 기다릴 여유를 주는 값입니다.
무한 재시작의 함정: start-limit-hit는 왜 뜨는가
여기가 이 글의 핵심입니다. systemd는 "재시작이 너무 빨리 반복되면 포기"하는 안전장치를 갖고 있습니다.
StartLimitIntervalSec=10
StartLimitBurst=5의미는 "10초 안에 5번을 초과해 재시작하면 더 이상 살리지 않는다" 입니다. 이 조건에 걸리면 로그에 이렇게 찍힙니다.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service.ExecStart가 즉시 실패하는 버그(설정 오타, 포트 충돌 등)가 있으면, RestartSec가 짧을 경우 10초 안에 순식간에 5번을 넘겨 이 상태에 빠집니다. 이때부터는 systemctl start를 쳐도 올라오지 않습니다. 카운터가 잠겨 있기 때문입니다.
해결책 3가지
① 한도 자체를 조정한다
StartLimitIntervalSec=60
StartLimitBurst=3② RestartSec를 늘려 burst를 회피한다 — 가장 중요한 핵심 공식이 있습니다.
RestartSec × StartLimitBurst > StartLimitIntervalSec이 부등식을 만족하면, 정해진 인터벌 안에서 burst 횟수를 채우는 게 물리적으로 불가능해져 start-limit-hit에 영원히 걸리지 않습니다. 예를 들어 RestartSec=5, StartLimitBurst=3이면 5×3=15이므로 StartLimitIntervalSec를 15보다 작게(예: 10) 잡으면 한도에 걸리지 않습니다. 반대로 진짜로 망가진 서비스를 빠르게 멈추고 싶으면 이 공식을 일부러 깨서 일정 횟수 후 멈추게 둘 수도 있습니다.
③ 제한을 아예 없앤다 — 어떤 일이 있어도 계속 재시작하길 원한다면:
StartLimitIntervalSec=0이 값이 0이면 카운팅이 비활성화되어 무한 재시도합니다. (단, 진짜 버그가 있으면 CPU를 갉아먹으니 RestartSec는 반드시 넉넉히 주세요.)
systemctl reset-failed: 잠긴 카운터 풀기
start-limit-hit에 걸린 상태에서는 설정만 고치고 start해도 안 됩니다. 먼저 카운터를 초기화해야 합니다.
# 1) unit 파일 수정 후
sudo systemctl daemon-reload
# 2) 실패 카운터 리셋 (이게 핵심!)
sudo systemctl reset-failed myapp.service
# 3) 다시 기동
sudo systemctl start myapp.service
# 4) failed 상태가 풀렸는지 확인
systemctl status myapp.service
systemctl is-failed myapp.service # 'active' 또는 'inactive'면 정상reset-failed 없이 헤매다 "왜 start가 먹통이지?" 하는 경우가 정말 많습니다. start-limit 로그를 봤다면 이 명령어를 먼저 떠올리세요.
죽은 원인 진단과 실전 unit 파일
자동 재시작을 걸기 전에, 왜 죽었는지부터 봐야 합니다. journald가 다 기록해 둡니다.
# 최근 50줄 (페이저 없이 바로 출력)
journalctl -u myapp.service -n 50 --no-pager
# 최근 10분간 로그만
journalctl -u myapp.service --since "10 min ago"
# 현재 상태와 마지막 종료 코드/시그널
systemctl status myapp.service
# 실제 적용된 재시작 관련 값 확인 (오타·daemon-reload 누락 점검)
systemctl show myapp.service -p Restart -p RestartSec -p StartLimitBurst -p StartLimitIntervalUSecsystemctl status의 마지막 줄에 Main PID exited, code=exited, status=1 또는 code=killed, signal=SEGV 같은 단서가 나옵니다. 여기서 종료 원인을 잡고 나서 Restart 정책을 정하는 게 순서입니다.
실무 경험 한마디: 저는
RestartSec를 짧게 두고 외부 DB 장애를 만났다가, 재시작 폭주로 DB 커넥션 풀이 더 빨리 고갈되는 악순환을 본 적이 있습니다. 그 뒤로는 상주 서비스의 기본값을on-failure+RestartSec=5+ 넉넉한 StartLimit로 고정해 두고 시작합니다. "빨리 살리기"보다 "안전하게 살리기"가 결국 가용성에 더 좋았습니다.
따라치면 되는 .service 전체 샘플
/etc/systemd/system/myapp.service:
[Unit]
Description=My Application Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3
# 표준출력/에러를 journald로
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target적용 절차:
# 파일 저장 후 systemd에 변경 알림
sudo systemctl daemon-reload
# 부팅 자동 시작 등록 + 즉시 기동
sudo systemctl enable --now myapp.service
# 잘 떴는지 확인
systemctl status myapp.service참고:
StartLimitIntervalSec/StartLimitBurst는 본래[Unit]섹션 지시어입니다. 위처럼[Service]에 적어도 최신 systemd는 대부분 인식하지만, 동작이 이상하면[Unit]섹션으로 옮겨보세요.
결론: 자동 재시작 체크리스트
상주 서비스라면 아래 기본값으로 시작하면 90%는 안전합니다.
- ✅
Restart=on-failure— 의도적 stop(exit 0)엔 살아나지 않게 - ✅
RestartSec=5— CPU·DB 폭주 방지 - ✅
StartLimitIntervalSec=60,StartLimitBurst=3— 진짜 망가진 서비스는 멈추게 - ✅ 무한 재시도가 꼭 필요하면
StartLimitIntervalSec=0 - ✅ start-limit-hit를 만나면 →
daemon-reload→reset-failed→start순서 - ✅ 정책 정하기 전에
journalctl -u로 종료 원인부터 확인
RestartSec × StartLimitBurst > StartLimitIntervalSec 공식 하나만 기억해도 무한 재시작 함정의 절반은 피해갑니다.
자주 묻는 질문 (FAQ)
Q. "start request repeated too quickly"가 뜨고 systemctl start를 해도 안 올라옵니다.
A. start-limit 카운터가 잠긴 상태입니다. sudo systemctl reset-failed <서비스명>으로 카운터를 초기화한 뒤 sudo systemctl start <서비스명>을 다시 실행하세요. 근본 원인(ExecStart 즉시 실패)을 고치지 않으면 또 걸리니, journalctl -u로 종료 원인을 함께 확인하세요.
Q. Restart=always와 on-failure 중 무엇을 써야 하나요?
A. 운영자가 systemctl stop으로 내렸을 때 다시 살아나면 곤란한 일반 상주 서비스는 on-failure가 안전합니다. 정상 종료(exit 0)조차 즉시 다시 띄워야 하는 특수한 경우에만 always를 쓰세요.
Q. 설정을 바꿨는데 적용이 안 됩니다.
A. unit 파일을 수정했으면 반드시 sudo systemctl daemon-reload를 먼저 해야 합니다. 그 후 systemctl show <서비스> -p Restart -p RestartSec -p StartLimitBurst로 실제 반영된 값을 확인하세요.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...