LabHub
배우기 러닝패스 코스

CI/CD 파이프라인 · 실패 분류와 원인 좁히기 · 실습

빨간불이 열 개인데 아무도 로그를 읽지 않았다

LabHub 에서 이어서 보기

목표

파이프라인의 실패를 표로 갈래를 나누고, 갈래마다 다른 대응으로 보내고, 불안정을 측정으로 증명하고, 실패한 실행을 기록만으로 다시 일으키고, 결정적인 판정 스크립트로 범인 커밋을 자동으로 찾아내는 조사 흐름을 처음부터 끝까지 만듭니다.

왜 중요한가

실패가 하루에 열 번이면 사람은 로그를 읽지 않습니다. 재시도 단추가 먼저 눌리고, 통과하면 잊힙니다. 그 습관이 붙은 파이프라인에서는 진짜 결함도 두세 번 재시도되다 어쩌다 통과하면 그대로 병합되고, 빨간불은 신호이기를 그만둡니다. 그래서 필요한 것은 더 좋은 로그가 아니라 갈래입니다. 인프라 문제는 재시도해도 되고, 불안정은 재시도로 덮으면 영원히 남고, 진짜 결함은 몇 번을 돌려도 같은 답이라 재시도가 시간만 버립니다. 갈래를 정한 다음에야 '언제 깨졌는가' 라는 질문이 성립하고, 그 질문에 답하는 이분 탐색은 판정이 결정적일 때만 믿을 수 있습니다. 한 번의 잘못된 판정이 남은 탐색 전체를 엉뚱한 구간으로 몰기 때문입니다. 이 실습은 그 사슬을 통째로 만듭니다 — 표로 가르고, 측정으로 증명하고, 기록으로 되살리고, 종료 코드로만 말하는 판정 스크립트로 범인을 찾습니다.

단계

