LabHub
배우기 러닝패스 코스

Data Pipelines

The 3 A.M. Settlement Job That Died: A Ledger and an Atomic Swap

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

도중에 죽어도 안전한 실행기 runner.py 를 만든다. 산출물을 임시 이름으로 쓰고 바꿔 달아 반쯤 쓰인 파일이 남지 않게 하고, 실행 한 번을 원장에 한 줄로 남기고, 죽은 조각부터 재개하고, 같은 실행을 두 번 커밋해도 숫자가 늘지 않게 만든다.

왜 중요한가

파이프라인은 반드시 도중에 죽는다. 문제는 죽는다는 사실이 아니라, 죽었을 때 무엇이 남느냐다. 목적지 파일에 직접 쓰고 있었다면 반쯤 쓰인 파일이 남고, 그 파일은 크기도 이름도 멀쩡해서 다음 단계가 그대로 읽어 간다. 다시 돌리는 것도 위험하다. 앞 실행이 어디까지 갔는지 기록이 없으면 처음부터 다시 도는 수밖에 없고, 마지막에 장부로 덧붙이는 자리에서 같은 금액이 두 번 더해진다. 이것이 "다시 돌렸는데 왜 두 배가 됐나" 의 정체다. 이 실습이 만드는 장치는 셋이다. 첫째는 원자적 교체다. 같은 디렉터리에 임시 이름으로 다 쓴 뒤 os.replace 로 바꿔 단다. 둘째는 실행 원장이다. 실행마다 한 줄을 덧붙이고, 실패했다면 어느 조각에서 죽었는지를 적는다. 셋째는 재개다. 끝난 조각의 산출물 자체를 표식으로 삼아 건너뛴다. 이 코스의 dp-idempotent 실습은 데이터베이스 쪽 멱등성을 다룹니다 — 같은 행을 두 번 넣어도 한 행이 되게 만드는 일입니다. 여기는 그 앞자리입니다. 프로세스가 죽은 자리에서 파일 시스템에 무엇이 남는가, 남은 것을 보고 어디부터 다시 시작할 것인가를 다룹니다. 채점기는 여러분이 적어 낸 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 입력 조각을 차려 놓고 여러분의 실행기를 실제로 돌린다. 일부러 죽인 뒤 목적지 파일이 그대로인지, 임시 파일이 목적지 옆에 남았는지, 원장에 실패한 조각 이름이 적혔는지까지 본다. 조각 수와 금액은 실행마다 바뀐다.

단계

  1. /root/runx/gen_shards.py 를 만들어 실행해 /root/runx/work/in 아래 입력 조각을 만드세요.
  2. /root/runx/runner.pyscan 을 만들어 조각 목록과 건수와 합계를 내게 하세요.
  3. part 를 더해 조각 하나를 처리하고 산출물을 임시 이름으로 썼다가 바꿔 달게 하세요. --crash=write 로 바꿔 달기 직전에 죽는 길도 만듭니다.
  4. run 을 더해 조각 전체를 처리하고 합친 산출물을 만들게 하세요.
  5. 실행마다 원장 한 줄을 남기고 ledger 로 요약을 내게 하세요.
  6. --crash-shard 로 가운데 조각에서 죽게 하고, 원장에 실패한 조각이 남고 합친 산출물은 건드려지지 않는지 확인하세요.
  7. --resume 을 더해 끝난 조각을 건너뛰고 죽은 자리부터 이어 가게 하세요.
  8. commit 을 더해 하루치 장부 /root/runx/work/out/daily.jsonl 에 같은 실행이 두 번 붙지 않게 하세요.

참고

상류가 떨어뜨린 조각 만들기

/root/runx/gen_shards.py 를 만들어 실행해 /root/runx/work/in 아래 조각 파일을 만드세요. 조각은 4개 이상, 한 조각에 5줄 이상, 전체 40줄 이상이어야 하고, 한 줄은 id 와 정수 amount 를 담은 JSON 한 덩이입니다.

조각 하나가 JSON Lines 파일 하나입니다. 파일 이름에서 .jsonl 을 뺀 것이 조각 이름이 되므로, 정렬했을 때 시간 순서가 되도록 h00·h01 처럼 자리를 맞춰 붙이세요. 난수 씨앗을 고정해 두어야 나중에 재개를 시험할 때 입력이 흔들리지 않습니다.

무엇이 들어왔는지 먼저 센다

/root/runx/runner.pyscan <작업폴더> 를 만들어 조각 이름 목록과 전체 건수와 금액 합계를 JSON 으로 내게 하세요. 조각 이름은 오름차순입니다.

