- Published on
systemd 서비스가 계속 재시작될 때 7단계 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 리눅스 서버에서 systemd 서비스가 계속 재시작되면, 증상 자체보다 재시작을 유발한 트리거를 먼저 분리해내는 게 핵심입니다. 보통은 설정 실수, 권한, 환경변수 누락, 포트 충돌, 의존 서비스 미기동, 리소스 제한, 혹은 크래시로 인한 Restart= 정책 발동 중 하나로 수렴합니다.
이 글은 “왜 계속 재시작되는지”를 감으로 찍지 않고, 7단계 체크리스트로 원인을 빠르게 좁히는 방식으로 구성했습니다. 각 단계는 다음 단계의 입력을 만들도록 설계되어 있어, 순서대로 진행하면 재현과 수정이 쉬워집니다.
참고: 컨테이너 환경에서 주기적 재시작은
systemd가 아니라livenessProbe가 원인인 경우도 많습니다. 쿠버네티스 쪽 증상이 의심되면 EKS Pod 1분마다 재시작? livenessProbe 실패 해결도 함께 확인하세요.
1단계: 재시작 패턴부터 고정하기 (Restart 정책 vs 외부 요인)
먼저 “서비스가 스스로 재시작되는지”, “누군가가 자꾸 시작을 걸고 있는지”를 분리합니다.
확인 포인트
systemd의Restart=정책이 발동하는가StartLimitIntervalSec=및StartLimitBurst=에 걸려 실패 루프가 끊기는가- 외부에서
systemctl restart가 반복 호출되는가 (배포 스크립트, 크론, 감시 데몬 등)
명령어
systemctl status myservice --no-pager
systemctl show myservice -p Restart -p RestartSec -p StartLimitIntervalUSec -p StartLimitBurst
journalctl -u myservice -b --no-pager | tail -n 200
해석 팁
Active: activating (auto-restart)가 보이면systemd정책 기반 재시작일 확률이 큽니다.- 로그에
Start request repeated too quickly가 보이면 실패 루프가 너무 빠르다는 뜻입니다.
재시작이 너무 빨라 로그를 읽기 어려우면, 임시로 재시작을 끄고 원인을 관찰합니다.
sudo systemctl edit myservice
오버라이드 파일에 아래를 추가합니다.
[Service]
Restart=no
적용:
sudo systemctl daemon-reload
sudo systemctl restart myservice
2단계: 종료 코드와 실패 타입을 숫자로 확정하기
“그냥 죽었다”가 아니라 어떤 종료 코드로 왜 죽었는지를 확정해야 다음 단계가 빨라집니다.
명령어
systemctl status myservice --no-pager
systemctl show myservice -p ExecMainStatus -p ExecMainCode -p Result
자주 보는 케이스
ExecMainStatus=1혹은Result=exit-code: 애플리케이션이 비정상 종료Result=signal및ExecMainCode=killed: 시그널로 종료 (예:SIGKILL)Result=timeout:TimeoutStartSec=내에 기동 완료를 못 함
여기서 timeout이면 5단계(의존성/네트워크/준비상태)로 바로 넘어가는 게 효율적입니다.
3단계: journalctl에서 “첫 번째 에러”를 잡아내기
재시작 루프에서는 마지막 로그가 아니라 첫 번째 실패 원인이 가장 중요합니다. 특히 설정 파싱 실패, 파일 접근 권한 실패, 환경변수 누락은 로그 초반에 나옵니다.
명령어
journalctl -u myservice -b --no-pager
journalctl -u myservice -b -p err..alert --no-pager
journalctl -u myservice -b -o cat --no-pager | sed -n '1,200p'
체크리스트
Permission denied(파일/디렉터리/소켓)No such file or directory(바이너리 경로, 설정 파일, 작업 디렉터리)Address already in use(포트 충돌)Exec format error(아키텍처 불일치, 잘못된 실행 파일)Failed to parse(유닛 파일 문법 오류)
로그에 인증/권한 관련 에러가 보이고, 특히 AWS STS 같은 외부 인증 호출이 연관되어 있다면 IAM 쪽도 함께 의심해야 합니다. 예를 들어 AssumeRole 실패 패턴은 애플리케이션이 즉시 종료하며 재시작을 유발할 수 있습니다. 이 경우 AWS IAM AssumeRole AccessDenied 원인 10가지도 참고하면 원인 좁히기에 도움이 됩니다.
4단계: 유닛 파일의 실행 조건을 점검하기 (User, WorkingDirectory, Environment)
systemd는 “내가 터미널에서 실행하면 잘 되는데 서비스로는 실패” 같은 케이스가 매우 흔합니다. 대부분 실행 사용자, 작업 디렉터리, 환경변수 차이에서 터집니다.
유닛 파일 확인
systemctl cat myservice
systemctl show myservice -p User -p Group -p WorkingDirectory -p Environment -p EnvironmentFile
자주 터지는 포인트
User=로 실행되는데 해당 사용자가 설정 파일을 읽을 권한이 없음WorkingDirectory=가 존재하지 않거나 권한이 없음EnvironmentFile=경로가 틀려서 환경변수가 비어 있음- 상대 경로를 전제로 만든 앱이
WorkingDirectory차이로 실패
개선 예시
아래는 흔한 웹 서비스 유닛 예시입니다.
[Unit]
Description=My API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapi
Group=myapi
WorkingDirectory=/opt/myapi
EnvironmentFile=/etc/myapi/myapi.env
ExecStart=/opt/myapi/bin/myapi --config /etc/myapi/config.yaml
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
환경변수는 실제로 서비스 컨텍스트에서 어떻게 보이는지 확인해야 합니다.
sudo systemctl show myservice -p Environment
5단계: 의존성/네트워크/준비상태 문제를 분리하기 (After, Wants, Type)
서비스가 “기동은 되지만 바로 죽거나”, “타임아웃으로 실패”하는 경우는 대개 아래 중 하나입니다.
- DB, Redis, 메시지 브로커 등 의존 서비스가 아직 준비되지 않음
- DNS 또는 라우팅 문제로 외부 API 호출이 실패
Type=설정이 앱의 실제 동작과 안 맞아systemd가 준비 완료를 오판
의존성 확인
systemctl list-dependencies myservice --no-pager
systemctl status network-online.target --no-pager
네트워크 빠른 점검
resolvectl status
getent hosts example.com
curl -v --max-time 3 https://example.com/
Type= 관련 팁
- 대부분의 단일 프로세스 서버는
Type=simple이 안전합니다. - 포크하는 데몬이면
Type=forking과 PID 파일 설정이 필요할 수 있습니다. - 준비 완료 시그널이 필요한 경우
Type=notify와sd_notify연동이 필요합니다.
의존성 “준비 완료”를 기다리는 로직이 없다면, Restart=on-failure는 오히려 장애를 증폭시킬 수 있습니다. 이 경우 앱 레벨의 재시도, 혹은 ExecStartPre=로 사전 체크를 넣는 편이 낫습니다.
[Service]
ExecStartPre=/usr/bin/bash -lc 'until getent hosts db.internal; do sleep 1; done'
ExecStartPre=/usr/bin/bash -lc 'until /usr/bin/nc -z db.internal 5432; do sleep 1; done'
6단계: 리소스/제한/보안 정책으로 인한 강제 종료 확인 (OOM, Limit, SELinux)
로그에 명확한 애플리케이션 에러가 없는데도 죽는다면, 커널 또는 정책이 죽였을 수 있습니다.
OOM 킬 확인
dmesg -T | grep -i -E 'killed process|out of memory|oom'
journalctl -k -b --no-pager | grep -i -E 'oom|killed process'
systemd 제한값 확인
systemctl show myservice -p MemoryMax -p CPUQuota -p TasksMax -p LimitNOFILE -p LimitNPROC
파일 디스크립터 부족 예시
고부하 네트워크 서버에서 Too many open files가 나면 LimitNOFILE을 올려야 합니다.
[Service]
LimitNOFILE=1048576
SELinux/AppArmor
- SELinux Enforcing 환경에서는 접근이 막혀도 앱 로그에는 단순
Permission denied만 남을 수 있습니다.
getenforce
sudo ausearch -m avc -ts recent | tail -n 50
7단계: 코어덤프/스택트레이스로 “진짜 크래시”를 확정하기
Segfault, panic, 네이티브 모듈 크래시처럼 프로세스가 갑자기 죽는다면 코어덤프가 가장 빠른 답입니다.
coredumpctl 확인
coredumpctl list myservice
coredumpctl info myservice
디버거로 스택 확인 (예: gdb)
coredumpctl debug myservice
Node.js나 런타임 기반 서비스 팁
- 네이티브 애드온, OpenSSL, glibc 호환 문제는 특정 환경에서만 재현되기도 합니다.
- “서비스로만” 죽는다면 4단계의 환경 차이와 함께, 런타임 옵션(
NODE_OPTIONS등)도 같이 확인하세요.
재시작 루프를 멈추고 안전하게 수정하는 운영 팁
진단 중에는 재시작 루프 자체가 장애를 확대할 수 있습니다.
- 임시로
Restart=no로 바꾸고 원인 파악 - 혹은
RestartSec를 늘려 로그/관찰 시간을 확보
[Service]
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=3
또한 배포/설정 변경 시에는 아래 순서를 습관화하면 휴먼 에러를 줄일 수 있습니다.
sudo systemctl daemon-reload
sudo systemctl restart myservice
sudo systemctl status myservice --no-pager
journalctl -u myservice -b --no-pager | tail -n 200
마무리: “원인 후보”를 1개로 줄이는 순서가 중요하다
systemd 재시작 문제는 결국 다음 질문으로 귀결됩니다.
- 누가 재시작시키는가 (정책 vs 외부)
- 어떤 종료 형태인가 (exit-code, signal, timeout)
- 첫 번째 에러는 무엇인가 (journal)
- 서비스 컨텍스트에서만 실패하는가 (User, Env, WorkingDirectory)
- 의존성이 준비되었는가 (network-online, DB, DNS)
- 커널/정책이 죽였는가 (OOM, Limit, SELinux)
- 진짜 크래시인가 (coredump)
위 7단계를 순서대로 밟으면, “추측 기반 수정”이 아니라 증거 기반 수정으로 전환할 수 있고, 재발 방지까지 자연스럽게 연결됩니다.