RabbitMQ wysyłka email

0

W pewnym projekcie powstał problem na produkcji. Emaile do klientów z fakturami się nie wysyłają. Architektura jest taka, że job wstaje codziennie o określonej godzinie i wysyła wystawione faktury klientom. Aby to zrobić wrzuca na kolejkę to co trzeba wysłać a inna aplikacja odpowiedzialna za wysyłkę to odbiera i wysyła (komunikuje się z serwerem pocztowym itd.). Gdy wysyłka się nie powiedzie aplikacja do wysyłki wrzuca na topic failure to co trzeba było wysłać a dalej już z tego topicu nic nie odbiera. Aktualnie na topicu failure wisi około 70 rzeczy do wysyłki, nie udało się wysłać z różnych powodów np. Request timeout.
Czy jest lepsze rozwiązanie?
Może job powinien wrzucać na kolejke wszystko to co jest do wysyłki uwzględniając to co nie udało się wysłać? Wtedy wysyłka tych nieudanych byłaby ponawiana codziennie. Tylko nie ma on informacji o tym czy się udało czy nie, wysyłka odbywa się w innym serwisie. Może ten serwis powinien wrzucać na jakiś topic np. success jako response czy się udało czy nie?

4

Miałem podobną zagwozdkę kiedyś w jednej z firmy. Opiszę ci trochę, jak my to rozwiązaliśmy.

Otóż był sobie task - w twoim przypadku jest to wysyłka mejla akurat.

Był sobie jakiś tam obiekt. W twoim przypadku jest to faktura. W db mieliśmy tam rekord tak dla spokoju typu Faktura() { created_at, updated_at, invoice, sent=False }

Był wykonywany jakiś task. Task się mógł udać, lub nie. Jeśli się udał, to spoko, oprócz wykonania taska i zrobienia danej czynności, na sam koniec musiał też polecieć aptejd do bazy, że dana Faktura sent=True.

Jeśli się nie udał task, to to, co się działo dalej, zależało od sytuacji.

Były czasami taski, które chcemy retrajować w nieskończoność, muszą się udać albo być powtarzane ciągle.
Tutaj logika była prosta, failure worker zjadał sobie taskiz kolejki, jak się nie udało, odkładał na górę, jak się udało oznaczał rekord w bazie i commitował taska. I tak w kółko. Tutaj potencjalne zagrożenie: kolejka która rośnie w nieskończoność.

Były taski, które miały się retrajować maksymalnie X razy. Wtedy mieliśmy dwa różne podejścia.
Pierwszym jest podobny przypadek co wyżej, ale z modyfikacją. To znaczy robimy maksymalnie X retrajów, oddalonych od siebie w jakiś plus minus stałych odstępach czasowych albo podobnych i tyle. Jeśli się uda retrajing - super. Jeśli po X razach task dalej się nie uda, wrzucaj go na kolejke failed_retrying. I tutaj masz dwa podejscia rozbicia tego problemu: ze schedulingiem tzn każdy retraj co X minut, albo po prostu FIFO na kolejce i w ten sposób taski będą konsumowane.

Drugim rozwiązaniem był exponential backoff. Robiliśmy X retrajów, każdy oddalony od drugiego wykładniczo dłużej w czasie. Po X nieudanym retraju tak samo - na failed_retrying topic/kolejke.
Tutaj też pamiętam, że rozważaliśmy dwa rozwiązania odnośnie implementacji - jeden topic ze wszystkimi failurami i do danych taska pchać kiedy ma się wykonać vs failury per backoff czyli topic_5m_retry, topic_15m_retry, topic_60m_retry i tak dalej. Z racji wydajnościowych wybraliśmy opcję numer dwa by miec jak najwięcej topiców i workerów, większe rozłożenie.

