Kolejki martwych wiadomości: obsługa „trujących” wiadomości w systemach rozproszonych
Zapobiegaj blokowaniu kolejek przez toksyczne komunikaty
Kolejka wiadomości martwych (dead-letter queue) to siatka bezpieczeństwa, która przechwytuje wiadomości, z którymi konsumenci nie mogą sobie poradzić, dzięki czemu jedna niepoprawna ładunek nie blokuje ani nie cicho pomija wszystkich pozostałych w kolejce.
Każdą system oparty na przesyłaniu wiadomości w końcu otrzyma wiadomość, której nie jest w stanie obsłużyć: niepoprawny ładunek, schemat, który uległ zmianie w trakcie przetwarzania przez konsumenta, lub wywołanie downstreamowe, które nie powiedzie się niezależnie od liczby prób ponowienia. Bez kolejki martwych wiadomości taka wiadomość albo na zawsze blokuje początek kolejki, albo jest cicho odrzucana, a oba te scenariusze są gorsze niż świadome poinformowanie o błędzie.
Kolejka martwych wiadomości zamienia niewidoczny błąd w widoczny, możliwy do zbadania. Daje Ci miejsce do izolatowania wiadomości, ustawiania alertów na jej temat oraz podejmowania świadomej decyzji — celowo, a nie przypadkowo — czy naprawić ją i odtworzyć, czy trwale odrzucić.

