- Published on
systemd 서비스 재시작 루프 진단 - 로그·유닛·쉘
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 systemd 서비스가 “떴다 죽었다”를 반복하면, 표면 증상은 단순합니다. Active: activating (auto-restart) 혹은 failed가 번갈아 보이고, 재시작 카운터가 빠르게 증가합니다. 하지만 원인은 크게 세 갈래로 갈립니다.
- 로그가 말해주는 즉시 원인: 포트 점유, 권한, 파일 누락, 설정 파싱 실패, OOM 등
- 유닛(Unit) 설정이 만든 재시작 루프:
Restart=/RestartSec=/StartLimit*=/Type=/Timeout*=조합 - 쉘/실행 환경 문제:
ExecStart가 기대한 환경변수·작업 디렉터리·PATH·권한을 못 받거나, 스크립트가 잘못된 종료 코드를 내는 경우
이 글은 “로그 → 유닛 → 쉘” 순서로 최단 시간에 재시작 루프를 끝내는 진단 루틴을 제공합니다.
관련해서 더 넓은 관점의 원인 추적은 systemd 서비스가 계속 재시작될 때 원인 추적도 함께 참고하면 좋습니다.
1) 증상 고정: 지금 무슨 상태인지 먼저 캡처
재시작 루프는 시간이 지나면 로그가 밀리고, StartLimit에 걸리면 또 양상이 바뀝니다. 먼저 아래 3개를 캡처해 “현 상태”를 고정하세요.
systemctl status myapp.service -n 200 --no-pager
systemctl show myapp.service -p ActiveState -p SubState -p NRestarts -p Result
systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p MainPID
ExecMainStatus가 가장 중요한 힌트입니다. 일반적으로0이면 정상 종료,1이상이면 오류 종료입니다.ExecMainCode가exited인지killed인지 구분하세요.killed면 OOM 또는 시그널 종료 가능성이 큽니다.
재시작 루프를 잠깐 멈추는 방법(진단용)
원인 분석 중 로그가 폭주하면 잠시 멈추는 게 좋습니다.
sudo systemctl stop myapp.service
sudo systemctl reset-failed myapp.service
서비스를 “아예 다시 못 뜨게” 막고 싶다면 마스킹도 가능합니다.
sudo systemctl mask myapp.service
# 해제
sudo systemctl unmask myapp.service
2) 로그 진단: journalctl을 ‘정확히’ 보는 법
systemctl status는 요약만 보여줍니다. 루프의 직접 원인은 대부분 journalctl에 있습니다.
최근 부팅부터 서비스 로그만 보기
journalctl -u myapp.service -b --no-pager -n 300
시간 범위를 좁혀 재시작 직전/직후를 보기
journalctl -u myapp.service --since "10 min ago" --no-pager
재시작 루프에서 특히 유용한 옵션
-o short-precise: 밀리초 단위 타임스탬프-o cat: 메타데이터 없이 메시지만
journalctl -u myapp.service -b -o short-precise --no-pager
자주 나오는 로그 패턴과 의미
1) status=203/EXEC
ExecStart에 지정한 바이너리/스크립트를 실행하지 못했다는 뜻입니다.
대표 원인:
- 경로 오타
- 실행 권한 없음(
chmod +x누락) - 인터프리터 경로 문제(예: shebang이 잘못됨)
No such file or directory지만 실제로는 동적 링커/라이브러리 누락
확인:
systemctl cat myapp.service
ls -al /path/to/exec
file /path/to/exec
ldd /path/to/exec 2>/dev/null | sed -n '1,120p'
2) status=1/FAILURE 혹은 애플리케이션 자체 에러
이 경우는 서비스가 “실행은 됐는데 바로 죽는” 상황입니다. 애플리케이션 로그(표준출력/표준에러)가 journalctl에 남는지부터 확인하세요.
만약 출력이 없으면 StandardOutput/StandardError 설정 문제일 수도 있습니다(뒤에서 다룹니다).
3) start-limit-hit
너무 빨리/자주 재시작되어 systemd가 더 이상 시도하지 않는 상태입니다.
systemctl status myapp.service --no-pager | sed -n '1,80p'
이 상태에서는 원인을 고친 후에도 아래를 해줘야 다시 시도합니다.
sudo systemctl reset-failed myapp.service
sudo systemctl start myapp.service
4) Killed process ... out of memory
커널 OOM으로 죽은 케이스입니다.
dmesg -T | grep -i -E "oom|killed process" | tail -n 50
journalctl -k -b --no-pager | grep -i -E "oom|killed process" | tail -n 80
메모리 압박이 디스크/인오드 문제와 함께 터지는 경우도 자주 있습니다. 디스크가 꽉 차면 로그/임시파일/소켓 생성 실패로 연쇄 장애가 나기도 하니 리눅스 디스크 100%? inode 고갈 진단·복구 실전도 같이 점검해보세요.
3) 유닛(Unit) 진단: 재시작을 만든 설정을 찾아라
로그에서 애플리케이션 오류가 명확하면 그걸 고치면 되지만, 많은 경우 유닛 설정이 상황을 악화시킵니다.
유닛 원문 확인: drop-in까지 포함해서 보기
systemctl cat myapp.service
운영 환경에서는 /etc/systemd/system/myapp.service.d/override.conf 같은 drop-in이 숨어 있는 경우가 많습니다.
핵심 필드만 빠르게 덤프
systemctl show myapp.service \
-p FragmentPath -p DropInPaths \
-p Type -p Restart -p RestartSec \
-p StartLimitIntervalUSec -p StartLimitBurst \
-p TimeoutStartUSec -p TimeoutStopUSec \
-p ExecStart -p WorkingDirectory -p User -p Group \
-p Environment -p EnvironmentFile \
--no-pager
Restart=가 루프를 만드는 전형적인 경우
Restart=always로 되어 있고- 애플리케이션이 설정 오류로 즉시 종료하며
RestartSec=0또는 매우 짧음
이면 로그 폭주 + CPU/IO 낭비가 발생합니다.
진단 중에는 임시로 재시작 정책을 완화할 수 있습니다.
sudo systemctl edit myapp.service
아래처럼 drop-in을 추가합니다.
[Service]
Restart=no
적용:
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
원인 해결 후 원복하세요.
Type= 불일치로 “정상인데 죽었다고 판단”하는 케이스
Type=forking인데 실제로는 포그라운드 프로세스Type=simple인데 실제로는 데몬화(포크 후 부모 종료)
이 경우 systemd가 메인 PID를 잘못 추적해 서비스가 죽었다고 보고 재시작할 수 있습니다.
확인 포인트:
systemctl show myapp.service -p Type -p MainPID -p ControlPID -p GuessMainPID --no-pager
대부분의 현대 서버 프로세스는 포그라운드 실행 + Type=simple 또는 준비 신호를 쓰면 **Type=notify**가 안정적입니다.
TimeoutStartSec= 때문에 “느린 기동”이 실패로 처리되는 케이스
DB 마이그레이션, 캐시 워밍업, 대형 설정 로딩 등으로 기동이 길어지면 TimeoutStartSec에 걸려 강제 종료되고 재시작 루프가 됩니다.
systemctl show myapp.service -p TimeoutStartUSec --no-pager
증상은 로그에 start operation timed out 비슷한 메시지로 남습니다.
StartLimit*=로 인해 “어느 순간부터 아예 안 뜨는” 케이스
재시작 루프가 심하면 결국 start-limit-hit로 굳습니다.
StartLimitBurst가 작고RestartSec가 짧으면
금방 막힙니다. 진단 중에는 값을 늘려 원인 파악 시간을 벌 수도 있지만, 근본적으로는 즉시 종료를 유발하는 원인을 제거해야 합니다.
4) 쉘/실행 환경 진단: 같은 명령을 수동 실행해도 재현되나
서비스는 로그인 쉘과 환경이 다릅니다.
PATH가 다름HOME이 비어 있거나 다름WorkingDirectory가 다름- 권한/그룹/umask가 다름
EnvironmentFile이 로드 실패해도 조용히 망가지는 경우가 있음
4-1) ExecStart를 “그대로” 뽑아서 수동 실행
systemctl show myapp.service -p ExecStart --no-pager
출력된 커맨드를 복사해 수동 실행해보되, 반드시 같은 사용자로 실행해야 의미가 있습니다.
sudo -u myapp -H bash -lc '/path/to/exec --flag1 --flag2'
-H는 HOME을 대상 사용자로 맞추는 데 도움이 됩니다.bash -lc는 로그인 쉘처럼 초기화를 약간 더 흉내냅니다. 단, 서비스 환경과 완전히 같지는 않습니다.
4-2) WorkingDirectory, 상대경로 의존성 확인
유닛에서 WorkingDirectory가 없으면 기본은 /입니다. 애플리케이션이 상대경로로 설정 파일을 읽는다면 바로 죽을 수 있습니다.
systemctl show myapp.service -p WorkingDirectory --no-pager
해결은 보통 유닛에 명시합니다.
[Service]
WorkingDirectory=/opt/myapp
4-3) EnvironmentFile 로드 실패/값 누락
EnvironmentFile이 가리키는 파일이 없거나 권한이 없으면 환경변수가 비어 서비스가 즉시 종료할 수 있습니다.
systemctl show myapp.service -p EnvironmentFile -p Environment --no-pager
파일 존재/권한:
sudo ls -al /etc/myapp/myapp.env
sudo -u myapp cat /etc/myapp/myapp.env
4-4) 스크립트 ExecStart의 흔한 함정: 종료 코드와 파이프
ExecStart=/bin/bash -c 'cmd1 | cmd2' 형태에서 cmd1이 실패해도 파이프 때문에 성공처럼 보이거나, 반대로 set -e 때문에 의도치 않게 즉시 종료할 수 있습니다.
권장 패턴:
[Service]
ExecStart=/bin/bash -lc 'set -euo pipefail; /opt/myapp/bin/myapp --config /etc/myapp/config.yml'
set -euo pipefail로 실패를 빠르게 드러내고-l로 최소한의 로그인 환경을 맞추되- 가능한 한 최종적으로는 “스크립트가 아니라 바이너리 직접 실행”으로 단순화하는 게 가장 좋습니다.
4-5) 표준출력/표준에러가 어디로 가는지 확인
로그가 비어 보이면 출력이 다른 곳으로 가는 중일 수 있습니다.
systemctl show myapp.service -p StandardOutput -p StandardError --no-pager
진단 단계에서는 journal로 모으는 편이 편합니다.
[Service]
StandardOutput=journal
StandardError=journal
5) 재현과 격리: systemd-run으로 “서비스 환경”에 가깝게 테스트
수동 실행이 애매하면 systemd-run으로 유사 환경을 만들어 재현성을 높일 수 있습니다.
sudo systemd-run --unit myapp-debug \
--property=User=myapp \
--property=WorkingDirectory=/opt/myapp \
--property=Environment="ENV=prod" \
/opt/myapp/bin/myapp --config /etc/myapp/config.yml
journalctl -u myapp-debug -b --no-pager -n 200
이 방식은 “유닛으로 돌릴 때만 실패”하는 문제(권한, 경로, 환경)를 잡는 데 특히 효과적입니다.
6) 실전 체크리스트: 15분 안에 끝내는 순서
아래 순서대로 하면 대부분의 재시작 루프는 빠르게 방향이 잡힙니다.
systemctl status로ExecMainStatus,Result,NRestarts확인journalctl -u -b로 재시작 직전 로그 확인status=203/EXEC면 경로/권한/인터프리터/라이브러리부터- OOM 의심이면
journalctl -k와dmesg -T확인 systemctl cat로 drop-in 포함 유닛 전체 확인Restart=/Type=/TimeoutStartSec=/WorkingDirectory/EnvironmentFile점검sudo -u로 동일 사용자 수동 실행 재현- 필요하면
systemd-run으로 유사 환경 재현 - 진단 중에는
Restart=nodrop-in으로 루프를 잠깐 끊고 원인 해결
7) 쿠버네티스 CrashLoopBackOff와의 공통점
systemd 재시작 루프는 쿠버네티스의 CrashLoopBackOff와 구조가 유사합니다.
- “프로세스가 빨리 죽는다”는 사실은 같고
- 차이는 “런타임과 선언(유닛/매니페스트)이 무엇을 재시작 조건으로 보느냐”입니다.
컨테이너 환경의 관점으로 원인 분류를 확장하고 싶다면 K8s CrashLoopBackOff 원인 10가지·즉시 진단법도 도움이 됩니다.
8) 예시: 재시작 루프를 줄이고 원인 노출을 강화한 유닛
아래는 흔한 웹/워커 계열 서비스를 가정한 예시입니다.
[Unit]
Description=MyApp API
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.yml
# 진단/운영 안정화 포인트
Restart=on-failure
RestartSec=3s
StartLimitIntervalSec=60
StartLimitBurst=10
TimeoutStartSec=60
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
# 필요 시 리소스 제한(OOM/폭주 방지)
# MemoryMax=500M
# CPUQuota=200%
[Install]
WantedBy=multi-user.target
적용 커맨드:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service --no-pager -n 200
마무리
systemd 재시작 루프는 “로그만 보면 될 것 같은데” 실제로는 유닛 설정과 실행 환경이 원인을 숨기거나 증폭시키는 경우가 많습니다. journalctl로 직접 원인을 잡고, systemctl show로 유닛의 재시작 조건과 타입/타임아웃을 점검한 뒤, 마지막으로 sudo -u 및 systemd-run으로 환경 차이를 좁히면 대부분의 케이스는 짧은 시간 안에 해결 방향이 나옵니다.