- Published on
systemd 서비스가 계속 재시작될 때 원인 추적법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 서비스가 “죽었다가 살아나고”를 반복하면, 겉보기엔 systemd가 문제처럼 보이지만 실제로는 애플리케이션 종료 코드, 타임아웃, 환경 변수, 권한, 의존성, 리소스 제한 같은 운영 조건이 원인인 경우가 대부분입니다. 이 글은 systemd 서비스가 계속 재시작될 때, 재현 가능한 절차로 원인을 좁혀가는 방법을 정리합니다.
참고로 이 현상은 쿠버네티스의 CrashLoopBackOff와 유사한 패턴입니다. 컨테이너 환경에서의 진단 흐름이 궁금하다면 K8s CrashLoopBackOff 원인별 10분 진단법도 함께 보면 사고 대응 속도가 빨라집니다.
1) 먼저 “재시작”이 진짜인지 확인하기
가장 먼저 해야 할 일은 “서비스가 실제로 프로세스 크래시로 종료되는지”, “설정 때문에 systemd가 재기동하는지”, “헬스체크/워치독 때문에 재시작되는지”를 구분하는 것입니다.
상태와 최근 이벤트 확인
systemctl status myservice.service
systemctl show myservice.service -p ActiveState -p SubState -p Result -p NRestarts
Result=가exit-code,signal,timeout중 무엇인지가 1차 분기점입니다.NRestarts=가 증가하는지 확인합니다.
Restart 정책 확인
systemctl cat myservice.service
systemctl show myservice.service -p Restart -p RestartUSec -p StartLimitBurst -p StartLimitIntervalUSec
Restart=always또는Restart=on-failure가 설정되어 있으면 “실패 시 자동 재시작”은 정상 동작입니다.StartLimitBurst와StartLimitIntervalUSec는 “짧은 시간 내 재시작 횟수 제한”입니다. 제한에 걸리면 재시작이 멈추고start-limit-hit가 뜹니다.
2) 로그를 ‘정확히’ 보는 법: journalctl 필터링
재시작 루프는 로그가 금방 밀립니다. “이번 부팅에서”, “해당 유닛만”, “최근 N분만” 같은 필터로 빠르게 핵심을 뽑아야 합니다.
# 이번 부팅에서 해당 유닛 로그
journalctl -u myservice.service -b
# 최근 10분
journalctl -u myservice.service --since "10 min ago"
# 가장 최근 로그부터
journalctl -u myservice.service -b -r
# systemd 자체 메시지도 같이(유닛 + PID/우선순위로)
journalctl -b _SYSTEMD_UNIT=myservice.service
여기서 확인할 포인트는 다음입니다.
- 종료 직전의 에러 스택/메시지
Main process exited, code=exited, status=...또는code=killed, status=...Watchdog timeout또는Start request repeated too quickly
3) 종료 코드로 원인 분류하기
systemd는 종료 이유를 꽤 정확히 알려줍니다. 아래 분류를 기준으로 원인을 좁히면 빠릅니다.
A. status=1 같은 일반 종료 코드
애플리케이션이 스스로 exit 1로 종료한 케이스입니다. 보통은 설정/환경/의존성 문제입니다.
- 환경 변수 미설정
- 설정 파일 경로 오류
- DB, Redis, 외부 API 연결 실패
- 포트 바인딩 실패
진단 팁:
# 유닛에 설정된 환경 확인
systemctl show myservice.service -p Environment -p EnvironmentFiles
# 실제 실행 커맨드 확인
systemctl show myservice.service -p ExecStart
B. code=killed, status=9/KILL 또는 status=137 비슷한 패턴
대개 OOM(메모리 부족) 또는 외부에서 강제 종료한 케이스입니다.
# 커널 OOM 로그 확인
journalctl -k -b | grep -i -E "oom|killed process|out of memory"
# cgroup 메모리 제한 확인(유닛에 MemoryMax가 있는지)
systemctl show myservice.service -p MemoryMax -p MemoryCurrent
Java 계열이라면 힙 설정과 컨테이너/서버 메모리의 관계를 함께 봐야 합니다.
C. Result=timeout 또는 start operation timed out
TimeoutStartSec 내에 “서비스가 기동 완료 상태”로 올라오지 못해 systemd가 실패 처리하는 경우입니다.
systemctl show myservice.service -p TimeoutStartUSec -p TimeoutStopUSec
- 기동이 느린 서비스라면
TimeoutStartSec를 늘리거나, Type=설정이 잘못되어 systemd가 “기동 완료”를 기다리다 타임아웃 나는지 확인해야 합니다.
4) 유닛 파일에서 자주 터지는 설정 실수들
4-1. Type=simple vs Type=forking vs Type=notify
Type=simple:ExecStart로 실행한 프로세스가 메인 프로세스Type=forking: 데몬이 포크 후 부모가 종료하는 형태(전통적인 데몬)Type=notify: 프로세스가 systemd에 readiness를 통지
forking 데몬인데 simple로 두면 “부모 종료”를 실패로 오해하거나 메인 PID 추적이 꼬일 수 있습니다.
4-2. ExecStart에 셸 문법을 그대로 넣는 문제
systemd의 ExecStart=는 기본적으로 셸이 아니라 “직접 실행”입니다. 리다이렉션, 파이프, && 같은 셸 문법을 쓰면 실패하거나 예상과 다르게 동작합니다.
잘못된 예:
ExecStart=/usr/bin/myapp --port 8080 >> /var/log/myapp.log 2>&1
해결 1: 셸을 명시적으로 사용
ExecStart=/bin/bash -lc 'exec /usr/bin/myapp --port 8080 >> /var/log/myapp.log 2>&1'
해결 2: systemd 로깅 사용(권장)
StandardOutput=journal
StandardError=journal
4-3. Restart=가 과하게 공격적인 경우
크래시가 아니라 “정상 종료”인데도 계속 재시작되는 경우가 있습니다.
- 배치 작업(한 번 실행하고 끝나는 성격)인데
Restart=always ExecStart가 즉시 종료되는 커맨드(예: 설정 검증만 하고 끝)
배치 작업이라면 Restart=no 또는 Restart=on-failure로 바꾸고, Type=oneshot도 고려하세요.
5) 의존성/네트워크 준비 문제: 부팅 직후만 죽는 경우
서비스가 부팅 직후에만 반복 재시작되다가 시간이 지나면 안정화된다면, 대개 의존 대상이 준비되기 전에 먼저 뜨려다가 실패하는 패턴입니다.
5-1. After=와 Wants= 점검
[Unit]
Wants=network-online.target
After=network-online.target
- 단순
network.target는 “네트워크 스택이 올라옴” 정도라, 실제 라우팅/DNS가 준비되지 않았을 수 있습니다. - 클라우드 환경에서는
network-online.target가 체감상 중요합니다.
5-2. DB/외부 의존성 재시도 전략
애플리케이션이 DB 연결 실패 시 즉시 종료하면 systemd 재시작 루프가 됩니다. 이때는 앱 레벨에서 백오프/재시도를 넣거나, 준비될 때까지 기다리는 래퍼를 둘 수 있습니다.
간단한 대기 래퍼 예시:
#!/usr/bin/env bash
set -euo pipefail
host="${DB_HOST:-127.0.0.1}"
port="${DB_PORT:-5432}"
for i in $(seq 1 60); do
if nc -z "$host" "$port"; then
exec /usr/bin/myapp
fi
sleep 1
done
echo "DB not ready" >&2
exit 1
이런 문제는 애플리케이션 쪽 커넥션/풀 설정 문제로도 이어질 수 있습니다. Java/Spring 계열에서 DB 관련 이상 징후가 반복된다면 Spring Boot DB 커넥션 누수? HikariCP 원인 9가지도 함께 점검하면 좋습니다.
6) 권한/경로/환경 변수 문제: 운영에서 가장 흔한 원인
로컬에서는 되는데 systemd로만 띄우면 죽는 경우, 대부분은 “실행 유저/작업 디렉토리/환경 변수” 차이입니다.
6-1. User=, Group=, WorkingDirectory= 확인
[Service]
User=myuser
Group=myuser
WorkingDirectory=/opt/myapp
- 로그 파일을 직접 쓰도록 해두었다면 해당 경로에 쓰기 권한이 있는지 확인
- 상대 경로로 설정 파일을 찾는 앱이라면
WorkingDirectory가 매우 중요
6-2. EnvironmentFile= 경로와 형식
EnvironmentFile=/etc/myapp/myapp.env
- 파일 권한(읽기 가능 여부)
KEY=value형식 준수 여부- 따옴표/공백 처리 문제
점검:
sudo -u myuser env -i bash -lc 'source /etc/myapp/myapp.env; env | sort | head'
7) 리소스 제한(systemd cgroup) 때문에 죽는 경우
systemd는 서비스별로 cgroup을 만들고 제한을 걸 수 있습니다. 특히 LimitNOFILE, TasksMax, MemoryMax 같은 값이 낮으면 특정 트래픽/부하에서만 재시작 루프가 생깁니다.
7-1. 파일 디스크립터 부족(Too many open files)
systemctl show myservice.service -p LimitNOFILE
journalctl -u myservice.service -b | grep -i "too many open files"
유닛에 설정 추가:
[Service]
LimitNOFILE=1048576
7-2. 프로세스/스레드 제한(TasksMax)
systemctl show myservice.service -p TasksMax -p TasksCurrent
필요 시:
[Service]
TasksMax=8192
8) 워치독(Watchdog) 때문에 재시작되는 경우
WatchdogSec가 설정되어 있고 애플리케이션이 systemd에 주기적으로 “살아있음”을 통지하지 않으면 systemd가 강제 재시작합니다.
systemctl show myservice.service -p WatchdogUSec -p Type
Type=notify인데 앱이sd_notify를 하지 않으면 워치독 타임아웃이 납니다.- 이 경우
WatchdogSec를 제거하거나, 애플리케이션에서 notify를 구현해야 합니다.
9) 재시작 루프를 “멈추고” 디버깅하기
루프가 너무 빨라 로그/상태 확인이 어려우면, 일단 재시작을 멈춰 분석 시간을 확보하세요.
9-1. 임시로 Restart 끄기(드롭인 사용)
sudo systemctl edit myservice.service
아래 내용 추가:
[Service]
Restart=no
적용:
sudo systemctl daemon-reload
sudo systemctl restart myservice.service
9-2. 동일 환경으로 수동 실행
systemd와 동일한 유저/작업 디렉토리/환경으로 실행해보면 원인이 바로 드러나는 경우가 많습니다.
sudo -u myuser -H bash -lc 'cd /opt/myapp && /usr/bin/myapp'
10) 실전 체크리스트(10분 컷 흐름)
systemctl status에서Result와 종료 코드 확인journalctl -u ... -b -r로 종료 직전 로그 확보systemctl cat으로Restart,Type,ExecStart,EnvironmentFile확인timeout이면TimeoutStartSec와Type의심killed면 OOM/리소스 제한(MemoryMax, 커널 OOM 로그) 의심- 부팅 직후만 실패하면
network-online.target및 의존성 준비 문제 의심 - 로컬 OK, systemd만 실패면
User,WorkingDirectory, 환경 변수/권한 의심
11) 예시 유닛 파일(안전한 기본값)
아래는 웹 서비스 계열에서 자주 쓰는 “무난한” 템플릿입니다. 환경에 맞게 조정하세요.
[Unit]
Description=My Service
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=myuser
Group=myuser
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/myapp
# 로그는 journal로
StandardOutput=journal
StandardError=journal
# 실패 시에만 재시작
Restart=on-failure
RestartSec=3
# 리소스 기본 튜닝(필요 시 조정)
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
마무리
systemd 재시작 루프는 “systemd가 미친 것”이 아니라, 대개 종료 코드와 유닛 설정이 알려주는 신호를 놓쳐서 길어지는 경우가 많습니다. Result와 종료 코드로 1차 분류하고, journalctl로 종료 직전 로그를 확보한 뒤, Type/ExecStart/Timeout/Restart/Environment/User를 순서대로 체크하면 대부분 10분 내에 원인을 좁힐 수 있습니다.
만약 서비스가 컨테이너로도 배포된다면, 동일한 증상이 쿠버네티스에서는 CrashLoopBackOff로 나타납니다. 운영 관점의 빠른 분기법은 K8s Pod CrashLoopBackOff 원인 7가지와 해결도 참고해보세요.