Mechaniki różnią się w zależności od brokerów, ale podstawowy wzorzec jest wszędzie taki sam: licznik prób dostawy, próg oraz miejsce przeznaczenia dla wiadomości, które go przekroczą. Ten przewodnik omawia, co dokładnie robi kolejka martwych wiadomości, jak odróżnić wiadomość truciznę od przejściowej awarii, kiedy powtórzyć próbę, a kiedy odrzucić, oraz jak bezpiecznie odtworzyć wiadomości po naprawieniu przyczyny źródłowej. Szerszy kontekst wzorców integracji, w jakim ten wzorzec się znajduje, znajdziesz w artykule Architektura aplikacji.
Czym jest kolejka martwych wiadomości
Kolejka martwych wiadomości to osobna, zwykła kolejka, do której broker lub konsument przesyła wiadomość po licznych nieudanych próbach przetwarzania. Nie jest to specjalny konstruktor — kolejka martwych wiadomości w RabbitMQ to zwykła kolejka powiązana ze zwykłą wymianą, a kolejka martwych wiadomości SQS to zwykła kolejka standardowa lub FIFO. Tym, co sprawia, że kolejka jest „kolejką martwych wiadomości”, jest wyłącznie fakt, że coś innego kieruje do niej nieudane wiadomości.
Każdy broker implementuje przekierowanie w inny sposób:
- Amazon SQS wykorzystuje politykę redrive z parametrem
maxReceiveCount. Gdy wiadomość zostanie odebrana tyle razy bez usunięcia, SQS przenosi ją do skonfigurowanegodeadLetterTargetArn. AWS wyraźnie zaleca, aby okres przechowywania wiadomości w kolejce martwych był dłuższy niż w kolejce źródłowej, ponieważ to oryginalna znacznik czasu enqueue — a nie czas przeniesienia — nadal określa wygaśnięcie. - RabbitMQ przekierowuje wiadomość do kolejki martwych, gdy została odrzucona z
requeue=false, wygaśnie jej TTL na poziomie wiadomości, kolejka osiągnie limit długości lub kolejka quorum przekroczydelivery-limit. Konfiguruje się to za pomocą argumentów kolejkix-dead-letter-exchange(i opcjonalniex-dead-letter-routing-key), a RabbitMQ dołącza nagłówkix-deathrejestrujące przyczynę, pochodzącą kolejkę oraz liczbę wystąpień. - Apache Kafka nie ma wbudowanej w broker kolejki martwych wiadomości. Kafka śledzi tylko offsety; nie ma pojęcia o „błędnej” wiadomości. Wzorzec tematu martwych wiadomości to coś, co budujesz w konsumencie, w topologii Kafka Streams lub w connectorze Kafka Connect — często w parze z warstwą retry-topic przed końcowym DLT, tak jak robią to Spring Kafka’s
@RetryableTopiciDeadLetterPublishingRecoverer. - Azure Service Bus automatycznie przekierowuje wiadomości do kolejki martwych, gdy liczba dostarczeń przekroczy
MaxDeliveryCount(domyślnie 10), a także z kilku powodów systemowych, takich jakTTLExpiredException,HeaderSizeExceedediMaxTransferHopCountExceeded, każdy z nich rejestrowany w właściwościDeadLetterReasonwiadomości.
Szerszy przegląd tego, jak brokerzy i platformy strumieniowe współgrają operacyjnie, a nie jako wzorzec niezawodności, znajdziesz w artykułach Apache Kafka Quickstart oraz RabbitMQ na AWS EKS vs SQS, które omawiają stronę infrastruktury uruchamiania tych brokerów.
Wiadomości trucizny
Wiadomość trucizny to taka, która nigdy się nie powiedzie niezależnie od liczby prób ponowienia przez konsumenta — niepoprawny ładunek JSON, pole schematu, które producent zmienił w nazwie, naruszenie reguły biznesowej lub błąd, który zawsze rzuca wyjątek przy określonym wejściu. Jest to coś innego niż awaria przejściowa, gdzie wiadomość jest w porządku, ale środowisko chwilowo nie jest: timeout downstreamowy, błyskawiczna awaria połączenia z bazą danych, odpowiedź o przekroczeniu limitu częstotliwości.
Traktowanie obu typów błędów w ten sam sposób jest najczęstszym błędem w kontekście kolejek martwych wiadomości. Jeśli przekierowujesz do kolejki martwych po pierwszym błędzie, karzesz błędy przejściowe, które przy ponowieniu by się udały. Jeśli próbujesz ponownie setki razy wiadomości trucizny przed poddaniem się, marnujesz obliczenia, opóźniasz inne wiadomości za nimi (w kolejkach uporządkowanych i partycjach) oraz zalewasz logi tym samym wyjątkiem.
Kilka sygnałów detekcji pomaga odróżnić te dwa przypadki:
- Typ wyjątku. Błędy deserializacji, walidacji i błędy stylu
ClassCastExceptionsą prawie zawsze trwałe.DefaultErrorHandlerw Spring Kafka wyraźnie traktuje określone wyjątki jako fatalne i pomija ich ponawianie, zamiast wyczerpywać budżet prób najpierw. - Liczba powtórzeń bez wahania. Tablica nagłówków
x-deathw RabbitMQ pozwala zobaczyć dokładnie, ile razy wiadomość została przekierowana do kolejki martwych i dlaczego; wiadomość z rosnącym licznikiem i identycznymx-first-death-reasonprzy każdym cyklu to trucizna, a nie pech. - Spójna awaria na replikach. Jeśli każda instancja konsumenta zawodzi na tej samej wiadomości, succeeding na wszystkim wokół niej, problemem jest sama wiadomość, a nie infrastruktura.
Aby rozróżniać błędy ponawialne i nieponawialne na poziomie kodu — czyli tę samą klasyfikację, na której opiera się polityka DLQ — zapoznaj się z artykułem Architektura obsługi błędów w Go: Granice i wzorce.
Ponawianie próby vs odrzucenie
Główna decyzja polityczna za każdym DLQ to próg ponowienia: ile prób dostawy otrzymuje wiadomość, zanim zostanie izolowana. Jeśli ustawisz go za nisko, przekierujesz do kolejki martwych wiadomości, które po krótkim wahnięciu downstreamowym by się udały. Jeśli ustawisz go za wysoko, wiadomość trucizny długo siedzi w głównej kolejce, zużywając zasoby workerów i — w systemach uporządkowanych — blokując wszystko, co znajduje się za nią w kolejce.
Obecne wytyczne wśród głównych brokerów zbiegają się w podobnych liczbach:
| Broker | Mechanizm | Typowy próg |
|---|---|---|
| Amazon SQS | maxReceiveCount w polityce redrive |
3–5 dla mieszanych obciążeń przejściowych/trwałych |
| RabbitMQ (kolejki quorum) | argument polityki delivery-limit |
3–5, dostrajany na poziomie kolejki |
| Azure Service Bus | MaxDeliveryCount |
Domyślnie 10, często redukowane dla kolejek wrażliwych na opóźnienia |
| Kafka (poprzez retry topics) | nagłówek retry-count + warstwa retry-topic | 3–4 przeskoki retry-topic przed końcowym DLT |
Praktyczną złotą środka, na którą trafia wiele zespołów, jest: zacznij konserwatywnie (2–3 próby), obserwuj rzeczywisty mix awarii w produkcji i zwiększaj próg tylko dla kolejek, gdzie możesz wykazać, że większość błędów rozwiązuje się w ciągu kilku prób. Połącz liczbę prób z wykładniczym opóźnieniem i jitterem między próbami, aby awaria downstreamowa nie zamieniła się w burzę powtórzeń — ta sama dyscyplina, o której mowa w projektowaniu backoff i breakerów obwodu. Breaker obwodu na granicy integracji uzupełnia ten obraz: przestaje wysyłać żądania do chorej zależności, zamiast pozwalać każdej wiadomości w kolejce indywidualnie odkrywać awarię i przekierowywać się do kolejki martwych jedna po drugiej.
Gdy wiadomość znajdzie się w kolejce martwych, „odrzucenie” nadal powinno być świadomą akcją, a nie zaniedbaniem. Ustaw okres przechowywania na samej kolejce martwych — wystarczająco długi, aby przeprowadzić dochodzenie (AWS zaleca, aby okres przechowywania DLQ przekraczał okres źródła; tydzień to powszechna dolna granica dla kolejek DLQ w RabbitMQ) — i ustawiaj alerty na głębokość i wiek kolejki martwych, aby błędy były triażowane, a nie cicho wygasały. Wiadomość, która wygaśnie z kolejki martwych bez zbadania, to wiadomość, którą postanowiłeś stracić, bez świadomej decyzji o jej utracie.
Idempotentność ma tu takie samo znaczenie jak gdziekolwiek indziej: mogą wystąpić duplikaty — wiadomość, która jest ponownie przesyłana z DLQ z powrotem do głównej kolejki, jest funkcjonalnie duplikatem dostawy. Jeśli Twój konsument nie jest bezpieczny do uruchomienia dwa razy na tej samej wiadomości, ponowne przesłanie z DLQ może stworzyć dokładnie ten błąd skutków ubocznych duplikatu, przed którym próbowałeś się uchronić. Zapoznaj się z artykułem Idempotentność w systemach rozproszonych, która naprawdę działa, aby poznać wzorce po stronie konsumenta, które czynią ponowne przesłanie bezpiecznym.
Strategie odtwarzania
Wypuszczenie wiadomości z DLQ w sposób prawidłowy to odrębna dyscyplina, oddzielna od samego wrzucenia jej tam.
- Najpierw napraw przyczynę źródłową. Wdrożenie naprawy konsumenta przed odtworzeniem to różnica między czystym odzyskaniem a ponownym zatruwaniem kolejki tym samym błędem po raz drugi.
- Ponowne przesłanie celowe, a nie automatyczne. SQS obsługuje funkcję redrive-to-source, która przenosi wiadomości z powrotem do ich oryginalnej kolejki (lub innego miejsca docelowego) na żądanie; RabbitMQ i Kafka wymagają samodzielnego zbudowania odpowiedniego konsumenta lub narzędzia. W każdym traktuj odtwarzanie jako akcję inicjowaną przez operatora z rejestracją tego, co zostało odtworzone i kiedy.
- Zachowaj kolejność tam, gdzie ma to znaczenie. Dla Kafka temat martwych wiadomości powinien mieć co najmniej tyle samo partycji co temat źródłowy i powinien zachowywać oryginalny klucz wiadomości, dzięki czemu odtwarzane wiadomości lądują na właściwej partycji i zachowują kolejność na poziomie klucza.
- Ogranicz liczba prób odtwarzania. Wiadomość, która ponownie zawiedzie po cyklu naprawa-odtwarzanie, nie jest przejściowa — przekieruj ją do trwałego archiwum (tabela w bazie danych, wiadro w storage obiektowym) zamiast zapętlać ją przez DLQ w nieskończoność. Dokumentacja RabbitMQ ostrzega, że wiadomość przekierowana do kolejki martwych może być kierowana między kolejkami tylko ograniczoną liczbę razy (16), po dalszym przekierowaniu z TTL wyłączane jest.
- Nigdy nie pozwalaj, aby DLQ przekierowywała się sama do siebie. Jeśli Twoja DLQ ma własny
x-dead-letter-exchange(RabbitMQ) lub własną politykę redrive (SQS) wskazującą z powrotem na ten sam łańcuch, awaria odtwarzania może stworzyć nieskończoną pętlę. Utrzymuj własną konfigurację martwych wiadomości DLQ pustą lub wskaż ją na ściśle terminalne archiwum. - Ustaw alerty na objętość, a nie tylko na obecność. Pojedyncza wiadomość w DLQ to punkt danych; nagły skok to incydent. Podłącz głębokość DLQ i wiek wiadomości do tego samego pipeline’u alertowego, którego używasz do wszystkiego innego — zapoznaj się z artykułem Nowoczesny projekt systemów alertowych dla zespołów observability, aby poznać praktyki routingu i redukcji szumów, które bezpośrednio dotyczą alertów DLQ.
Jeśli Twój przepływ pracy obejmuje procesy wielokrokowe o długim czasie trwania zamiast pojedynczych wiadomości, to samo myślenie o martwych kolejkach ma zastosowanie na warstwie przepływu pracy — logika rekompensaty sagi potrzebuje tej samej dyscypliny „izolacja, inspekcja, decyzja”, gdy krok zawodzi trwale, a nie przejściowo. A gdy zdarzenia same pochodzą z zapisu do bazy danych, wzorzec transactional outbox już buduje obsługę martwych wiadomości w workerze relay, więc wzorzec pojawia się o warstwę wcześniej niż broker.
Gdzie DLQ wpasowują się w szerszy kontekst
Kolejka martwych wiadomości nie sprawia, że awarie przestają istnieć — sprawia, że są przetrwalne i możliwe do przeglądania, zamiast być ciche. Działa najlepiej w towarzystwie powtórzeń z backoff dla przypadku przejściowego, idempotentnych konsumentów, dzięki czemu ponowne przesłanie jest bezpieczne, oraz breakera obwodu, aby chora zależność nie zalewała głównej kolejki (a w konsekwencji DLQ) tym samym błędem tysiące razy. Traktuj próg DLQ, okres przechowywania i alerty jako decyzje konfiguracji pierwszej klasy, a nie domyślne ustawienia, których nie dotkniemy, a martwe wiadomości staną się narzędziem diagnostycznym, a nie miejscem, gdzie dane cicho znikają.