- Published on
systemd 타이머가 안 도는 이유 10가지 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 크론 대신 systemd timer로 배치 작업을 돌리다 보면, 분명히 enable도 했고 unit 파일도 만들었는데 타이머가 “안 도는” 상황을 자주 만납니다. 문제는 대개 타이머 자체가 멈춘 게 아니라, 트리거 조건이 충족되지 않았거나, 서비스가 즉시 실패했거나, 원하는 스케줄로 해석되지 않았거나, 환경 차이로 실행이 안 되는 것입니다.
아래는 운영에서 가장 많이 만나는 원인 10가지를 “어디를 보면 바로 결론이 나는지” 중심으로 정리한 진단 가이드입니다.
먼저, 3분 안에 전체 상황 파악하기
아래 명령으로 “타이머가 로드되었는지, 활성화되었는지, 다음 실행이 잡혀 있는지, 마지막 실행이 있었는지”를 한 번에 확인합니다.
# 1) 타이머 목록과 다음 실행 시각 확인
systemctl list-timers --all
# 2) 특정 타이머 상태 확인
systemctl status myjob.timer
# 3) 타이머가 트리거하는 서비스 상태/로그 확인
systemctl status myjob.service
journalctl -u myjob.service -b --no-pager
# 4) unit 파일 실제 로딩 내용 확인(드롭인 포함)
systemctl cat myjob.timer
systemctl cat myjob.service
# 5) unit 파일 문법/의존성 검증(중요)
systemd-analyze verify /etc/systemd/system/myjob.timer /etc/systemd/system/myjob.service
이제부터는 “다음 실행이 없거나(Next가 비어 있음)”, “계속 inactive”, “실행은 되는데 실패”, “주기가 이상함” 같은 케이스별로 원인을 좁혀갑니다.
1) 타이머를 enable이 아니라 start만 했다
enable은 부팅 시 자동 시작이고, **지금 당장 돌게 하는 건 start**입니다. 반대로 start만 하면 재부팅 후 사라져 “어느 날부터 안 돈다”가 됩니다.
# 부팅 자동 시작 + 즉시 시작
sudo systemctl enable --now myjob.timer
# 현재 활성화 여부
systemctl is-enabled myjob.timer
systemctl is-active myjob.timer
증상:
- 재부팅 이후
systemctl list-timers에서 사라지거나inactive로 남음
2) unit 파일 수정 후 daemon-reload를 안 했다
/etc/systemd/system 아래 unit을 수정해도 systemd는 자동으로 다시 읽지 않습니다.
sudo systemctl daemon-reload
sudo systemctl restart myjob.timer
systemctl status myjob.timer
증상:
- 파일은 바꿨는데 동작이 그대로
systemctl cat으로 보면 예전 내용이 로드됨
3) .timer와 .service 이름이 안 맞거나, Unit=이 잘못됐다
타이머는 기본적으로 동일한 베이스 이름의 서비스를 트리거합니다. 예를 들어 backup.timer는 기본적으로 backup.service를 호출합니다. 다른 서비스를 호출하려면 [Timer]에 Unit=를 정확히 지정해야 합니다.
# /etc/systemd/system/backup.timer
[Unit]
Description=Backup timer
[Timer]
OnCalendar=*-*-* 02:00:00
Unit=backup.service
[Install]
WantedBy=timers.target
확인 포인트:
systemctl status backup.timer
# Triggers: ● backup.service 처럼 보이는지 확인
증상:
- 타이머는 “활성”인데 서비스가 전혀 실행되지 않음
journalctl -u ...에 아무 로그도 없음
4) OnCalendar 표현식이 의도와 다르게 해석됐다
OnCalendar는 강력하지만, 사람이 기대한 “매일 자정”과 systemd가 해석한 “특정 조건”이 어긋나는 일이 많습니다. 특히 요일, 타임존, 초 단위, 월/일 와일드카드가 함정입니다.
다음으로 실제 해석 결과를 확인하세요.
systemd-analyze calendar "Mon..Fri 02:00"
systemd-analyze calendar "*-*-* 02:00:00"
또한 list-timers의 NEXT 컬럼이 비어 있거나 과거로 잡히면 표현식이 잘못됐을 가능성이 큽니다.
증상:
- “매시간”으로 했는데 하루에 한 번만 실행
NEXT가 기대와 다름
5) Persistent= 설정 때문에 재부팅 후 실행이 건너뛰어 보인다
노트북/개발 서버처럼 꺼져 있는 시간이 길면, 그 사이 스케줄을 어떻게 처리할지 결정해야 합니다.
Persistent=true: 꺼져 있던 동안 놓친 실행을 부팅 후 즉시 따라잡음Persistent=false: 놓친 실행은 버림
[Timer]
OnCalendar=daily
Persistent=true
확인:
systemctl show myjob.timer -p Persistent -p LastTriggerUSec -p NextElapseUSecRealtime
증상:
- 재부팅 후 “왜 바로 안 돌지?” 혹은 “왜 부팅하자마자 돌지?” 같은 혼란
6) 서비스가 즉시 실패하는데 타이머만 보고 “안 돈다”고 오해했다
타이머는 트리거만 합니다. 실제 실패는 .service 로그에 있습니다. 특히 ExecStart 경로, 권한, 작업 디렉터리, 환경 변수 문제로 즉시 실패하는 경우가 흔합니다.
# 서비스 실패 원인 확인
systemctl status myjob.service
journalctl -u myjob.service -b --no-pager -n 200
# 최근 실패만
journalctl -u myjob.service -p err..alert -b --no-pager
서비스 예시(권장 패턴):
# /etc/systemd/system/myjob.service
[Unit]
Description=My batch job
[Service]
Type=oneshot
User=myuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/run.sh
# 실패 시 원인 추적을 돕는 옵션(필요 시)
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[Install]
WantedBy=multi-user.target
관련해서 “권한/보안 정책 때문에 실행이 실패”하는 케이스는 logrotate에서 특히 자주 보이는데, 진단 흐름이 유사합니다. 권한과 보안 정책 확인 루틴은 이 글도 함께 참고하면 좋습니다: 리눅스 logrotate 실패? 권한·SELinux 7분 해결
7) Type=이 맞지 않아 systemd가 “끝나지 않은 작업”으로 판단한다
배치성 작업은 대부분 Type=oneshot이 적합합니다. 그런데 기본값(simple)로 두고 백그라운드 포크를 하거나, 반대로 데몬인데 oneshot으로 두면 상태 판단이 꼬입니다.
- 배치 스크립트:
Type=oneshot - 장기 실행 데몬:
Type=simple또는notify
특히 배치가 내부에서 nohup으로 백그라운드 실행 후 즉시 종료하면, systemd는 “서비스는 끝났는데 실제 작업은 딴 데서 도는” 형태가 되어 추적이 어려워집니다.
진단:
systemctl show myjob.service -p Type -p ExecMainStatus -p ExecMainCode
증상:
- 타이머는 도는 것 같은데 실제 작업은 중간에 끊기거나, 중복 실행됨
8) User/권한/파일 소유권 때문에 실행이 막힌다
root로는 되는데 타이머로는 안 되는 경우가 많습니다. 타이머는 결국 서비스의 User=로 실행되므로 다음을 점검하세요.
- 실행 파일에 실행 권한이 있는지(
chmod +x) - 스크립트의 shebang이 유효한지(예:
#!/bin/bash) WorkingDirectory접근 가능한지- 출력/로그 파일 경로에 쓰기 권한이 있는지
# 권한/소유자 확인
namei -l /opt/myapp/bin/run.sh
ls -al /opt/myapp/bin/run.sh
# 해당 유저로 실제 실행 테스트
sudo -u myuser /opt/myapp/bin/run.sh
증상:
Permission denied,No such file or directory가 서비스 로그에 남음
9) 환경 변수가 달라서(특히 PATH) 커맨드를 못 찾는다
인터랙티브 쉘에서는 .bashrc나 profile이 PATH를 만들어 주지만, systemd 서비스는 그렇지 않습니다. python, node, aws, kubectl 같은 커맨드가 “수동 실행은 되는데 타이머로는 안 됨”의 대표 원인입니다.
해결은 두 가지 중 하나입니다.
ExecStart에 절대 경로 사용Environment=PATH=...명시
[Service]
Type=oneshot
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/bin/python3 /opt/myapp/job.py
진단:
journalctl -u myjob.service -b --no-pager | grep -i "not found"
10) 시간/타임존/NTP 문제로 스케줄 자체가 흔들린다
타이머가 “안 도는 것처럼 보이는” 마지막 큰 축은 시간 문제입니다.
- 서버 타임존이 의도와 다름
- NTP 동기화가 꺼져 있거나, 시간이 점프함
- 컨테이너/VM 환경에서 호스트 시간 이슈
점검:
timedatectl status
# NTP 동기화 여부
timedatectl show -p NTPSynchronized -p NTP
# 현재 타임존
timedatectl show -p Timezone
OnCalendar는 로컬 타임존 기준이므로, UTC 기준으로 생각하고 작성했는데 서버가 KST로 되어 있으면 9시간 차이가 납니다.
또한 시간이 크게 점프하면(수동 변경, NTP 교정) NEXT가 재계산되며 예상치 못한 스킵처럼 보일 수 있습니다.
실전 체크리스트: “안 돈다”를 5분 내로 쪼개기
아래 순서대로 보면 대부분 5분 안에 원인 범위를 확정할 수 있습니다.
systemctl list-timers --all에서 해당 타이머의NEXT가 존재하는가systemctl status myjob.timer에서Loaded와Active가 정상인가Triggers가 의도한 서비스로 연결되는가journalctl -u myjob.service -b에 실패 로그가 있는가systemd-analyze calendar "..."로 스케줄 해석이 의도와 같은가systemctl cat으로 최종 unit 내용이 맞는가(드롭인 포함)User/WorkingDirectory/권한/PATH 문제를 재현 가능한 방식으로 확인했는가
자주 쓰는 템플릿: 매 10분 실행 + 안정적인 로그 확인
# /etc/systemd/system/myjob.timer
[Unit]
Description=MyJob timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
Persistent=true
Unit=myjob.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/myjob.service
[Unit]
Description=MyJob runner
[Service]
Type=oneshot
User=myuser
WorkingDirectory=/opt/myapp
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/opt/myapp/bin/run.sh
# 필요 시 실행 시간 제한
TimeoutStartSec=5min
적용:
sudo systemctl daemon-reload
sudo systemctl enable --now myjob.timer
systemctl list-timers --all | grep myjob
마무리
systemd 타이머는 “타이머가 멈췄다”보다 스케줄 해석, 서비스 실패, 환경/권한 차이, 리로드 누락이 원인인 경우가 훨씬 많습니다. 특히 list-timers의 NEXT/LAST, status의 Triggers, 그리고 journalctl -u 서비스 3가지만 습관처럼 보면 대부분의 케이스는 빠르게 결론이 납니다.
타이머가 정상인데도 서비스가 계속 실패한다면, 그 시점부터는 타이머가 아니라 “서비스 실행 실패” 디버깅으로 관점을 전환하는 것이 해결 속도를 크게 올립니다.