LabHub
배우기 러닝패스 코스

실패했는데 종료 코드는 0 이었다 · 재시도가 두 번째 장애를 만들었다 · 실습

재시도가 두 번째 장애를 만들지 않게

LabHub 에서 이어서 보기

목표

외부 명령을 부르는 파이썬 도구가 "끝나지 않는 명령" 과 "두 번 실행되는 명령" 을 막게 만든다 — 타임아웃, 재시도, 멱등성, 잠금, 신호 처리, 시도 로그.

왜 중요한가

재시도는 명령이 언젠가 끝나고 다시 실행해도 안전하다는 두 전제 위에서만 안전하다. 그 전제 없이 붙인 재시도는 장애를 세 배로 키운다. subprocess.run 의 timeout 은 자식을 죽이고 기다려 주고, 표식 파일은 성공 뒤에만 남기며, flock 은 동시 실행을 막고, SIGTERM 처리기는 고아 프로세스를 남기지 않는다. 이 실습은 그 네 가지를 작은 실행기 하나에 전부 넣는다.

재료 명령은 /opt/fixtures/pyops/bin/ 에 있다 — flaky.sh(처음 두 번 실패, 세 번째 성공; 시도 횟수는 환경변수 FLAKY_STATE 파일에), hang.sh(60초 동안 끝나지 않음), apply.sh <이름>(부를 때마다 APPLY_LOG, 기본 /root/pyops/sub/applied.log 에 한 줄 추가; APPLY_FAIL=1 이면 실패).

단계

1. /root/pyops/sub/runner.py 를 만든다. runner.py run -- <명령...> 은 subprocess.run 으로 명령을 실행해 명령의 표준 출력을 그대로 표준 출력에 내고, 명령의 종료 코드로 끝난다.
2. --timeout <초> 를 붙인다. 시간이 지나면 명령은 죽고 표준 오류에 timeout 이 포함된 한 줄이 찍히며 종료 코드 124 로 끝난다. 명령 프로세스가 남아 있으면 안 된다.
3. --retries <n> --backoff <초> 를 붙인다. 종료 코드가 0 이 아니면 backoff × 2^(시도-1) 초를 기다렸다가 최대 n 번 다시 시도한다. 타임아웃(124)도 실패로 보고 재시도한다.
4. runner.py apply <이름> 을 만든다. /root/pyops/sub/state/<이름>.done 표식이 있으면 already applied 를 찍고 0 으로 끝나며 apply.sh 를 부르지 않는다. 없으면 /opt/fixtures/pyops/bin/apply.sh <이름> 을 부르고 성공(0)했을 때만 표식을 만든다. 실패하면 표식 없이 1 로 끝난다.
5. apply/root/pyops/sub/state/lock 에 fcntl.flock(LOCK_EX | LOCK_NB) 잠금을 잡는다. 잠금을 못 잡으면 표준 오류에 another run in progress 를 찍고 종료 코드 3 으로 끝난다.
6. SIGTERM 을 받으면 실행 중인 명령에 SIGTERM 을 전달하고 끝나기를 기다린 뒤 종료 코드 143 으로 끝난다. 명령 프로세스가 남아 있으면 안 된다.
7. 시도마다 /root/pyops/sub/runs.jsonl 에 JSON 한 줄을 덧붙인다. 키는 ts(ISO 8601 문자열)·cmd(문자열 목록)·attempt(1부터)·rc(정수)·duration_ms(정수)다.
8. apply --dry-run <이름> 은 apply.sh 를 부르지 않고 표식도 만들지 않으면서 무엇을 할지 표준 출력에 이름을 포함해 적고 0 으로 끝난다. 이미 적용된 이름이면 already applied 를 적는다.

참고

단계 8개

  1. 명령을 실행하고 종료 코드를 그대로
  2. 끝나지 않는 명령을 끝낸다
  3. 지수 백오프 재시도
  4. 표식으로 멱등한 apply
  5. 동시 실행을 잠금으로 막는다
  6. SIGTERM 을 자식에게 전달한다
  7. 시도마다 JSON 한 줄
  8. 하지 않고 무엇을 할지만 말한다