자동화 파이프라인8 분 읽기

대기열에 40편이 있었는데, 맨 앞 한 편이 사흘을 통째로 먹었습니다

무인 잡이 사흘 동안 아무것도 만들지 못했습니다. 감시판은 초록이었고 대기열도 차 있었습니다. 원인은 대기열에서 맨 앞 하나만 꺼내는 한 줄이었습니다.

#automation#monitoring#debugging#verification#reality-check
개념 도식: 대기열 맨 앞의 실패한 한 건이 뒤의 40건을 막고 있는 구조와, 그 동안 감시판이 초록으로 보이는 이유
글 내용을 요약한 개념 도식.

무인 잡 하나가 사흘 동안 결과물을 0건 만들었습니다.

죽지도 않았고, 알림도 안 왔고, 감시판은 계속 초록이었습니다. 저는 오늘 아침 완전히 다른 걸 조사하다가 우연히 발견했습니다.

당신의 자동화가 "돌았지만 아무것도 안 만든 날"을, 무엇이 알려줍니까?

로그는 이렇게 생겼습니다

09-15 18:05  social-growth-engine  claude_fail
09-15 18:06  social-growth-engine  claude_fail
09-16 10:00  social-growth-engine  claude_fail
09-16 10:01  social-growth-engine  claude_fail
09-16 18:05  social-growth-engine  claude_fail
09-16 18:06  social-growth-engine  claude_fail

세 번의 실행, 매번 두 번씩 재시도, 전부 같은 항목에서 실패했습니다.

그 잡의 대기열에는 미처리 항목이 마흔 편 넘게 있었습니다. 사흘 동안 손도 안 댔습니다.

범인은 한 줄이었습니다

cands = pending(lang)
slug, md = cands[0]      # 맨 앞 하나만

대기열에서 맨 앞 하나만 꺼내서 처리합니다. 실패하면 그 회차는 끝입니다. 다음 회차가 오면 다시 대기열을 읽고, 실패한 그 항목이 여전히 맨 앞에 있으니, 다시 그걸 집습니다.

단일 실패점입니다. 뒤에 아무리 멀쩡한 재고가 쌓여 있어도 맨 앞 한 건이 죽어 있으면 파이프라인 전체 산출이 0입니다.

그런데 왜 아무도 몰랐나

여기가 진짜 교훈입니다. 이 잡을 감시하는 화면이 두 개 있었고, 둘 다 정상으로 보였습니다.

첫째, 감시판은 산출물의 신선도를 봅니다. 종료코드는 거짓말을 하니까 산출물을 보기로 한 겁니다 — 좋은 설계입니다. 그런데 이 잡의 산출물로 등록된 파일이 로그 파일이었습니다. 실패할 때마다 실패 줄이 찍히니까 로그 파일은 계속 자랍니다. 신선도만 보면 방금 일한 봇입니다.

둘째, 대기열도 차 있었습니다. 같은 날 다른 경로(수동 배치)로 12편이 들어와 있었습니다. 대기열 깊이를 보면 건강해 보입니다.

즉 "실패 줄이 로그를 키워서 초록"과 "다른 경로가 큐를 채워서 초록"이 겹쳤습니다. 두 계측기 모두 정직하게 자기 일을 했고, 둘 다 진실을 못 봤습니다.

당신이라면 무엇부터 고치겠습니까

실패한 그 항목을 고치겠습니까, 아니면 구조를 고치겠습니까.

저는 그 항목을 먼저 수동으로 돌려봤습니다. 22초 만에 성공했습니다. 항목 자체엔 아무 문제가 없었습니다(막은 건 별개의 인증 문제였고, 그건 다른 글에서 다뤘습니다).

그러니까 그 항목만 고쳤으면 오늘은 복구됐을 겁니다. 그리고 다음에 다른 항목이 일시적으로 막히면 똑같이 사흘을 잃습니다. 증상만 고치는 겁니다.

구조 쪽 수정은 이렇게 됩니다.

for slug, md in cands[:MAX_CANDS]:      # 앞에서부터 최대 3건
    got, generated = _gen_slug(lang, slug, md)
    if generated:
        return got

여기서 한 번 더 조심해야 합니다

"실패하면 다음 걸로 넘어간다"를 단순하게 구현하면 새 사고가 납니다.

이 파이프라인은 생성(비싼 호출)과 렌더(그 결과물로 영상 만들기) 두 단계입니다. 렌더가 실패했다고 다음 후보로 넘어가면, 한 번 돌 때마다 재고를 세 건씩 태웁니다. 대본은 만들어졌는데 영상이 안 나왔을 뿐인데, 그 항목은 처리된 것으로 표시되고 다음 항목까지 소비됩니다.

그래서 반환값을 둘로 나눴습니다 — 만들어졌는가(다음으로 넘어갈지 결정)와 완성됐는가(회차 성과). 하류 실패가 상류 재고를 먹지 않게 하는 경계입니다.

테스트도 그 경계를 정확히 겨눕니다. 하나는 "막힌 후보가 있어도 회차가 빈손이 아니어야 한다", 다른 하나는 "렌더가 실패해도 호출 횟수가 1을 넘으면 안 된다".

자가진단 3개

  1. 대기열에서 작업을 꺼내는 코드가 맨 앞 하나만 집습니까?
  2. 그 잡의 "일했다는 증거" 파일이 실패해도 자라는 파일입니까? (로그를 산출물로 등록하면 대부분 그렇습니다)
  3. 재고가 있는데 산출이 0인 회차를 알려주는 장치가 있습니까? 신선도로는 절대 안 잡힙니다.

솔직한 부분

3번은 아직 안 만들었습니다. 오늘 고친 건 단일 실패점 하나뿐이고, "재고는 있는데 산출 0"을 직접 감시하는 장치는 없습니다. 지금은 후보 세 건이 연속으로 막혀야 회차가 빈손이 되니 확률은 크게 줄었지만, 줄어든 것과 보이는 것은 다릅니다.

사흘치 손실의 크기도 정직하게 말하면 크지 않습니다 — 재고가 21일치 쌓여 있어서 하류가 굶지는 않았습니다. 그래서 더 위험했습니다. 아무도 아프지 않은 고장이 제일 오래 삽니다.

지금 당신의 잡 하나를 골라, 마지막 회차가 실제로 뭘 만들었는지 확인해보세요. 로그가 자랐다는 것 말고요.

관련 글