Published on

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

Authors

서버에서 systemctl status를 보면 서비스가 activating (auto-restart)로 반복되고, 잠깐 running이었다가 다시 죽는 상황은 운영에서 자주 만납니다. 문제는 “왜 죽었는지”가 서비스 로그에 충분히 남지 않거나, systemd가 재시작을 빨리 걸어버려 원인 파악이 더 어려워진다는 점입니다.

이 글은 systemd가 자꾸 서비스를 재시작할 때, 증상 확인 → 로그 수집 → 종료 코드 해석 → 자주 터지는 원인군 점검 → 재시작 정책 조정으로 재현성 확보 순서로 원인을 좁히는 실전 체크리스트입니다.

1) 먼저: systemd가 재시작하는지, 앱이 자발적으로 종료하는지 구분

가장 먼저 확인할 것은 “systemd가 강제로 죽이고 다시 올리는지” vs “프로세스가 스스로 크래시/종료하는지”입니다.

상태/최근 이벤트 확인

systemctl status myservice -l
systemctl show myservice -p ActiveState -p SubState -p Result -p ExecMainStatus -p ExecMainCode
systemctl show myservice -p NRestarts -p Restart -p RestartUSec
  • Result=exit-code면 대개 프로세스가 비정상 종료했고 systemd가 재기동합니다.
  • Result=signal이면 시그널로 종료(예: SIGKILL)된 것입니다.
  • NRestarts가 빠르게 증가하면 재시작 루프입니다.

최근 부팅부터 해당 유닛 로그만 보기

journalctl -u myservice -b --no-pager
journalctl -u myservice -b -n 200 --no-pager

“왜 죽었는지”를 한 줄로 요약해주는 패턴

journalctl -u myservice -b --no-pager | egrep -i "failed|error|fatal|oom|killed|segfault|core dumped|permission|no such file"

여기서 핵심 단서가 안 나오면, 다음 단계로 종료 코드/시그널을 해석해야 합니다.

2) 종료 코드(Exit Code)와 시그널(Signal)로 원인 범주 좁히기

systemd는 종료 원인을 ExecMainCode, ExecMainStatus로 제공합니다.

systemctl show myservice -p ExecMainCode -p ExecMainStatus -p ExecMainPID
  • ExecMainCode=1 그리고 ExecMainStatus=1 같은 형태는 “프로세스가 exit(1)로 종료”를 의미합니다. 보통 앱 설정/환경변수/의존성 실패입니다.
  • ExecMainCode=2는 “시그널로 종료”입니다. 이때 ExecMainStatus는 시그널 번호가 들어갑니다.
    • 예: 9SIGKILL(강제 종료)인데, OOM killer나 systemd의 강제 kill 가능성이 큽니다.
    • 예: 11이면 SIGSEGV(세그폴트)로 크래시 가능성이 큽니다.

커널 OOM(Out Of Memory) 여부 확인

서비스가 SIGKILL로 죽는다면 가장 먼저 OOM을 의심합니다.

journalctl -k -b --no-pager | egrep -i "oom|out of memory|killed process"

dmesg -T | egrep -i "oom|killed process"

OOM이 맞다면:

  • 메모리 누수/버퍼 폭주/캐시 설정 등을 점검하고
  • systemd의 메모리 제한(MemoryMax)이 걸려 있는지도 확인합니다.
systemctl show myservice -p MemoryMax -p MemoryCurrent -p TasksMax

3) systemd 재시작 루프 자체를 잠깐 완화해 “로그를 남기게” 만들기

재시작이 너무 빠르면 로그가 잘리거나, 포트가 TIME_WAIT로 남아 다음 기동도 실패하며 더 혼란스러워집니다. 원인 파악 중에는 재시작 정책을 잠시 조정하는 게 좋습니다.

즉석에서 재시작 딜레이 늘리기(드롭인)

sudo systemctl edit myservice

아래 내용을 추가:

[Service]
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60
StartLimitBurst=3

적용:

sudo systemctl daemon-reload
sudo systemctl restart myservice
  • StartLimitBurst를 낮추면 무한 루프 대신 “몇 번 실패 후 멈춤” 상태가 되어 분석이 쉬워집니다.

