실패의 대부분은 실패가 아니었다
무인으로 몇 주째 돌던 파이프라인이 있다. 그 실패 로그가 168건까지 쌓였고, 성공은 132건이었다. 비율만 보면 망가진 시스템이다. 무엇을 고치기 전에 로그를 항목의 의미대로 먼저 갈라봤다 — 그랬더니 168건 중 124건은 실패가 아니었다. 의도된 건너뜀이었다. GPU를 많이 쓰는 작업이 이미 돌고 있어 메모리가 빠듯할 때, 파이프라인이 한 발 물러서서 그 항목을 다음 회차 대기열에 남겨두는 동작. 내가 설계한 그대로였다. 다만 그걸 진짜 고장과 같은 파일에 적고 있었을 뿐이다.
진짜 실패는 44건 남았다. 168건과는 전혀 다른 문제고, 이제야 읽을 수 있는 문제다.
가면 스물여덟 개를 쓴 사고 하나
44건 중 28건은 이유가 아예 기록되지 않았다 — 왜인지 말하기도 전에 실행이 죽었다. 그리고 전날 저녁 어느 한 시각 이후에 몰려 있었고, 그중 21건은 연속이었다. 그 순간 무언가가 바뀐 것이다. 저장소 안에서는 아무것도 바뀌지 않았는데.
원인은 코드 밖에 있었다. OS 개발도구 사용 약관에 동의하지 않은 상태가 되면서 시스템 파이썬(/usr/bin/python3)이 실행을 거부하는 껍데기로 변해 있었다. 내가 직접 터미널에서 두드릴 때는 멀쩡했다 — 내 셸은 패키지 매니저가 깔아둔 파이썬을 먼저 찾기 때문이다. 스케줄러는 내 셸을 물려받지 않는다. 스케줄러는 껍데기를 집어 들었고, 내 코드 첫 줄에 닿기도 전에 죽었다.
약관에 동의하니 당장의 고장은 사라졌다. 스케줄러의 작업 정의에 경로(PATH)를 명시해 박아 넣으니 이 부류의 고장이 사라졌다. 이제 데몬은 내 운을 빌리지 않고 스스로 진짜 인터프리터를 찾는다.
증거를 스스로 지우는 결함 둘
남은 16건은 두 모양이었다. 여섯 건은 “일치하는 것 없음”, 열 건은 오직 “Traceback” 한 줄. 서로 무관한 두 버그처럼 보였고, 실제로도 결함은 정확히 두 개였다. 다만 성격은 같았다 — 실패하면서 그 이유를 같이 파괴하는 단계였다.
분할기는 긴 원문을 섹션 단위로 잘라 모델에 넘긴다. 섹션 제목이 없는 짧은 문서를 주자 조각을 0개 반환했고, 그다음 단계가 아무것도 매치되지 않는 와일드카드에서 넘어졌다. 문서가 잘못된 게 아니라, 분할기가 “이미 충분히 짧다”는 경우에 대한 답을 갖고 있지 않았던 것이다. 지금은 문서 전체를 조각 하나로 반환한다.
품질 게이트는 모델이 내놓은 출력 파일을 읽어 점수를 매긴다. 모델이 시간 초과로 그 파일을 아예 만들지 못했을 때, 게이트는 파일을 여는 그 줄에서 죽었고, 로그에 남은 건 게이트를 가리키는 스택 트레이스뿐이었다 — 원인인 시간 초과가 아니라. 지금은 파일이 존재하고 비어 있지 않은지 먼저 확인하고, 아니면 그 사실을 말하는 깨끗한 실패 판정을 돌려준다.
두 결함 모두 사슬에서 모델보다 앞에 있다. 그래서 모델 출력 실패 유형과 같이 묶지 않고 절차 문서에서 별도 항목으로 갈라놨다. 배관이 터진 것과 글이 잘못 나온 것은 완전히 다른 곳에서 진단해야 한다.
여기서 남길 것
가장 오래된 실패 문서 두 건을 처음부터 끝까지 다시 돌려봤다. 둘 다 통과했다. 애초에 잘못된 구석이 없던 문서들이었다.
교훈은 이 세 버그에 있지 않다. “내가 안 하기로 했다”와 “내가 못 했다”를 같이 적는 로그는 영원히 재난처럼 읽힌다는 것, 그리고 오류를 처리하다 죽는 오류 처리기는 아예 없느니만 못하다는 것 — 원래의 증거 자리에 자기 스택 트레이스를 대신 밀어 넣기 때문이다. 나는 이 세션의 앞부분을, 한 번도 사실이었던 적 없는 숫자를 들여다보며 썼다.