작업 폴더 아래 in/ 에서 .jsonl 로 끝나는 파일만 골라 이름에서 확장자를 뗍니다. 빈 줄은 세지 않습니다. 작업 폴더가 없으면 종료 코드 3 으로 끝내야 뒤 단계의 오류 메시지가 정직해집니다.

목적지에 직접 쓰지 않는다

part <작업폴더> <조각> [--crash=write] 를 더하세요. 조각을 세어 out/part-<조각>.json 에 shard·events·amount 를 남기되, 목적지와 같은 디렉터리에 임시 이름으로 다 쓴 뒤 os.replace 로 바꿔 답니다. --crash=write 를 주면 바꿔 달기 직전에 종료 코드 9 로 죽습니다.

임시 이름이 part-*.json 목록에 걸리면 뒤에서 그 파일까지 합계에 들어갑니다. 점으로 시작하는 이름을 쓰세요. 그리고 임시 파일을 /tmp 에 만들면 안 됩니다 — os.replace 는 파일 시스템을 넘으면 실패하고, 그 실패는 개발 기계에서 재현되지 않습니다. 채점기는 죽인 뒤 목적지 파일이 그대로인지, 임시 파일이 목적지 옆에 남았는지를 봅니다.

실행 하나로 묶기

run <작업폴더> --run-id=<이름> 을 더해 조각 전체를 순서대로 처리하고, 조각 산출물을 다시 훑어 out/total.json 에 shards·events·amount 를 남기게 하세요. total.json 도 바꿔 달기로 씁니다.

총계를 이번에 처리한 조각만 더해서 내면 뒤에서 재개할 때 건너뛴 조각이 빠집니다. 총계는 언제나 out/part-*.json 전체를 다시 읽어서 내세요. 이 규칙 하나가 재개를 공짜로 만듭니다.

실행 한 번을 한 줄로 남기기

run 이 끝날 때 /root/runx/work/ledger.jsonl 에 한 줄을 덧붙이게 하고, ledger <작업폴더>{"runs": 정수, "ok": 정수, "failed": 정수, "last": 마지막 줄} 을 내게 하세요.

원장은 덧붙이기만 합니다. 앞줄을 고치기 시작하면 실행 하나가 한 줄이라는 규칙이 무너지고, 그 순간 원장은 로그와 다를 바가 없어집니다. 한 줄에 run_id·started_at·ended_at·status·shards_total·shards_done·events·amount 를 담으세요.

가운데 조각에서 죽여 보기

run--crash-shard=<조각> 을 더하세요. 그 조각 차례가 오면 원장에 statusfailed 이고 failed_shard 가 그 조각인 줄을 남기고 종료 코드 9 로 죽습니다. 합친 산출물 out/total.json 은 건드리지 않습니다.

죽기 전에 원장을 먼저 써야 합니다. 원장이 없으면 다음 사람이 할 수 있는 일은 처음부터 다시 돌리는 것뿐입니다. shards_done 에는 이번 실행에서 실제로 끝낸 조각만 담고, total.json 은 손대지 않은 채로 둡니다 — 지난 실행의 답이 그대로 서 있어야 합니다.

죽은 자리부터 이어 가기

run--resume 을 더하세요. 조각 산출물 out/part-<조각>.json 이 이미 있고 그 안의 shard 이름이 맞으면 그 조각을 건너뛰고, 건너뛴 것은 응답의 skipped 에, 이번에 처리한 것은 done 에 담습니다.

표식 파일을 따로 두지 말고 조각 산출물 자체를 표식으로 삼으세요. 산출물은 바꿔 달기로 만들어지므로, 있다는 것은 그 조각이 확실히 끝났다는 뜻입니다. 표식과 산출물을 따로 두면 표식만 남고 산출물은 반쯤 쓰인 상태가 생깁니다. 총계는 여전히 조각 산출물 전체에서 다시 모읍니다.

두 번 붙지 않게 하기

commit <작업폴더> --run-id=<이름> 을 더하세요. out/total.json 을 읽어 하루치 장부 out/daily.jsonl 에 run_id·events·amount 한 줄을 붙이되, 그 run_id 가 이미 있으면 붙이지 않고 {"appended": false, ...} 를 냅니다. 자기 작업 폴더에서도 실행 하나를 커밋해 두세요.

조각 처리는 덮어쓰기라 몇 번을 해도 같지만, 장부에 한 줄 붙이는 일은 부를 때마다 늘어납니다. 중복을 시각으로 판단하면 같은 날 두 번 돈 실행을 가르지 못합니다. 실행의 이름으로 판단하세요.