resend:products 대량 재전송 작업을 웨이브로 나눠 수십 개 웨이브에 걸쳐 수만 건을 사고 없이 처리했다. 그러고 나서 이어 돌린 다음 배치에서 처음으로 사고가 났다.
Table of contents
Open Table of contents
배경 — 응답과 다른 실제 상태
배치를 아홉 청크로 나눠 보냈는데 전부 200 이고 응답의 failed 목록도 비어 있었다. 응답만 놓고 보면 아무 문제도 없어 보이는 결과였고 그대로 넘어갈 수 있는 상황이었다.
응답을 믿지 않고 channel_products.status 를 되읽어 보니 수십 건 중 삼분의 일이 OFF_SALE 로 전환돼 있었다. 이 사후 검사를 안 했으면 이 배치도 아무 표시 없이 완료로 넘어갔을 것이다.
검증 — 인과를 확정할 근거
여기서 전송 직후에 전환됐다는 것과 전송이 전환시켰다는 것은 서로 다른 얘기다. 우연히 그 시간대에 전혀 다른 이유로 전환됐을 가능성도 남아 있었다.
전송 전 스냅숏을 떠 놓지 않았으므로 전후를 비교할 대상 자체가 없었다. 두 가지 방법이 있었는데 채널 쪽에 문의해 그쪽 로그를 받는 것과 우리 쪽 updated_at 분포로 판별하는 것이다. 뒤쪽을 골랐다. 이유는 채널 문의는 며칠이 걸리고 그동안 배치를 세워 둬야 해서다. 대신 채널 쪽 원인은 이 방법으로 못 보는 것을 감수했다. 전환된 건들의 updated_at 을 전부 조회해서 그 분포가 어떤 모양인지를 봤다.
원인 — 분포가 가른 것
그 삼분의 일의 updated_at 이 1분이 안 되는 구간에 순차적으로 몰려 있었고 sent_at 이 바로 그 직전이었다. 개별 상품의 사유로 전환된 것이라면 그 시각이 여기저기 흩어져 있어야 한다.
짧은 구간에 순차로 몰린 분포는 개별 사유로 나올 수 없으므로 군집 하나로 전송 기인성이 확정됐다. 사전 스냅숏이 없어도 갱신 시각은 어차피 있으므로 이 판별은 언제든 쓸 수 있다.
주의 — 실적이 보장하지 않는 안전
이 사고에서 얻은 교훈이 하나 더 있었는데 실적에 관한 것이었다. 앞선 수십 개 웨이브가 전부 무사고였는데 이 배치에서는 삼분의 일이 OFF_SALE 로 전환됐기 때문이다.
누적 실적이 안전을 보장하지 않는다는 뜻이므로 웨이브 전체 무사고 실적을 다음 배치의 안전 근거로 삼지 않기로 했다. 매 배치마다 사후 검사를 하고 지금까지 괜찮았으니 이번도 괜찮으리라는 판단을 쓰지 않는다.
조치 — 완료로 표시하지 않기
사고를 발견하고 즉시 중단하면서 그 배치의 wave_no 태깅까지 함께 보류해 뒀다. 태깅하면 처리 완료로 표시돼 재처리 대상에서 빠져 버리기 때문이다.
전송이 원인이라는 것은 확정됐지만 왜 그렇게 되는지는 미확정으로 뒀다. 자체 교정 루프와 외부 삭제로 인한 전환 오류와 extend:sale 미동작 세 가설의 시그니처가 비슷해서 셋을 배제하기 전에는 원인을 단정하지 않기로 했고 전환된 건들의 product_id 범위가 다른 배치보다 넓다는 관측도 단정 없이 INCIDENT.md 에 단서로 기록했다.
정리
- 응답이 전부 성공이어도 사후 검사를 한다
- 사전 스냅숏이 없어도 갱신 시각 분포로 인과를 확정할 수 있다
- 짧은 구간에 순차로 몰린 분포는 개별 사유로 안 나온다
- 무사고 실적을 다음 배치의 안전 근거로 삼지 않는다
- 사고 난 배치는 완료로 표시하지 않는다
- 재처리 대상으로 남긴다
- 시그니처가 비슷한 가설이 여럿이면 단정하지 않는다
- 단정할 근거 없는 관측도 다음 조사의 단서로 기록한다