Podsumowując, biorąc pod uwagę to, co opisałeś, ja to bym zrobił w sposób następujący:

  1. Worker zjada taski wysyłki z danego topica.
  2. Te taski, które się udały - super, świetnie, oznaczmy je w db, że się udały.
  3. Te, które się nie udały, wrzucam na pierwszą kolejkę exponential backoff retry czyli np. retry_1m, które po 1 minucie spróbuje ponownie.
  4. Powtarzam kroki 2-3, przy czym przerzucam taski nieudane na kolejki o wykładniczo większym backoffie, aż do momentu kiedy przekroczę maksymalną liczbę retrajów. Liczbę retrajów możesz śledzić w bazie albo eg. redisie jak baza za wolna lub poprzez obserwacje który task znajduje się na jakiej kolejce - jak jest na 3, to wiadomo, że 1 i 2 się nie udały.
  5. Gdy przekroczę maksymalną liczbę retrajów do jakiegoś taska, wrzucam go na kolejkę retry_failed skąd zjada go ostatni worker, podejmujący jakąś akcję - eg. informacja admina czy coś, że wysyłka tego się nie udała mimo retrajów i raczej się nie uda w przyszłości -> nie ma sensu próbować dalej.
0

topic_5m_retry, topic_15m_retry, topic_60m_retry

Korzystałeś może z czegoś ala https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
czy konsumer gdy otrzymał message sam opóźniał jej przetwarzanie?

Gdy przekroczę maksymalną liczbę retrajów do jakiegoś taska, wrzucam go na kolejkę retry_failed skąd zjada go ostatni worker, podejmujący jakąś akcję - eg. informacja admina czy coś, że wysyłka tego się nie udała mimo retrajów i raczej się nie uda w przyszłości -> nie ma sensu próbować dalej.

Co jeśli przez 2 dni nie było sieci? Czy wysyłka po 2 dniach zostanie ponowiona tego co się nie udało?

0

A znacie powody dla których nie udaje się ich wysłać? Czy są to sytuacje losowe? Co z takimi wiadomościami powinno się stać z punktu widzenia biznesu?

0

Korzystałeś może z czegoś ala https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
czy konsumer gdy otrzymał message sam opóźniał jej przetwarzanie?

  1. Nie, do tej pory raczej pracowałem z kafką, przynajmniej jeśli chodzi o takie masowe, troszkę bardziej skomplikowane zastosowania. Prostych appek z celerką gdzie rabbit był jedynie jako najprostszy broker, nie liczę.
  2. Wygląda na to, że ten plugin zrobi ci robotę i w tym wypadku tak - będzie to zachowane by default. nie jestem jednak pewien, nigdy z niego nie korzystałe. Kkiedyś widziałem za to Leniwą implementacje. Można to zrobić w ten sposób, że w message doszywasz czas kiedy task powinien być wykonany, consumer sobie przelatuje przez wszystkie taski, patrzy na datę i jeśli jest za wcześnie, to ich nie commituje jako wykonanych/jeśli commituje to znowu wysyła na kolejke.

Co jeśli przez 2 dni nie było sieci? Czy wysyłka po 2 dniach zostanie ponowiona tego co się nie udało?

Jeśli często sytuacje gdzie nie macie sieci przez 2 dni to myślę, że to nie nad retrajem powinno się tu popracować a zmianą seweroni :D

A czy powtórzy? To zależy którą strategię zaimplementujecie - w przypadku powtarzania z pętlą to wiadomo - będzie powtarzać i po 2 dniach i po 10 dniach - dopóki nie każesz mu przestać.

W przpyadku ograniczenia z maksymalną liczbą powtórzeń, to zależy ile będzie tych powtórzeń w jakim odstępie czasowym - jeśli np. ostatni retraj wypada za 3 dni to tak, ale w innym wypadku pewnie nie.

Oczywiście jest też opcja, że dwa dni bez sieci to jakiś kluczowy requirement, którego się nie da zmienić i specjalnie isę przed tym zabezpieczysz i dopiszesz w consumerze kod który sprawdza czy jest sieć, jak nie, to sobie wchodzi w stan uśpienia i sprawdza znowu eg za minutę czy coś.

Poza tym dość specyficzna sytuacja - 2 dni bez sieci, dzieje się to u was?
Macie jakieś logi które by wam powiedziały dlaczego te niektóre faktury nie są wysyłane? Może warto by to zbadać?

Z emailem generalnie to sprawa jest prosta i o ile jest sieć a smtp działa, to wysyłka się nie powiedzie raczej bardzo rzadko: emaila nie ma(czyli powtórka nie ma sensu bo i tak się nie uda), serwer klienta/twój chwilowo nie działa (tu warto powtarzać) albo właśnie nie ma sieci. 1 pewnie dość częste, 2 i 3 raczej dość rzadkie.

0

