Devin.KR

예약 실행과 중복 방지

100분 안팎

학습 목표

systemd timer로 점검을 예약하고 잠금으로 중복 실행을 막으며 서비스 실행 환경에서 검사합니다.

개념

정기 실행에서는 터미널의 도움을 기대하지 않습니다

수동 실행은 성공하는데 예약 실행은 파일을 찾지 못하는 일이 자주 생깁니다. 로그인 셸의 현재 위치와 PATH, 사용자 권한을 암묵적으로 이용했기 때문입니다. 이번 레슨은 동일 점검을 systemd oneshot service로 선언하고 timer로 예약합니다. 예약 작업과 손으로 실행한 작업이 겹칠 수 있으므로 같은 파일 잠금을 사용합니다. 점검 한 번의 결과뿐 아니라 누가 어떤 경로와 인자로 실행했는지를 재현하는 것이 목표입니다.

점검 서비스는 끝나는 작업입니다

bootcamp-health.service는 Type=oneshot으로 한 번 관측하고 판정·알림·기록 뒤 종료합니다. User=bcops, Group=bcapp, WorkingDirectory와 ExecStart의 절대 경로를 선언하고 Restart=no를 사용합니다. 결과 파일은 UMask=0077로 새 파일 접근을 제한합니다. 앱을 실행하는 bcweb과 점검을 실행하는 bcops를 혼동하지 않습니다. 서비스 설정의 이름이 같아도 작업 내용과 필요한 권한은 서로 다릅니다.

예약 시점의 기준을 읽습니다

timer의 OnActiveSec=5s는 timer를 시작한 시각에서 첫 실행까지의 시간입니다. OnUnitInactiveSec=60s는 연결한 점검 서비스가 비활성 상태로 돌아온 뒤 다음 실행까지의 시간입니다. 두 설정을 함께 사용해 첫 실행과 반복을 정의합니다. 점검이 3초 걸리면 이 설정은 매분 정각을 뜻하지 않습니다. AccuracySec=1s도 실제 실행이 정확히 그 순간에 보장된다는 뜻은 아니며 부하와 스케줄링 지연을 관측합니다.

같은 service가 이미 active라면

systemd timer는 연결된 유닛이 이미 active일 때 새 인스턴스를 다시 시작하지 않습니다. 그런데 사용자가 run-health.sh를 직접 실행하면 그 실행은 같은 유닛 상태 밖에 있습니다. 그러므로 timer의 동작만으로 모든 중복을 막았다고 주장하지 않습니다. 또한 oneshot에 RemainAfterExit=yes를 넣으면 끝난 뒤에도 active로 남아 반복 실행이 막힐 수 있습니다. 제공 유닛은 그 항목을 쓰지 않고 작업 종료 뒤 다음 예약을 기다립니다.

파일의 존재가 아니라 잠금 소유를 봅니다

health.py는 evidence/health.lock 파일을 열고 fcntl.flock의 배타·비대기 잠금을 요청합니다. 파일이 남아 있어도 잠금이 풀렸으면 다음 점검이 실행됩니다. 잠금 파일이 있으니 지워야 한다는 판단은 틀립니다. 같은 파일을 모두 사용하고 다른 사용자가 경로를 바꿀 수 없는 전용 디렉터리를 유지해야 합니다. 실행 중 파일을 지우면 다른 실행이 새 파일을 잠그며 동시에 점검할 수 있습니다.

중복 생략은 정상 점검과 다릅니다

잠금을 얻지 못한 실행은 관측과 알림을 수행하지 않고 SKIP_BUSY를 stderr에 쓰며 종료 75로 끝납니다. checks.jsonl에 정상 행을 새로 추가하지 않습니다. 실제로 점검하지 않았으므로 OK로 기록할 근거가 없습니다. 호출자는 생략이 가끔 발생하는지 계속되는지 확인해야 합니다. 계속되는 경합은 긴 요청이나 오래 걸리는 점검과 관련될 수 있어 소유 실행의 상태와 명령 시간 상한을 조사합니다.

실패해도 잠금 수명을 끝냅니다

잠금을 얻은 뒤에는 try/finally로 해제 경계를 정하고 열린 파일도 with로 닫습니다. 관측 함수가 예상 OSError를 발생시키면 UNKNOWN으로 기록한 뒤 종료합니다. 테스트는 실패 뒤 같은 경로에서 정상 점검을 다시 실행해 잠금이 풀렸는지 확인합니다. 락은 파일 기술자에 결합되므로 프로세스 종료 시에도 운영체제가 해제하지만, 실습은 코드 경로에서의 해제를 함께 검증합니다. 이 구현은 한 호스트의 파일 잠금이며 분산 잠금이 아닙니다.

실행 환경을 최소한으로 선언합니다

서비스는 /usr/bin/python3 -B로 코드를 실행하고 PATH=/usr/bin:/bin을 지정합니다. 관측기는 LC_ALL=C와 절대 도구 경로를 따로 사용합니다. 코드 위치와 증거 기본 경로는 스크립트 기준으로 계산하여 셸의 현재 폴더에 영향을 받지 않게 합니다. 셸의 별칭과 개인 profile은 서비스 계약으로 사용하지 않습니다. 미션 로컬 검사는 작은 환경과 다른 작업 디렉터리에서 run-health.sh를 호출해 이 경계를 확인합니다.

고정 입력 예약과 실제 점검 예약을 나눕니다

