디버깅 실전 · 순서가 보장되지 않아 생기는 것 · 실습
여덟 건이 들어왔는데 하나만 남았다
목표
출발선을 맞춰 경쟁 조건을 재현 가능한 사실로 만든 뒤, 잃어버린 갱신을 배타 잠금으로 막고, 확인 후 사용의 틈을 원자적 생성으로 닫고, 통째로 쓰는 문서를 원자적 이름 바꾸기로 갈아 끼운다. 마지막으로 잠금을 잡기 전에 파일을 비우는 실수가 무엇을 날리는지 직접 확인한다.
왜 중요한가
경쟁 조건은 "가끔 숫자가 안 맞는다" 로 신고된다. 혼자 돌리면 언제나 맞기 때문에 조사가 재현에서 막힌다. 그런데 프로세스를 먼저 다 띄워 두고 출발 표식 하나로 동시에 보내면, 경쟁은 확률이 아니라 언제나 일어나는 일이 된다. 재현이 되는 순간부터 그것은 고칠 수 있는 결함이다.
막는 방법은 증상의 모양에 따라 다르다. 읽고 고쳐 쓰는 구간은 잠금으로 묶고, 확인 후 사용은 한 번의 호출로 바꾸고, 통째로 쓰는 파일은 이름 바꾸기로 갈아 끼운다. 이 셋을 섞어 쓰면 잠금을 걸어도 증상이 남는다.
가장 자주 걸리는 함정은 잠금의 범위다. open(path, "w") 는 여는 순간 파일을 비우므로, 그다음 줄에서 잠금을 잡아도 이미 늦었다. 이 실수는 잠금을 건 코드에서 나기 때문에 더 오래 살아남는다.
채점기는 여러분의 설명을 믿지 않는다. 채점기가 자기 하네스로 여러분의 워커를 실제로 동시에 띄워 최종 값과 승자 수를 직접 센다. 프로세스 수는 실행마다 바뀌므로 값을 외워 넣을 수 없습니다.
단계
1. /root/race/gen_race.py 를 만들어 실행해 /root/race/spawn.py(출발선 하네스)를 만드세요.
2. /root/race/naive_add.py 로 잠금 없는 갱신을 동시에 보내 /root/race/lost.json 에 잃어버린 갱신을 적으세요.
3. /root/race/locked_add.py 로 같은 일을 배타 잠금 안에서 하고 /root/race/lock.json 에 적으세요.
4. /root/race/claim.py 로 확인 후 사용의 틈을 닫고 /root/race/claim.json 에 승자와 패자를 적으세요.
5. /root/race/safe_write.py 와 /root/race/read_probe.py 로 부분 기록을 막고 /root/race/atomic.json 에 적으세요.
6. /root/race/append_line.py 로 잠금 전에 파일을 비우지 않고 덧붙여 /root/race/append.json 에 적으세요.
7. 두 판을 여러 번 돌려 /root/race/evidence.json 에 재현 가능한 증거를 남기세요.
8. /root/race/summary.json 과 /root/race/race_report.md 에 네 절로 보고하세요.
참고
- 하네스 계약:
python3 /root/race/spawn.py --worker <스크립트> --procs <수> --barrier <파일> -- <워커 인자...>는 워커를 그 수만큼 띄우고, 전부 준비된 뒤 표식 파일을 만들어 동시에 출발시킨 다음 procs·exit_codes 를 담은 JSON 을 냅니다. - 워커 계약: 모든 워커는
--barrier <파일>을 받습니다. 기다리기 전에 준비 표식<barrier>.<pid>.up을 남기고, 그다음 표식 파일이 생길 때까지 기다렸다가 일을 시작합니다. 하네스는 그 준비 표식을 세어 전부 모인 뒤에 출발시킵니다 — 몇 초 기다리는 방식은 부하가 높은 날 뒤늦게 뜬 프로세스를 놓쳐 재현이 깨집니다. - 갱신 워커:
--file <카운터 파일> --barrier <파일> [--work-ms <밀리초>]. 카운터 파일에는 정수 하나가 들어 있고, 워커는 그 값을 하나 올립니다. 기본 작업 시간은 300밀리초입니다. - 선점 워커:
--dir <디렉터리> --barrier <파일> --owner <이름>. 이기면 종료 코드 0 으로 끝나고 그 디렉터리의lease파일에 owner 를 적습니다. 지면 9 로 끝납니다. - 쓰기·읽기:
python3 /root/race/safe_write.py --file <파일> --rounds <횟수> --entries <개수>와python3 /root/race/read_probe.py --file <파일> --reads <횟수> [--out <json>]은 file·reads·bad_reads 를 담은 JSON 을 냅니다. - 덧붙이기 워커:
--file <파일> --barrier <파일> --line <문자열>. 이미 있던 줄은 그대로 두고 자기 줄을 하나 덧붙입니다. - 잠금은 권고입니다. 잠금을 잡지 않고 여는 프로그램은 막지 못하므로, 같은 파일을 만지는 모든 코드가 같은 약속을 지켜야 합니다.
- 원자적 이름 바꾸기는 임시 파일이 같은 파일 시스템에 있을 때만 성립합니다. 같은 디렉터리에 만드는 것이 가장 쉽습니다.
- 흔한 실수: 잠금 전에
"w"로 열기, 읽기를 잠금 밖에 두기, 프로세스마다 다른 파일을 잠그기,/tmp에 임시 파일을 만들고 다른 위치로 옮기기. - 부하 시험을 만들지 마세요. 파드는 2코어이고 채점 하나의 예산은 60초입니다. 프로세스는 여덟 개면 충분합니다.
단계 8개
- 출발선을 맞추는 하네스 손에 쥐기
- 여덟 건이 하나가 되는 것 보기
- 배타 잠금으로 한 덩어리 만들기
- 확인 후 사용의 틈 닫기
- 반쯤 쓰인 파일을 읽지 않게 하기
- 잠금 전에 파일을 비우는 실수
- 재현 가능한 증거 남기기
- 무엇을 잃었고 무엇으로 막았는지 보고하기