큐와 비동기 API · 재시도와 DLQ · 실습
지수 백오프와 DLQ 구현하기
목표
재시도의 네 요소 — 지수 백오프, 지터, 상한, 포기 후 DLQ — 를 모두 구현하고, DLQ 에 담긴 정보만으로 원인을 파악해 재처리까지 완주한다.
왜 중요한가
재시도는 가장 쉬워 보이지만 가장 자주 사고를 만드는 코드입니다. 고정 간격 무한 재시도는 회복하려는 서버를 주기적으로 다시 쓰러뜨립니다. 지수 백오프만 넣으면 1만 개 클라이언트가 여전히 같은 순간에 재시도해 파도를 만듭니다. 지터까지 넣어도 상한이 없으면 10번째 시도에서 17분을 기다립니다. 그리고 포기 조건이 없으면 잘못된 메시지 하나가 큐 전체를 영원히 막습니다. 이것을 독이 든 메시지라고 부릅니다. DLQ 는 그 메시지를 격리해 나머지를 흐르게 하는 장치이고, 동시에 조사 대상 목록입니다. 그래서 메시지만 넣으면 쓸모가 없습니다 — 원인과 시도 횟수와 최초 시각이 함께 있어야 원인을 고치고 재처리할 수 있습니다.
단계
1. /root/qr/worker.py 로 q:tasks 를 소비하는 워커를 만든다. /opt/app/taskproc.py 의 process(msg) 는 msg["kind"] 가 bad 면 예외를 던진다.
2. 실패한 메시지를 시도 횟수를 1 올려 다시 큐에 넣는다. /root/qr/requeue.out 에 attempts=2 가 보여야 한다.
3. /root/qr/schedule.txt 에 base 1초, 시도 0~5 의 지수 백오프 값을 attempt=<n> wait=<초> 형식으로 6줄 적는다. 1, 2, 4, 8, 16, 32 이다.
4. /root/qr/jitter.txt 에 시도 번호 3의 완전 지터 대기 시간 20개를 한 줄씩 적는다. 모두 0 이상 8 이하이고 서로 다른 값이 15개 이상이어야 한다.
5. 상한 30초를 적용해 /root/qr/capped.txt 에 시도 0~8 의 값을 적는다. 마지막 세 값은 모두 30 이어야 한다.
6. 5회 실패한 메시지는 q:tasks:dlq 로 보낸다. q:tasks 는 비고 DLQ 길이가 1 이어야 한다.
7. DLQ 메시지 JSON 에 payload, error, attempts, first_seen_at 네 키가 모두 있어야 하고 attempts 는 5 이다.
8. /root/qr/replay.py 는 DLQ 에서 꺼내 attempts 를 0 으로 되돌리고 q:tasks 에 넣는다. 실행 후 DLQ 는 비고 q:tasks 에 1건이 있어야 한다.
참고
- 완전 지터:
wait = random.uniform(0, min(cap, base * 2 ** attempt)) - 재시도할 오류: 타임아웃, 503, 429. 재시도하지 말 것: 스키마 위반, 존재하지 않는 참조.
- 흔한 실수 1: DLQ 소비자를 만들어 자동 재처리하는 것 — 원인이 그대로면 무한 루프가 됩니다.
- 흔한 실수 2: DLQ 깊이에 알림을 걸지 않는 것 — DLQ 는 조용해서 몇 주 뒤에 발견됩니다.
단계 8개
- 큐 소비 워커 만들기
- 실패한 메시지 재큐잉하기
- 지수 백오프 스케줄 계산하기
- 완전 지터 적용하기
- 대기 시간 상한 걸기
- 5회 실패 후 DLQ 로 보내기
- DLQ 메시지에 조사용 정보 담기
- 원인 제거 후 재처리하기