Published on

systemd 서비스가 계속 재시작될 때 원인 추적

Authors

서버에서 systemd 서비스가 “계속 재시작(loop)” 상태에 빠지면, 증상 자체는 단순해 보여도 원인은 매우 다양합니다. 애플리케이션 크래시, 잘못된 ExecStart, 환경변수 누락, 권한 문제, 포트 충돌, 의존 서비스 미기동, 타임아웃, OOM(Out Of Memory) Killer, 파일 디스크립터/스레드 제한, 심지어는 systemd의 Restart= 설정이 너무 공격적인 경우까지 모두 같은 형태(재시작 반복)로 나타납니다.

이 글에서는 “왜 재시작되는지”를 가장 짧은 시간에 좁히는 방법을 다룹니다. 핵심은 감으로 설정을 바꾸기 전에 **systemd가 남기는 단서(Exit code, 상태 전이, 저널 로그, 코어덤프, 리소스 제한)**를 체계적으로 수집하는 것입니다.

관련해서 재부팅 이후 서비스가 안 뜨는 케이스와 점검 항목은 별도 글로 정리해 두었으니, 부팅 시점 문제까지 함께 의심된다면 같이 참고하세요: systemd 서비스가 재부팅 후 안 뜰 때 12단계 점검

1) 먼저 “재시작 주체”를 확인: systemd가 살리는가, 프로세스가 죽는가

재시작 루프는 크게 두 가지 패턴입니다.

  1. 프로세스가 즉시 종료(exit 1, segfault 등) → systemd가 Restart= 정책에 따라 다시 실행
  2. 프로세스는 살아 있는데 systemd가 실패로 판단(Type= 설정 불일치, watchdog 미응답, TimeoutStartSec 초과 등) → systemd가 kill 후 재시작

다음 명령으로 현재 상태와 최근 실패 이유를 가장 먼저 봅니다.

sudo systemctl status -l --no-pager myservice.service
sudo systemctl show myservice.service -p ActiveState,SubState,Result,ExecMainStatus,ExecMainCode,ExecMainPID,Restart,RestartUSec
  • ExecMainStatus: 마지막 종료 코드(예: 1, 127, 143)
  • ExecMainCode: exited, killed, dumped
  • Result: exit-code, signal, timeout

여기서 **Result=timeout**이면 애플리케이션 로직보다 systemd 타임아웃/Type 설정을 먼저 의심해야 합니다.

2) 재시작 루프를 잠깐 멈춰서 “관찰 가능한 상태” 만들기

루프가 심하면 로그가 밀려서 분석이 어려워집니다. 잠시 멈추고 원인을 재현할 수 있는 상태를 만듭니다.

sudo systemctl stop myservice
sudo systemctl reset-failed myservice

그리고 서비스 파일을 바로 고치기 전에, 원본을 유지한 채 override로 디버깅 옵션만 추가하는 방식을 권장합니다.

sudo systemctl edit myservice

예시(디버깅을 위해 재시작을 잠시 막고, 시작 제한을 완화):

# /etc/systemd/system/myservice.service.d/override.conf
[Service]
Restart=no

[Unit]
StartLimitIntervalSec=0

적용:

sudo systemctl daemon-reload
sudo systemctl start myservice
sudo systemctl status -l --no-pager myservice

이제 실패가 나더라도 무한 재시작이 멈춰서, 실패 순간의 로그/코드를 안정적으로 볼 수 있습니다.

3) 저널 로그로 “첫 번째 에러”를 잡아라 (마지막 로그가 아님)

재시작 루프에서 중요한 건 보통 마지막 줄이 아니라 처음 실패를 유발한 에러입니다.

# 서비스 단위로 보기
sudo journalctl -u myservice.service -b --no-pager

# 최근 200줄만
sudo journalctl -u myservice.service -b -n 200 --no-pager

# 시간 역순(최신부터)
sudo journalctl -u myservice.service -b -r --no-pager

특히 다음 패턴을 찾습니다.

  • ExecStart= 실행 자체가 실패: No such file or directory, Permission denied
  • 환경변수/설정 누락: missing env, config not found, failed to load
  • 바인딩 실패: EADDRINUSE, bind: address already in use
  • 의존 리소스 실패: DB 연결 실패, DNS 실패, 인증서 읽기 실패

시스템 전체 로그에서 커널 메시지도 확인

서비스 로그만 보면 OOM, cgroup kill 같은 커널 이벤트를 놓치기 쉽습니다.

sudo journalctl -b -k --no-pager | tail -n 200

OOM이 의심되면 아래 글의 체크리스트가 그대로 도움이 됩니다: Linux OOM Killer 로그 추적과 메모리 누수 진단

4) Exit code/Signal로 원인 분류 (가장 빠른 가지치기)