4) 가장 흔한 원인군 1: 환경변수/작업 디렉터리/권한 문제

터미널에서 수동 실행하면 되는데 systemd로만 실패한다면, 대부분 환경 차이입니다.

유닛 파일에서 실행 경로와 사용자 확인

systemctl cat myservice

특히 아래 항목을 봅니다.

  • User= / Group=: 권한 부족으로 파일/소켓/포트 접근 실패
  • WorkingDirectory=: 상대 경로 설정 파일 로드 실패
  • Environment= / EnvironmentFile=: 환경변수 누락
  • ExecStart=: 쉘 확장/리다이렉션을 기대했는데 systemd는 그대로 실행

자주 터지는 케이스: ExecStart에서 쉘 문법 사용

아래처럼 쓰면 실패할 수 있습니다.

ExecStart=/usr/bin/myapp --config ./config.yml >> /var/log/myapp.log 2>&1

systemd는 기본적으로 쉘이 아니라 “직접 exec”이므로 리다이렉션이 동작하지 않습니다. 해결은 둘 중 하나입니다.

  1. journald로 로깅하고 StandardOutput을 사용
[Service]
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yml
StandardOutput=journal
StandardError=journal
  1. 정말 쉘이 필요하면 명시적으로 쉘 실행(권장도는 낮음)
[Service]
ExecStart=/bin/bash -lc 'exec /usr/bin/myapp --config /etc/myapp/config.yml >> /var/log/myapp.log 2>&1'

또한 설정 파일 경로는 상대 경로 대신 절대 경로를 권장합니다.

5) 가장 흔한 원인군 2: 포트 바인딩 실패, 소켓/파일 잠금

재시작 루프에서 흔히 보는 로그는 address already in use입니다. 이전 프로세스가 완전히 죽지 않았거나, 다른 프로세스가 포트를 점유 중입니다.

sudo ss -lntp | grep ":8080"
sudo lsof -i :8080

systemd가 프로세스를 죽이는 데 시간이 걸려 다음 기동이 빨라서 실패한다면:

  • RestartSec를 늘리고
  • 필요하면 TimeoutStopSec / KillSignal / KillMode를 점검합니다.
[Service]
TimeoutStopSec=30s
KillSignal=SIGTERM
KillMode=mixed

6) 가장 흔한 원인군 3: 파일 디스크립터/프로세스/스레드 제한

트래픽이 늘면 갑자기 죽고 재시작되는 서비스는 ulimit 계열 제한에 걸렸을 가능성이 큽니다.

systemd에서 제한 확인

systemctl show myservice -p LimitNOFILE -p LimitNPROC

드롭인으로 상향

[Service]
LimitNOFILE=1048576
LimitNPROC=65535

리눅스에서 “용량은 남는데 쓰기 실패”가 나오거나, 파일 생성이 안 되면 inode 고갈도 실제로 서비스 장애를 만들 수 있습니다. 디스크 사용량만 보지 말고 inode도 함께 점검하세요.

7) 가장 흔한 원인군 4: systemd 타임아웃(시작/중지)으로 강제 종료

앱이 느리게 뜨는 경우, systemd가 “시작 실패”로 판단하고 죽였다가 재시작할 수 있습니다.

타임아웃 확인

systemctl show myservice -p TimeoutStartUSec -p TimeoutStopUSec -p Type
  • Type=simple인데 실제로는 준비 완료까지 시간이 오래 걸리는 앱이라면, readiness를 systemd가 모릅니다.
  • Type=notify를 지원하는 앱이면 sd_notify를 붙여 “준비 완료”를 알려주는 방식이 안정적입니다.

일단은 임시로 타임아웃을 늘려 원인 로그를 확보합니다.

[Service]
TimeoutStartSec=120
TimeoutStopSec=60

8) 코어 덤프와 세그폴트 추적(네이티브 바이너리/CGO 등)

segfault가 보이면 journald만으로는 부족합니다. 코어 덤프를 수집하고 스택을 봐야 합니다.