1. /root/triage/rules.tsv 에 분류 규칙을 로 적으세요. 탭으로 나뉜 세 칸 규칙id<TAB>갈래<TAB>확장정규식 이고, 위에서부터 첫 일치가 이깁니다. 규칙 여섯 개를 이 id 와 갈래로 둡니다 — dns(infra), disk(infra), oom(infra), timeout(flaky), assert(defect), syntax(defect). 그다음 /root/triage/logs/ 에 실패 로그 표본 여섯 편 run-01.log 부터 run-06.log 까지를 직접 쓰세요. 차례대로 이름 풀이 실패(dns)·디스크 부족(disk)·메모리 초과로 죽음(oom)·시간 초과(timeout)·단언 실패(assert)·문법 오류(syntax)가 드러나는 내용이어야 합니다. 마지막으로 /root/triage/classify.sh <로그파일> 을 만드세요. 규칙 표의 자리는 환경 변수 RULES_FILE 로 바꿀 수 있고 기본값은 /root/triage/rules.tsv 입니다. 첫 낱말로 갈래를, 둘째 낱말로 규칙 id 를 한 줄에 냅니다. 어느 규칙에도 걸리지 않으면 unknown - 을 냅니다. 로그 파일을 읽을 수 없으면 표준출력에는 아무것도 내지 말고 종료 코드 2 로 끝냅니다.
2. /root/triage/policy.tsv 에 갈래별 대응을 표로 적으세요. 탭으로 나뉜 세 칸 갈래<TAB>권장대응<TAB>재시도가능 이고 네 줄입니다 — infra requeue yes, flaky measure no, defect bisect no, unknown read no(칸 사이는 탭). 그다음 /root/triage/classify.sh 를 고쳐 한 줄에 네 낱말 <갈래> <규칙id> <권장대응> <재시도가능> 을 내게 하세요. 정책 표의 자리는 환경 변수 POLICY_FILE 로 바꿀 수 있고 기본값은 /root/triage/policy.tsv 입니다. 그리고 갈래를 종료 코드로도 말하게 하세요 — infra 는 0, flaky 는 3, defect 는 4, unknown 은 5, 로그를 읽을 수 없으면 2 입니다. 마지막으로 /root/triage/logs/ 의 여섯 편을 차례로 돌려 그 출력을 그대로 /root/triage/triage.txt 에 여섯 줄로 저장하세요(run-01 부터 run-06 순서).
3. /root/triage/tests/ 에 시험 스크립트 세 개를 만드세요 — always-pass.sh(언제나 0), always-fail.sh(언제나 0 이 아님), flip.sh(홀수 번째 실행에서만 실패). flip.sh 가 세는 값은 자기 파일 옆($(dirname "$0") 아래)에 두어 스크립트를 복사해 옮겨도 함께 따라가게 하세요. 그다음 /root/triage/flaky-probe.sh <시험스크립트> <횟수> 를 만드세요. 받은 스크립트를 그 횟수만큼 돌려 한 줄로 runs=<횟수> pass=<성공> fail=<실패> verdict=<판정> 을 내고, 판정은 stable-pass·flaky·stable-fail 중 하나입니다. 종료 코드는 stable-pass 가 0, flaky 가 3, stable-fail 이 4 이고, 인자가 없거나 횟수가 1 이상의 정수가 아니면 2 입니다. 마지막으로 세 스크립트를 각각 8회로 돌린 출력을 /root/triage/flaky-report.txt 에 세 줄로 저장하세요(always-pass, flip, always-fail 순서).
4. /root/triage/tests/seed-test.sh 를 만드세요. 환경 변수 SEED 를 읽어 같은 씨앗이면 언제나 같은 결과를 내되 씨앗에 따라 성공과 실패가 갈리는 시험입니다. SEED 가 비어 있으면 종료 코드 2 로 끝냅니다. 그다음 /root/triage/record-failure.sh <시험스크립트> <기록파일> 을 만드세요. 환경 변수 SEED매번 다른 값을 넣어 최대 30회까지 돌려 첫 실패에서 멈추고, 기록 파일에 다섯 줄을 SEED=, TZ=, LC_ALL=, SCRIPT_SHA256=(시험 스크립트의 sha256 앞 64자리), EXIT_CODE= 로 적고 0 으로 끝냅니다. 30회 안에 실패가 없으면 기록을 남기지 말고 0 이 아닌 값으로 끝냅니다. 마지막으로 /root/triage/replay.sh <기록파일> <시험스크립트> 를 만드세요. 기록의 SCRIPT_SHA256 이 지금 스크립트의 해시와 다르면 시험을 돌리지 말고 종료 코드 2 로 끝내고, 같으면 기록의 SEED·TZ·LC_ALL 을 넣어 시험을 돌린 뒤 그 시험의 종료 코드로 끝냅니다. 셋을 이어 /root/triage/repro/case.env 를 만드세요 — record-failure.sh /root/triage/tests/seed-test.sh /root/triage/repro/case.env 입니다.
5. /root/triage/repo 에 연습용 git 저장소를 만드세요. 커밋은 20개이고 제목은 change 1 부터 change 20 입니다. 각 커밋은 app/rate.pyapp/notes.txt 를 담고, python3 app/rate.py1번부터 11번 커밋까지 100, 12번 커밋부터 250 을 냅니다(12번이 결함을 넣은 커밋입니다). 파드에 git 신원이 없으니 저장소 안에서 git config user.namegit config user.email 을 먼저 세우세요. /root/triage/expected.txt 에는 기대값 100 한 줄을 적습니다. 그다음 /root/triage/oracle.sh 를 만드세요. 체크아웃된 작업 트리의 뿌리에서 돌아 python3 app/rate.py 의 출력이 기대값과 같으면 0, 다르면 1 로 끝납니다. 기대값 파일의 자리는 환경 변수 EXPECT_FILE 로 바꿀 수 있고 기본값은 /root/triage/expected.txt 입니다. 표준출력에는 아무것도 내지 않습니다.
6. /root/triage/repo 에서 첫 커밋을 good, HEAD 를 bad 로 잡고 git bisect run 으로 범인 커밋을 찾으세요. 판정에는 /root/triage/oracle.sh 를 씁니다. 찾은 뒤 /root/triage/bisect/culprit.txt 에 두 줄을 적으세요 — culprit=<40자리 커밋 해시>subject=<그 커밋의 제목>. 그리고 /root/triage/bisect/steps.txt 에 세 줄을 적으세요 — revisions=<good 뒤부터 bad 까지의 커밋 수>, tests=<판정 스크립트가 실제로 불린 횟수>, reason=<왜 그 횟수인지 한 줄 설명>(20자 이상). 끝나면 git bisect reset 으로 저장소를 원래 가지로 되돌려 두세요.
7. /root/triage/repo-skip 에 두 번째 저장소를 만드세요. 커밋 20개에 제목은 같은 꼴이되 이번에는 10번 커밋의 app/rate.py 가 문법 오류라 실행조차 되지 않고, 18번 커밋부터 250 을 냅니다(1번부터 17번까지는 10번을 빼고 모두 100). 그다음 /root/triage/oracle-skip.sh 를 만드세요. oracle.sh 와 같되 app/rate.py 가 없거나 문법이 통과하지 않으면 종료 코드 125 로 끝냅니다. 같은 저장소에서 첫 커밋을 good, HEAD 를 bad 로 잡고 이분 탐색을 두 번 돌리세요 — 한 번은 /root/triage/oracle.sh 로, 한 번은 /root/triage/oracle-skip.sh 로. 결과를 /root/triage/bisect/skip-report.txt 에 네 줄로 적으세요 — naive=<oracle.sh 가 지목한 40자리 해시>, skip=<oracle-skip.sh 가 지목한 40자리 해시>, true=<진짜 범인의 40자리 해시>, limit=<건너뛰기의 한계를 적은 한 줄>(20자 이상). 끝나면 git bisect reset 을 잊지 마세요.
8. /root/triage/investigate.sh <로그파일> <저장소> <보고서파일> 을 만드세요. 로그를 /root/triage/classify.sh 로 분류하고, 갈래가 defect 일 때만 저장소를 git clone 으로 복제해서 첫 커밋부터 HEAD 까지 /root/triage/oracle-skip.sh 로 이분 탐색해 범인을 찾습니다. 받은 저장소는 한 글자도 바뀌면 안 되고, 그 저장소에서 누군가 고치고 있던 파일도 그대로여야 합니다. 보고서는 다섯 줄입니다 — category=, action=, culprit=(defect 가 아니거나 못 찾았으면 -), subject=(마찬가지), reproduce=(다시 일으키는 방법 한 줄). 보고서 파일의 상위 디렉터리가 없으면 만들고, 정상적으로 끝나면 0 으로, 인자가 모자라거나 로그·저장소를 읽을 수 없으면 2 로 끝냅니다. 만든 뒤 두 번 돌리세요 — /root/triage/logs/run-05.log/root/triage/repo-skip 으로 /root/triage/report/case-defect.txt 를, /root/triage/logs/run-01.log/root/triage/repo-skip 으로 /root/triage/report/case-infra.txt 를 만드세요.

참고

단계 8개

  1. 빨간불이 열 개인데 읽은 사람은 아무도 없다
  2. 재시도해도 되는 실패와 절대 재시도하면 안 되는 실패
  3. 한 번 실패한 것을 불안정이라 부를 수는 없다
  4. 재현이 안 되는 실패는 조사할 수가 없다
  5. 이분 탐색이 믿을 수 있는 판정 스크립트를 먼저 만든다
  6. 커밋 열아홉 개를 네 번 만에 좁힌다
  7. 실행조차 안 되는 커밋 하나가 범인을 바꿔 놓았다
  8. 로그 한 편을 넣으면 범인 커밋이 나오게 한다