수만 건을 외부 시스템에 다시 보내야 했는데 한 번에 돌리면 중간에 실패했을 때 어디까지 됐는지 알 수 없었다. 나눠서 하기로 했지만 나누는 것만으로는 부족했다.
Table of contents
Open Table of contents
문제 — 대상의 고정
제일 먼저 부딪힌 것이 대상을 어떻게 잡느냐였다. 처음에는 아직 처리 안 된 것이라는 조건으로 잡았는데 처리하면 그 조건에서 빠진다는 문제가 있었다.
진행률을 재려고 다시 세면 숫자가 줄고 성공할수록 대상이 사라지니 아무것도 안 된 것처럼 보인다. 그래서 시작할 때 대상 행에 wave_no를 UPDATE로 찍어 고정하고 그다음부터는 WHERE wave_no = N으로 대상을 지정했다. 조건이 바뀌어도 대상이 안 바뀌는 것이 이 방식의 핵심이다.
여기서도 실수를 했는데 조건에 다른 경로로 이미 완료된 것이 빠져 있어서 그것들이 계속 후보로 뽑혔다. status만 보고 external_id는 안 봤기 때문이었고 조건에 external_id IS NULL을 추가하고 나서야 정리됐다.
판단 기준 — 웨이브 크기와 실패율
50건씩과 100건씩을 각각 돌려 보니 실패율이 달랐다. 50건일 때 한 자릿수 퍼센트였고 100건일 때 그 두 배쯤이었다.
크게 잡을수록 처리 시간이 길어지면서 timeout에 걸리거나 Lock wait timeout 경합이 나기 때문이었다. 두 가지 방법이 있었는데 크기를 고정값으로 정하는 것과 실패율을 보면서 조절하는 것이다. 뒤쪽을 골랐다. 이유는 시간 초과와 잠금 경합이 상대 쪽 부하에 따라 달라져 고정값이 오늘 맞아도 내일 안 맞아서다. 대신 웨이브마다 실패율을 사람이 보고 크기를 정하는 수고를 감수했다.
검증 — 응답을 믿지 않는 판정
여기가 가장 중요했다. 호출이 성공 응답을 돌려주고 실패 목록이 비어 있어도 실제로는 안 된 것이 있었는데 내부 검증에서 걸리면 아무것도 반환하지 않고 조용히 건너뛰는 경로가 있었다.
그래서 성공 판정을 데이터로 한다. 웨이브별 COUNT(*)와 status = 'sent' 건수와 external_id IS NULL 건수를 함께 세면 응답이 무엇이라고 하든 실제 상태가 나온다. 응답은 요청이 접수됐다는 것이지 처리됐다는 증거가 아니다.
주의 — 끊겼을 때의 재시도
한 번은 앞단 프록시가 502로 끊겨서 응답을 아예 못 받았는데 이때는 재시도하면 안 된다. 응답을 못 받은 것과 처리가 안 된 것은 다르고 뒤에서는 이미 접수했을 수 있어서 그대로 다시 보내면 중복이 생긴다.
그래서 그 웨이브의 실제 처리량을 세고 안 된 것만 골라 그것만 다시 보낸다. 한 번은 100건 중 한 자릿수 건만 실제 미처리였고 전량 재전송했으면 90여 건이 중복됐을 것이다.
조치 — 실패 사유의 분류
실패한 것을 그냥 다시 보내면 대개 또 실패해서 사유별로 갈랐다. timeout이나 잠금 경합 같은 일시적 문제는 다시 하면 되고 필수값 NULL이나 형식 불일치 같은 데이터 문제는 데이터를 고쳐야 한다.
상대가 거부하는 외부 문제는 우리가 못 고치고 애초에 보내면 안 되는 대상 아님이 나오면 대상 선정으로 돌아가야 한다. 실제로 한 번은 한 번도 등록된 적 없는 것이 대상에 섞여 있었다.
웨이브마다 건수와 성공과 실패와 사유별 내역을 기록으로 남겼다. 다음 웨이브 크기를 정하는 근거가 되고 나중에 개별 건의 경위를 추적할 수 있다. 기록을 안 남기고 진행했다가 며칠 뒤에 처음부터 다시 조사한 적이 있다.
정리
- 조건으로 대상을 잡으면 처리할수록 대상이 사라진다
- 시작할 때
wave_no표식으로 대상을 고정한다 - 조건에
external_id IS NULL결과 컬럼을 함께 건다 - 크기가 커지면 실패율이 오르므로 실패율을 보며 조절한다
- 성공 응답을 믿지 않고 데이터로 세어 확인한다
- 응답을 못 받았으면 재시도 전에 실제 처리량부터 센다
- 실패 사유를 분류하고 대상 아님이 나오면 대상 선정으로 돌아간다
- 웨이브별 기록이 없으면 며칠 뒤 처음부터 다시 조사하게 된다