이 레슨의 유닛은 normal.json을 --snapshot으로 읽어 스케줄 경로와 출력 계약을 검증합니다. 같은 입력을 수동으로 실행한 OK와 timer의 OK를 비교하므로 서비스 상태가 도중에 바뀌는 영향을 줄입니다. 이것은 실제 VM 앱이 건강하다는 증거가 아닙니다. 미션의 유닛은 snapshot 없이 현재 systemctl·curl·df를 관측하고 선택 백업과 이전 복구 기록을 함께 검사하도록 확장합니다.

관리자 설치 전에 일반 사용자 검사를 합니다

먼저 bash check-local.sh로 세 검사를 통과시키고 유닛의 계정·절대 경로를 준비한 VM과 비교합니다. /srv/bootcamp-health에 전용 사본을 두고 bcops가 evidence를 쓸 수 있게 설정합니다. systemd-analyze verify로 제공 service와 timer의 문법을 확인한 뒤 같은 이름의 기존 유닛이 없는 경우에만 설치합니다. 설치 권한과 점검 실행 권한을 분리하고 기존 앱의 유닛을 덮어쓰지 않습니다.

트리거와 실행 결과는 별도 확인합니다

list-timers는 다음과 마지막 예약 정보를 보여주지만 실행한 점검이 정상인지까지 증명하지 않습니다. service의 Result와 ExecMainStatus, journal 및 checks.jsonl을 읽어 종료 상태를 확인합니다. check-vm.py는 timer 활성 상태·LastTriggerUSec·설치된 ExecStart의 origin과 수동/예약 결과를 조회합니다. 아직 첫 실행이 없으면 TIMER_NOT_TRIGGERED입니다. 기다린 시간을 가짜 출력으로 적지 않고 실제 VM의 값으로 증거를 보완합니다.

비교 실패는 실행 조건의 차이를 찾습니다

수동과 예약의 status 또는 reasons가 다르면 입력 파일·실행 인자·계정·코드 사본·증거 경로부터 비교합니다. 미션에서 앱이 20초 뒤 자연 종료했다면 시간이 달라 결과가 달라질 수 있습니다. 상태 변화를 실패한 테스트라고 감추지 않고 안정된 동일 조건에서 다시 비교합니다. origin은 실행 인자가 지정한 기록이며 혼자서는 scheduler 증거가 아니므로 timer 트리거와 실제 유닛 실행 정보도 함께 확인합니다.

예약 해제와 작업 종료는 같은 동작이 아닙니다

검증이 끝나면 timer를 정지해 새로운 예약을 해제합니다. 이미 실행 중인 점검은 제한된 요청을 마치고 자연 완료하도록 기다립니다. service가 inactive 또는 종료한 failed이고 ExecMainPID=0인지 확인한 뒤 이번 실습에서 설치한 유닛만 정리합니다. 다른 프로세스를 종료하거나 앱 설정을 고치지 않습니다. 작업이 끝나지 않으면 증거와 경로를 보존하고 원인을 조사하는 것이 정리보다 먼저입니다.

누적 프로젝트의 완료 기준

미션은 이전 복구 solution을 previous에 보존하며 입력 백업의 해시·내용과 recovery.json의 verdict·데이터 일치·원본 보존을 검사합니다. 과거 PASS는 현재 데이터의 복구 가능성을 보장하지 않으므로 기록 시각과 한계를 인계 문서에 씁니다. timer 예약 결과, 수동 비교, 알림 전달과 실패, 중복 생략, 예약 해제 기록을 함께 제출합니다. systemctl 일반 명령은 더 읽기를 참고하고 여기서는 반복 실행 환경과 실패 전달의 연결을 완성합니다.

예약 기준과 active 유닛 처리 규칙은 systemd 공식 timer 문서 원문에 설명되어 있습니다.

따라하기

같은 파일의 잠금 경합 확인

solution ZIP에서 실제 파일 잠금을 잡고 두 번째 실행의 생략을 확인합니다. 네트워크나 systemd를 실행하지 않습니다.

python3 -B demo.py timer

실행 결과

BUSY_EXIT 75 ROWS 0
NEXT_EXIT 0 ROWS 1

실패 후 잠금과 유닛 계약 회귀

starter 중복 분기를 구현하고 로컬 세 테스트를 통과시킵니다.

bash check-local.sh

VM 유닛 문법과 수동 판정

README에 따라 새 전용 경로와 bcops 권한을 준비한 VM에서 실행합니다. 고정 normal.json의 OK 기록은 예약 경로 검증용입니다.

systemd-analyze verify bootcamp-health.service bootcamp-health.timer
bash /srv/bootcamp-health/run-health.sh --snapshot /srv/bootcamp-health/normal.json

설치된 timer 트리거와 결과 조회

README의 관리자 설치 후 임시 timer를 시작합니다. 첫 실행 뒤 상태·트리거·수동/예약 기록을 확인하고 check.sh를 실행합니다. 확인 뒤 timer 예약만 해제하고 점검 자연 완료 및 자기 유닛 정리를 기록합니다.

sudo systemctl start bootcamp-health.timer
systemctl list-timers --all bootcamp-health.timer
systemctl show bootcamp-health.service -p ExecMainStatus -p Result
bash check.sh

확인 문제

실습

health.py의 중복 생략 반환 코드를 구현합니다. bash check-local.sh의 실제 파일 잠금·실패 뒤 해제·유닛 계약 세 검사를 통과합니다. README의 VM 설치 절차와 check.sh로 문법·timer 활성/트리거·수동/예약 판정 일치를 확인합니다. 실제 예약 검증은 external입니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

로컬 3 tests, OK와 VM_TIMER_EVIDENCE_OK

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 점검 스크립트가 실패를 알리는 방식을 설명합니다.