systemctl status에서 흔히 보는 종료 형태와 의미는 다음과 같습니다.

  • status=127: 실행 파일/라이브러리 못 찾음(PATH, ExecStart 경로, 동적 라이브러리)
  • status=126: 실행 권한 없음
  • status=1: 앱 내부 에러(로그로 원인 추적)
  • code=killed, status=9/KILL: systemd가 강제 종료했거나 OOM/cgroup kill
  • code=dumped, status=11/SEGV: 세그폴트(코어덤프 확인)

추가로 자세히:

sudo systemctl show myservice -p ExecMainStatus,ExecMainCode,ExecMainStartTimestamp,ExecMainExitTimestamp

5) 가장 흔한 실수 1: Type= 설정이 실제 프로세스 동작과 불일치

Type=simple(기본)은 ExecStart로 실행한 프로세스가 포그라운드에서 계속 떠있어야 합니다. 그런데 앱이 데몬화(백그라운드로 fork)하면 systemd는 “메인 PID가 끝났다”고 보고 실패/재시작할 수 있습니다.

  • 이미 자체 데몬화하는 프로그램이라면 Type=forking + PIDFile= 필요
  • 가능하면 앱은 포그라운드 실행, systemd가 데몬화 역할 담당(권장)

예시(포그라운드 실행 권장):

[Service]
Type=simple
ExecStart=/usr/local/bin/myservice --foreground
Restart=on-failure

반대로 Type=notify를 써놓고 앱이 sd_notify()를 호출하지 않으면 TimeoutStartSec 후 실패로 간주될 수 있습니다.

6) 가장 흔한 실수 2: ExecStart 경로/권한/사용자 문제

다음은 “재시작 루프처럼 보이지만 사실은 실행 자체가 안 되는” 케이스입니다.

ExecStart 경로 검증

sudo systemctl cat myservice
sudo ls -al /usr/local/bin/myservice
sudo file /usr/local/bin/myservice

서비스 계정으로 직접 실행해보기

systemd 환경과 100% 동일하진 않지만, 권한/파일 접근 문제를 빠르게 잡을 수 있습니다.

sudo -u myuser -s
/usr/local/bin/myservice --config /etc/myservice/config.yml

WorkingDirectory, EnvironmentFile 누락 점검

[Service]
WorkingDirectory=/var/lib/myservice
EnvironmentFile=/etc/default/myservice

EnvironmentFile 경로가 없으면 서비스가 실패할 수 있습니다(접두 -를 붙이면 없어도 무시).

EnvironmentFile=-/etc/default/myservice

7) StartLimitHit: 너무 빨리 죽어서 systemd가 “포기”하는 경우

재시작 루프가 계속되다가 어느 순간 Start request repeated too quickly로 멈추는 경우가 있습니다. 이는 systemd의 시작 제한 정책에 걸린 것입니다.

sudo systemctl status -l myservice
sudo systemctl show myservice -p StartLimitIntervalSec,StartLimitBurst

원인 해결 전까지는 제한을 풀기보다(Interval=0) 왜 빨리 죽는지를 잡는 게 우선입니다. 다만 디버깅 중에는 일시적으로 완화할 수 있습니다.

[Unit]
StartLimitIntervalSec=30
StartLimitBurst=20

[Service]
RestartSec=2

8) TimeoutStartSec/Watchdog: “죽은 게 아니라 느린 것”일 수 있다

서비스가 실제로는 정상 부팅 중인데 초기화가 길어 TimeoutStartSec을 초과하면 systemd는 실패로 처리하고 kill 후 재시작합니다.

  • DB 마이그레이션, 캐시 워밍, 대용량 파일 스캔 등
  • 네트워크 의존성이 있는데 After=network-online.target 없이 시작

점검:

sudo systemctl show myservice -p TimeoutStartUSec,WatchdogUSec

해결(예시):

[Unit]
After=network-online.target
Wants=network-online.target

[Service]
TimeoutStartSec=180

또는 “초기화 작업”을 서비스 시작과 분리해 oneshot 유닛으로 떼는 것도 좋습니다.

9) OOM / 메모리 제한 / cgroup kill 확인

서비스가 갑자기 Killed로 끝나고 재시작된다면 OOM 또는 cgroup 제한을 강하게 의심합니다.

systemd 관점에서 리소스 제한 확인

sudo systemctl show myservice -p MemoryMax,MemoryHigh,CPUQuota,TasksMax,LimitNOFILE

커널 OOM 로그 확인

sudo journalctl -b -k --no-pager | grep -E "Out of memory|Killed process|oom-killer" -n

OOM 분석/메모리 누수 진단은 아래 글의 절차(로그에서 victim 찾기, cgroup 메모리, RSS 추적)가 그대로 적용됩니다.

추가로 systemd에서 명시적으로 메모리 상한을 줬다면, 실제 워킹셋보다 너무 낮지 않은지 확인하세요.

[Service]
MemoryMax=500M
# 필요 시 상향 또는 제거

