- Published on
systemd 서비스가 반복 재시작될 때 원인 추적
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 리눅스 서버에서 systemd 서비스가 Active: activating (auto-restart) 상태로 계속 재시작되면, 겉으로는 “서비스가 죽는다” 정도로만 보이지만 실제 원인은 매우 다양합니다. 예를 들어 프로세스가 즉시 종료되는 버그일 수도 있고, systemd가 기대하는 타입(Type=)과 실제 동작이 맞지 않아 “정상 실행 중인데도 실패로 판정”되는 경우도 있습니다.
이 글은 재시작 루프를 빠르게 멈추고, 정확한 실패 지점을 재현 가능하게 만들며, 로그/상태/종료 코드 기반으로 원인을 좁혀가는 방법을 단계별로 다룹니다. (비슷한 주제를 더 넓게 보고 싶다면 systemd 서비스가 계속 재시작될 때 원인 9가지도 함께 참고하세요.)
1) 먼저 “재시작 루프”를 안전하게 멈추기
재시작이 계속되면 로그가 폭주하고, 시스템 리소스를 소모하며, 원인 분석을 더 어렵게 만듭니다. 우선 아래 중 하나로 루프를 멈춘 뒤 분석하세요.
임시로 서비스 중지
sudo systemctl stop myapp.service
자동 재시작 비활성화(임시 오버라이드)
운영 환경에서는 유닛 파일을 직접 수정하기보다 drop-in override를 추천합니다.
sudo systemctl edit myapp.service
아래 내용을 추가합니다.
[Service]
Restart=no
적용:
sudo systemctl daemon-reload
sudo systemctl start myapp.service
이렇게 하면 “한 번만 실행해 보고 실패 지점”을 잡기 쉬워집니다.
2) systemd가 보는 “실패 이유”를 1차로 확인
가장 먼저 볼 것은 status와 show입니다. 특히 종료 코드, 마지막 로그, 재시작 카운트가 핵심입니다.
systemctl status myapp.service -l
다음도 같이 확인합니다.
systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p Result -p NRestarts -p Restart -p RestartUSec
ExecMainStatus: 프로세스 종료 코드(예:1,2,137등)ExecMainCode:exited/killed등Result:exit-code,signal,timeout같은 systemd 판정NRestarts: 재시작 횟수
여기서 ExecMainStatus=137 같은 값이 보이면 대개 SIGKILL 계열(예: OOM kill) 가능성을 의심할 수 있습니다.
3) journalctl로 “부팅부터/단위부터” 로그를 좁히기
재시작 루프의 원인을 찾을 때 journalctl -u는 필수입니다.
최근 부팅 기준으로 해당 유닛 로그
journalctl -u myapp.service -b --no-pager
가장 최근 200줄만
journalctl -u myapp.service -b -n 200 --no-pager
시간 범위로 자르기
journalctl -u myapp.service --since "2026-02-24 09:00:00" --until "2026-02-24 09:10:00" --no-pager
재시작 루프에서 중요한 패턴
로그에서 아래 같은 문구가 반복되면 systemd가 “실패로 판정하고 재시작”하는 구조입니다.
Main process exited, code=exited, status=...Failed with result 'exit-code'Service RestartSec=... expired, scheduling restartStart request repeated too quickly
Start request repeated too quickly가 보이면 StartLimitIntervalSec / StartLimitBurst에 걸린 것입니다. 이 경우는 “원인을 고치기 전까지” systemd가 시작 자체를 막습니다.
4) 재시작이 “정상”인지, “설정이 잘못되어 실패로 판정”인지 구분
가장 흔한 함정은 서비스는 살아있는데 systemd가 실패로 간주하는 케이스입니다.
Type=과 실제 실행 방식 불일치
Type=simple:ExecStart가 포그라운드에서 계속 실행되는 프로세스여야 함Type=forking: 데몬이 포크하고 부모가 종료되는 전통적인 방식Type=notify: 프로세스가sd_notify로 준비 완료를 알려야 함
예를 들어 실제로는 포그라운드 프로세스인데 Type=forking이면 systemd가 PID 추적을 잘못해서 종료로 오해할 수 있습니다.
유닛 파일 확인:
systemctl cat myapp.service
ExecStart가 셸 문법에 의존하는 문제
systemd 유닛의 ExecStart=는 기본적으로 셸이 아닙니다. 따라서 리다이렉션, 파이프, && 같은 문법이 들어가면 의도대로 동작하지 않거나 즉시 실패합니다.
나쁜 예(셸 문법을 그대로 사용):
[Service]
ExecStart=/usr/bin/myapp --port 8080 >> /var/log/myapp.log 2>&1
좋은 예(셸이 필요하면 명시적으로 호출):
[Service]
ExecStart=/bin/bash -lc 'exec /usr/bin/myapp --port 8080 >> /var/log/myapp.log 2>&1'
또는 더 권장되는 방식은 StandardOutput=journal을 쓰고 journald로 로그를 모으는 것입니다.
5) 종료 코드와 시그널로 원인 범주를 빠르게 좁히기
systemctl status와 journalctl에서 확인한 종료 상태를 아래처럼 해석하면 디버깅 속도가 빨라집니다.
자주 등장하는 케이스
status=1또는status=2: 애플리케이션 내부 오류, 인자/환경 변수 누락, 설정 파일 파싱 실패status=127: 실행 파일을 못 찾음(경로 오류) 또는 인터프리터 문제code=killed, status=9/KILL: 강제 종료(자원 문제, OOM, 수동 kill 등)Failed with result 'timeout':TimeoutStartSec또는WatchdogSec관련
OOM Kill 여부 확인
dmesg -T | grep -i -E "oom|killed process"
cgroup 메모리 제한이 걸린 환경이면(예: 컨테이너, 제한된 slice) 더 자주 발생합니다.
6) 실행 환경(권한, 작업 디렉터리, 파일 접근) 점검
로컬에서 잘 되는데 systemd에서만 실패하면 대개 “환경이 다르기 때문”입니다.
User= / Group= 권한 문제
유닛 파일에서 User=myapp로 실행하는데, 해당 사용자가 아래에 접근 못 하면 바로 죽습니다.
- 로그 파일 경로
- 소켓/포트 바인딩(특권 포트)
- 설정 파일
- 데이터 디렉터리
권한 관련 오류는 애플리케이션 로그에 permission denied로 남거나, systemd에서 status=203/EXEC 등으로 나타날 수 있습니다.
WorkingDirectory= 누락
상대 경로로 설정 파일을 읽는 앱은 WorkingDirectory가 없으면 실패합니다.
[Service]
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config ./config.yaml
환경 변수 차이
systemd는 로그인 셸 환경과 다릅니다. .bashrc에만 있는 변수를 기대하면 실패합니다.
[Service]
Environment="APP_ENV=prod"
EnvironmentFile=-/etc/myapp/myapp.env
적용 후 확인:
systemctl show myapp.service -p Environment
7) 타임아웃/준비 신호 문제: “느려서 죽는” 재시작 루프
서비스가 느리게 뜨거나, 준비 완료 신호를 못 보내면 systemd는 실패로 판단할 수 있습니다.
TimeoutStartSec 조정
[Service]
TimeoutStartSec=120
특히 DB 마이그레이션, 캐시 워밍업, 대형 설정 로딩이 포함된 앱은 기본 타임아웃에서 실패할 수 있습니다.
Type=notify 사용 시 준비 신호 누락
Type=notify인데 앱이 sd_notify를 호출하지 않으면 영원히 준비가 안 된 것으로 간주되어 타임아웃으로 죽을 수 있습니다. 이 경우는 Type=simple로 바꾸거나, 실제로 notify를 구현해야 합니다.
8) 의존성/네트워크 준비 문제: 부팅 직후만 실패하는 케이스
부팅 직후에만 재시작하다가 어느 순간 정상화되는 경우, 흔히 아래가 원인입니다.
- 네트워크 미준비 상태에서 외부 API/DB 연결 시도
- DNS 준비 전 조회 실패
- 마운트/볼륨 미연결
After=와 Wants=로 의존성 정리
[Unit]
Wants=network-online.target
After=network-online.target
단, network-online.target이 실제로 “온라인을 보장”하려면 배포판에 따라 systemd-networkd-wait-online.service 또는 NetworkManager-wait-online이 활성화되어 있어야 합니다.
9) systemd 자체의 보안 옵션이 앱을 죽이는 경우
하드닝 옵션이 강하면 앱이 파일 시스템/네트워크/시스템 콜을 못 써서 즉시 종료할 수 있습니다.
예:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
이런 옵션을 켠 뒤 재시작 루프가 생겼다면, 일단 하드닝을 최소화해 원인 옵션을 좁혀가세요.
10) 재현 가능한 디버깅: systemd-run로 “동일 조건” 실행
가장 강력한 방법 중 하나는 systemd 환경에서 직접 실행해 보는 것입니다.
sudo systemd-run --unit=myapp-debug \
--property=WorkingDirectory=/opt/myapp \
--property=User=myapp \
--property=Group=myapp \
/opt/myapp/bin/myapp --config /etc/myapp/config.yaml
로그 확인:
journalctl -u myapp-debug -b --no-pager -n 200
이 방식은 “로그인 셸에서는 되는데 서비스에서는 안 됨” 같은 문제를 빠르게 재현합니다.
11) 유닛 파일 예시: 재시작 정책을 ‘관측 가능하게’ 만들기
재시작 자체는 유용하지만, 무한 루프는 장애를 숨깁니다. 아래는 운영에서 자주 쓰는 균형점 예시입니다.
[Unit]
Description=MyApp Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=3
# 너무 빠른 재시작 폭주 방지
StartLimitIntervalSec=60
StartLimitBurst=10
# 로그는 journald로
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
핵심은 다음입니다.
Restart=always는 정말 필요한 경우가 아니면 피하고on-failure부터 시작StartLimit...로 폭주를 제한- 로그를 journald로 모아
journalctl에서 한 번에 추적
12) 체크리스트: 10분 안에 원인 범주 정하기
아래 순서대로 보면 대부분의 재시작 루프는 빠르게 분류됩니다.
systemctl status -l로Result,ExecMainStatus확인journalctl -u ... -b -n 200으로 “실패 직전 로그” 확인systemctl cat으로Type,ExecStart,User,WorkingDirectory,EnvironmentFile점검- OOM 의심 시
dmesg -T | grep -i oom - 타임아웃이면
TimeoutStartSec,Type=notify여부 확인 - 부팅 직후만 문제면
network-online.target등 의존성 점검 - 셸 문법/경로 문제면
ExecStart를 단순화하고 절대 경로 사용
마무리
systemd 재시작 루프는 “서비스가 죽는다”가 아니라, systemd가 실패로 판정하는 조건과 프로세스의 실제 종료 원인이 교차하면서 발생합니다. 따라서 감으로 유닛 파일을 바꾸기보다, status의 종료 코드와 journalctl의 실패 직전 로그를 기준으로 원인을 좁혀가야 합니다.
같은 주제를 더 체계적으로 확장한 글로 systemd 서비스가 계속 재시작될 때 원인 추적법도 함께 보면, 각 원인별로 어떤 로그/증상이 나오는지 더 빠르게 매칭할 수 있습니다.