coredumpctl로 확인

coredumpctl list myservice
coredumpctl info myservice

디버깅 심볼이 있으면:

coredumpctl gdb myservice

여기서 backtrace를 보면 크래시 지점을 빠르게 좁힐 수 있습니다.

9) 의존 서비스/네트워크/스토리지 문제로 “정상 종료”가 반복되는 패턴

DB나 외부 API 연결 실패 시 앱이 아예 종료하도록 만들어진 경우도 많습니다. 이때는 systemd가 잘못이 아니라 앱의 종료 정책이 문제입니다.

  • 앱이 “연결 실패 시 즉시 exit” 대신 “재시도 후 계속 실행”하도록 수정하거나
  • systemd 레벨에서 의존성 준비를 강제합니다.

의존성 선언 예시

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

[Service]
Restart=on-failure
RestartSec=3s

다만 network-online.target은 “네트워크가 완전히 준비됨”을 항상 보장하지는 않습니다. 클라우드 환경에서 DNS/라우팅/보안그룹 문제로 특정 목적지 연결만 실패할 수 있으니, 실제 장애에서는 “연결 대상” 기준으로 점검해야 합니다.

네트워크 진단 관점이 필요하다면 아래 글의 점검 방식(계층적으로 원인 좁히기)이 참고됩니다.

10) 재현 가능한 형태로 로그를 남기는 운영 팁

1) journald에 충분히 남기기

  • 앱은 표준 출력/표준 에러로 로그를 내고
  • systemd에서 journal로 수집
[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myservice

조회는 다음처럼 단순해집니다.

journalctl -u myservice -b -o short-iso --no-pager

2) 실패 시점의 환경/리소스 스냅샷 남기기

ExecStartPre 또는 OnFailure 훅을 활용하면 “죽기 직전 상태”를 남길 수 있습니다.

[Service]
ExecStartPre=/bin/sh -c 'date; id; ulimit -a; df -h; df -i; free -m'

주의: 본문에 나온 /bin/sh -c 같은 쉘 실행은 디버깅 목적에만 제한적으로 사용하세요.

3) 너무 잦은 재시작은 장애를 키운다

무한 재시작은 로그 폭주, 포트/커넥션 누수, 외부 의존성에 대한 재시도 폭탄으로 이어질 수 있습니다. 특히 분산 시스템에서 재시작이 “중복 실행”을 유발하면 데이터 정합성 이슈로 번질 수 있으니(예: 재처리/중복 결제), 애플리케이션 레벨의 멱등성도 함께 점검해야 합니다.

11) 체크리스트: 10분 안에 원인 후보 좁히기

  1. systemctl status -lResult, ExecMainStatus, NRestarts 확인
  2. journalctl -u ... -b로 직전 실패 로그 확인
  3. ExecMainCode가 시그널이면 OOM/세그폴트부터 확인
  4. 포트 바인딩/락: ss, lsof
  5. 권한/환경 차이: systemctl cat, User, WorkingDirectory, EnvironmentFile
  6. 제한: LimitNOFILE, LimitNPROC, TasksMax
  7. 디스크는 df -h뿐 아니라 df -i도 확인
  8. 타임아웃: TimeoutStartSec, TimeoutStopSec
  9. 의존성 실패 시 앱이 exit하는지, 재시도하는지 확인
  10. 재시작 정책을 완화해 로그를 확보하고, 원인 제거 후 원복

마무리

systemd 재시작 루프는 “systemd가 문제”라기보다, **종료 코드가 가리키는 원인(리소스, 권한, 설정, 의존성, 크래시)**을 빠르게 분류하는 게임에 가깝습니다. 핵심은 journalctlsystemctl show로 증거를 모으고, 재시작 정책을 잠시 조정해 재현성과 로그 품질을 확보하는 것입니다.

원인 후보를 위 순서대로 제거해 나가면, 대부분의 재시작 문제는 30분 안에 “어느 레이어가 문제인지”까지는 좁힐 수 있고, 그 다음부터는 해당 레이어(앱/OS/네트워크/스토리지)의 정밀 진단으로 자연스럽게 이어집니다.