10) 파일 디스크립터/스레드/프로세스 제한으로 인한 즉시 종료

고부하 서비스는 Too many open files(EMFILE)로 죽고 재시작될 수 있습니다.

현재 제한 확인

sudo systemctl show myservice -p LimitNOFILE,LimitNPROC,TasksMax

상향 예시

[Service]
LimitNOFILE=1048576
TasksMax=4096

애플리케이션이 FD를 누수하는 경우도 있으니, 단순 상향만으로 숨기지 말고 앱 로그/메트릭도 같이 봐야 합니다.

11) 포트 충돌(EADDRINUSE)과 이중 실행

서비스가 재시작되며 포트 바인딩에 실패하면 계속 루프를 돌 수 있습니다.

sudo ss -lntp | grep ':8080'
sudo lsof -iTCP:8080 -sTCP:LISTEN -n -P
  • 이전 인스턴스가 완전히 종료되지 않았거나
  • 다른 서비스가 같은 포트를 점유하거나
  • systemd가 KillMode=process 등으로 자식 프로세스를 남겨두는 설정일 수 있습니다.

일반적으로는 기본값인 KillMode=control-group가 안전합니다(서비스의 모든 프로세스를 함께 정리).

12) 코어덤프로 크래시 원인 잡기 (SEGV, ABRT)

code=dumped 또는 segfault가 보이면 코어덤프를 확인합니다.

coredumpctl list myservice
coredumpctl info myservice
# gdb가 설치되어 있다면
coredumpctl gdb myservice

스택 트레이스에서 라이브러리 버전 불일치, null deref, native addon 문제 등을 확인할 수 있습니다.

13) 서비스 파일에서 자주 문제를 만드는 옵션들

재시작 루프를 “악화”시키거나 원인을 가리는 옵션들이 있습니다.

  • Restart=always: 정상 종료까지도 재시작 → 배치/oneshot에 쓰면 지옥
  • RestartSec=0: 너무 빠른 재시작으로 로그 폭주, StartLimitHit 유발
  • SuccessExitStatus= 미설정: 앱이 특정 코드로 종료하는 것을 실패로 인식

예시: 앱이 exit 143(SIGTERM)을 정상 종료로 취급해야 하는 경우

[Service]
Restart=on-failure
SuccessExitStatus=143

또한 임시로 디버깅할 때는 Restart=no로 고정해 “한 번 실패하고 멈추게” 만든 뒤 원인 로그를 보는 게 빠릅니다.

14) 실전 디버깅 템플릿: 재시작 루프 10분 컷 절차

아래 순서대로 하면 대부분의 케이스에서 원인을 빠르게 좁힙니다.

  1. systemctl status -lResult/Exit code/Signal 확인
  2. journalctl -u -b첫 실패 지점 찾기
  3. Result=timeout이면 Type/TimeoutStartSec/notify/watchdog부터 점검
  4. code=killed면 OOM/cgroup/리소스 제한 확인
  5. status=126/127이면 경로/권한/동적 라이브러리 확인
  6. 포트 충돌/의존 서비스/환경변수/WorkingDirectory 확인
  7. 필요 시 systemctl editRestart=no 적용 후 단발 재현
  8. 크래시면 coredumpctl로 스택 트레이스 확보

15) 예시: 재시작 루프를 “관찰 가능”하게 만드는 최소 유닛

마지막으로, 원인 추적을 위해 많이 쓰는 최소 단위 예시를 공유합니다.

# /etc/systemd/system/myservice.service
[Unit]
Description=MyService
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myuser
Group=myuser
WorkingDirectory=/var/lib/myservice
EnvironmentFile=-/etc/default/myservice
ExecStart=/usr/local/bin/myservice --config /etc/myservice/config.yml

# 디버깅 중엔 재시작을 끄고, 원인 해결 후 on-failure로 변경
Restart=on-failure
RestartSec=2

# 리소스 관련(필요 시)
LimitNOFILE=65536

# 로그를 저널로 확실히 모으기
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

적용:

sudo systemctl daemon-reload
sudo systemctl enable --now myservice
sudo systemctl status -l myservice

마무리

systemd 재시작 루프는 “서비스가 불안정하다”는 결과만 보여주지만, 실제 원인은 Exit code/timeout/OOM/권한/Type 불일치/리소스 제한처럼 서로 다른 층에 존재합니다. 설정을 이것저것 바꾸기 전에, systemd가 이미 제공하는 신호(상태 필드 + 저널 + 커널 로그 + 코어덤프)를 기반으로 원인을 분류하면 디버깅 시간이 급격히 줄어듭니다.

특히 Killed/OOM 징후가 보이면 커널 로그와 cgroup 메모리 제한을 함께 보는 것이 핵심이며, 이 경우는 아래 글의 절차를 그대로 따라가면 재현과 해결이 빨라집니다: Linux OOM Killer 로그 추적과 메모리 누수 진단