Co z takimi wiadomościami powinno się stać z punktu widzenia biznesu?

Z punktu widzenia biznesu powinny trafić do klienta :)

Jeśli często sytuacje gdzie nie macie sieci przez 2 dni to myślę, że to nie nad retrajem powinno się tu popracować a zmianą seweroni

Podałem to jako przykład, nwm jak często ale są błędy "Request timeout" co jakiś czas.
Myślę, że niezłym rozwiązaniem byłoby stworzenie tabeli np. shipment, w której odkładałbym rzeczy do wysyłki.
Tabela miałaby kolumnę, flagę sent=true/false. Job codziennie pobierałby z tej tabeli wszystko co niewysłane i wrzucał na kolejke.
Serwis odpowiedzialny za wysyłke odbierałby z kolejki rzeczy do wysłania i po udanej wysyłce robił update do bazy sent=true.
Obyłoby się bez retry ewentualnie jeden po godzinie, byłoby w miarę defensywnie.

4

W jednym z projektów, widziałem takie rozwiązanie:

  • Consumer kolejki jawnie robił ACK lub NACK, w zależności od tego, czy się udało, czy nie.
  • Dla kolejki zdefiniowana była DLQ,na którą trafiały komunikaty po otrzymaniu NACK.
  • Na DLQ ustawiony był TTL i po jego upłynięciu, trafiało to na jej DLQ, które ustawione było na tę pierwszą kolejkę.

Tj. masz 2 kolejki:
emails
emails-dlq

Wiadomość trafia na
1. emails ->
2. Consumer.read()
2. a) Consumer.nack() (wysyłanie się nie powiodło) ->
2. b) Conumer.ack() (wysyłanie się powiodło) END
3. emails-dlq ->
4. upłynął ttl ->
5. emails -> przejdź do punktu 2.

Problem jest taki, że mogą Ci zostać w obiegu komunikaty, przesyłane między kolejkami emails i emails-dlq, które z jakiegoś powodu nigdy nie zostaną wysłane i nie dość, że będą Ci zapychać kolejkę, to jeszcze Consumer je będzie bez sensu przetwarzał.

W tym rozwiązaniu, twórcy rozwiązania dodali pole typu retriesCount w wiadomościach i na podstawie parametru maxRetriesCount zdejmowali wiadomość w Consumerze po osiągnięciu takiego retriesCount.

Można by to nieco zmodyfikować i consumer mógłby wrzucać sam na kolejkę DLQ emails-dlq, zamiast NACK z różnym TTL i dodać do tego exponential back off (jak pisał @grski), gdzie na podstawie konkretnego retriesCount (1,2,3...) dawać inne TTL per message, a nie na poziomie całej kolejki.

1

Do tego co napisał @catom, to TTL nie zawsze jest dobry, ale też nie zawsze zły ;-)

Dla jednego z systemów opartego o kolejki mieliśmy 4 kolejki na obsługę odrzutów od jednego z konsumentów:

  • R1 - odrzucone przez konsumenta w pierwszym przetwarzaniu
  • R2 - zbiera odrzuty poddawane recyclingowi z R1 (po upływie dnia)
  • R3 - poddawane recyclingowi z R2 (po upływie tygodnia)
  • R4 - zbiera odrzuty, które nie rokują - do analizy przez zespół od utrzymania i podjęcia dalszych akcji (np. naprawienia czegoś i wrzucenia do INPUT albo wytriggerowania jakichś akcji, np. zgłoszenie błędu do softu, konfiguracji etc.)

R1..R4 nie miały podłączonych konsumentów, ale były zdefiniowane joby, które batchowo przenosiły wiadomości do kolejki "wejściowej":

  • R1 --> INPUT
  • R2 --> INPUT
  • R3 --> INPUT

Dla R4 monitorowanie ilości elementów + podnoszenie alarmów -> triggerowanie manualnych akcje.

Takie podejście wynikało z wolumenu przetwarzanych komunikatów (miliony dziennie - połączenia telefoniczne) i chęci odciążenia systemu, tak by "problematyczne wiadomości" nie krążyły non-stop między kolejkami (z drobnymi przerwami). Jedna taka wiadomość często przerzucana między INPUT<-->REJECT potrafi skutecznie zeżreć CPU.

1 użytkowników online, w tym zalogowanych: 0, gości: 1