큐와 비동기 API · 재시도와 DLQ · 이론
무한 재시도가 만드는 것
한 줄 요약
재시도에는 상한, 지수 백오프, 지터, 그리고 포기 후 갈 곳이 있어야 한다. 넷 중 하나라도 없으면 재시도는 장애 증폭기다.
왜 이게 필요했나
가장 나쁜 재시도는 즉시, 고정 간격으로, 무한히 하는 것입니다. 서버가 잠깐 과부하로 느려졌을 때 모든 클라이언트가 곧바로 다시 때리면 겨우 버티던 서버가 완전히 주저앉습니다. 재시도가 장애를 치료하는 대신 악화시킵니다.
첫 번째 개선은 지수 백오프입니다. 1초, 2초, 4초, 8초로 간격을 늘립니다. 실패가 계속될수록 재시도 압력이 지수적으로 줄어 서버에 숨 쉴 틈을 줍니다.
두 번째가 지터입니다. 지수 백오프만으로는 부족합니다. 서버가 잠시 죽으면 1만 개 클라이언트가 동시에 실패를 감지하고, 모두 같은 규칙을 따라 정확히 같은 순간에 재시도합니다. 서버가 회복하려는 찰나에 1만 개가 쏟아지고 다시 쓰러집니다. 그리고 이 파도는 2초, 4초, 8초 뒤에도 반복됩니다. 완전 지터(full jitter)는 0부터 계산된 백오프 값 사이의 무작위 시간을 기다려 이 동기화를 깹니다.
어떻게 동작하나
상한과 포기도 필요합니다. 간격이 무한정 커지지 않게 최대 대기 시간(예: 30초)을 두고, 일정 횟수 후에는 포기합니다. 포기한 메시지가 갈 곳이 DLQ(dead-letter queue)입니다.
DLQ 는 쓰레기통이 아니라 조사 대상 목록입니다. 그래서 메시지만 넣으면 쓸모가 없습니다. 함께 넣어야 할 것은 넷입니다 — 원래 메시지, 실패 원인(예외 메시지나 상태 코드), 시도 횟수, 그리고 최초 수신 시각입니다. 이것이 있어야 "이 메시지는 왜 여기 있는가"에 답할 수 있고, 원인을 고친 뒤 재처리할 수 있습니다.
재처리 스크립트도 미리 만들어 두어야 합니다. 새벽 3시에 DLQ 가 쌓였을 때 재처리 도구를 그 자리에서 만들게 되면 실수가 납니다.
그리고 아무 실패나 재시도해서는 안 됩니다. 재시도가 의미 있는 것은 일시적 오류뿐입니다. 메시지 자체가 잘못된 경우(스키마 위반, 존재하지 않는 참조)는 100번을 시도해도 같습니다. 이런 것은 첫 실패에서 바로 DLQ 로 보내는 편이 낫습니다. 이 구분을 하지 않으면 잘못된 메시지 하나가 큐 전체를 막습니다 — 이것을 독이 든 메시지(poison message)라고 부릅니다.
현장에서 만나는 모습
DLQ 에 알림을 걸지 않는 팀이 많습니다. DLQ 는 조용하기 때문에 몇 주 뒤에 발견됩니다. "DLQ 깊이 > 0" 은 페이지가 아니라도 최소한 하루 한 번은 누군가 봐야 하는 지표입니다.
또 하나. DLQ 자체의 소비자를 만들어 자동 재처리하게 두는 설계는 위험합니다. 원인이 그대로면 무한 루프가 됩니다. 재처리는 사람이 원인을 확인한 뒤 명시적으로 트리거하는 편이 안전합니다.
다음 실습에서 할 것
큐 소비자에 지수 백오프와 완전 지터를 붙이고, 상한을 걸고, 5회 실패 후 DLQ 로 보내고, DLQ 메시지에 원인과 시도 횟수를 담고, 마지막에 원인을 제거한 뒤 재처리 스크립트로 복구합니다.