한 채널의 상품 연동 실패가 폴백 로직이 있는데도 계속 쌓이고 있었다. 실패 건수를 세려고 조회를 걸었는데 한참을 기다려도 끝나지 않았다.
Table of contents
Open Table of contents
조사 — 조회부터 막힌 실행 계획
EXPLAIN 을 보니 type: ALL 전수 스캔이었다. idx_supplier 가 있는데도 안 타고 있었고 last_error LIKE '%image%' 처럼 양쪽 와일드카드가 붙은 조건 때문에 옵티마이저가 그쪽을 포기한 것으로 보였다. MySQL 8.0 매뉴얼도 LIKE 인자가 와일드카드로 시작하지 않는 상수일 때만 인덱스를 쓴다고 적고 있다.
FORCE INDEX (idx_supplier) 로 지정하니 조회가 바로 끝났다. 규모를 보니 한 달 만에 수십 배로 늘어 누적 미완료가 수천 건을 넘고 있었다.
증상 — 두 시점의 같은 오류
실패 메시지를 묶어 보니 크게 두 패턴으로 갈렸다. 상대가 이미지 크기를 계산할 때와 이미지를 내려받아 등록할 때 각각 실패하고 있었다.
둘 다 같은 IMAGE_FETCH_FAILED 코드였고 시점만 달랐다. 상대 서버가 우리 이미지 주소에 접근할 때 실패하고 있다는 뜻이었고 우리가 직접 호출하면 정상이었으므로 상대 쪽에서 대량으로 접근할 때만 간헐적으로 나는 성격으로 보였다.
원인 — 같은 곳을 가리킨 폴백
원래 의문으로 돌아가서 ImageSender 의 폴백 코드를 열어 봤다. 우리 주소가 안 되면 상대 쪽에 이미 등록된 이미지를 쓰자는 발상이었고 그 자체는 합리적이다.
문제는 이미 등록된 이미지의 주소도 우리 CloudFront 도메인이라는 점이었다. 상대가 그것을 재검증할 때 똑같이 실패하므로 폴백이 실패 원인을 피해 가지 못하고 같은 곳으로 간다.
판단 기준 — 폴백의 조건
여기가 이 건의 핵심이었다. 폴백의 조건은 실패 원인을 피해 가는 것인데 원인이 우리 도메인에 상대가 접근하지 못하는 것이면 폴백도 그 도메인을 쓰는 한 원인을 안 피한다.
결과적으로 두 번 실패하면서 시간만 두 배로 쓰게 된다. 폴백이 없는 것과 같은 상태이면서 있는 것처럼 보이므로 더 나쁘다.
조치 — 재설계와 복구
폴백을 어떻게 바꿔야 하는지는 두 가지 방법이 있었다. 다른 도메인 주소로 변환해 보내는 것과 주소를 주는 대신 이미지 바이트를 multipart/form-data 로 직접 올리는 것이다.
뒤쪽을 골랐다. 이유는 상대가 우리 서버에 접근할 필요 자체를 없애서다. 대신 상품마다 이미지를 우리가 내려받아 올리므로 전송 시간과 트래픽이 늘어나는 것을 감수했다. 밀린 수천 건은 channel_jobs.attempts 를 0 으로 리셋하면 자동 재시도가 도므로 새로 넣을 필요가 없었다.
마지막으로 순서를 정했는데 이미지 서버 인프라 확인이 코드 수정보다 먼저다. 폴백을 고쳐도 원인이 우리 서버의 간헐 실패면 다른 소비자에게서 또 터지고 서버가 정상인지 모르면 리셋해도 또 실패하므로 우리 쪽에 원인이 있을 가능성을 먼저 닫아야 한다.
정리
- 폴백이 있는데 실패가 쌓이면 폴백을 의심한다
- 폴백의 조건은 실패 원인을 피해 가는 것이다
- 같은 곳으로 가면 폴백이 아니고 시간만 두 배 쓴다
- 상대가 우리에게 접근 못 하는 것이 원인이면 접근 자체를 없앤다
- 로컬에서 정상이어도 대량 접근 시에만 나는 간헐 실패가 있다
LIKE '%…%'조건이 붙으면 인덱스를 포기할 수 있다- 밀린 건은
attempts리셋으로 자동 재시도를 태운다 - 인프라 확인이 코드 수정보다 먼저다