RabbitMQ zamiast własnej message queue w SQL, a gdy Rabbit padnie to dalej własna message queue w SQL?

1

Kilka miesięcy temu było tak:

  • Zauważam, że w dużej mierze korzystamy z SQLa do zadań, do których nadaje się kolejka komunikatów (i walczymy z deadlockami, gdy próbujemy robić blokowanie wierszy tak, żeby ta "kolejka" SQLowa działała, albo olewamy blokowanie wierszy i dajemy komentarz, że // UWAGA działa bo akurat jest tylko jeden konsumer, TODO wrócić do tego gdy będziemy chcieli uwspółbieżnić pracę tutaj
  • M.in. z tego powodu proponuję message queue i już mam pozwolenie na dodanie RabbitMQ do zależności (szefostwo bardzo krzywym okiem patrzy na wszelkie "niekonieczne" zależności i w naszej sytuacji myślę, że mogą mieć rację, choć to inny temat)
  • Czytam w mailu:

Jeśli RabbitMQ padnie, trzeba zapisywać zadania do odp. tabeli SQL i gdy tylko Rabbit wstanie / zostanie podniesiony, pchnąć je do Rabbita

Myślę sobie WTF, mamy rabbita po to, by nie robić pseudo-kolejki w SQL, a tu się okazuje że nawet z Rabbitem musi być pseudo-kolejka w SQL, po co ten Rabbit?

Próbuję odpowiedzieć:

Jeśli Rabbit padnie, to nie można po prostu zwrócić HTTP 500?

Nie można:

Nie po to piszemy nasze rozwiązanie, by zmniejszyć niezawodność całego systemu. Zwracanie 500 dlatego, że zależymy od Rabbita, to strzał w stopę!

Chyba jednak nasza aplikacja ma większe szanse na zaliczenie wywrotki niż Rabbit, pomimo odgórnego zakazu... Próbuję znów:

Realistycznie, jaka jest szansa, że Rabbit padnie? To jest chyba stabilne, dobrze przetestowane oprogramowanie, a jeśli nawet padnie, to winno być automatycznie podniesione przez środowisko?

Niestety, odpowiedź jest niewzruszona:

Rabbit jest aplikacją. Każda aplikacja może paść. Rabbit może paść.

W tym momencie poddałem się i zrezygnowałem z Rabbita. Z mojej perspektywy nie spełniałby on swojego celu, tj. usunięcia pokracznych, chałupniczych pseudo-kolejek implementowanych czy to jako tabele SQL, czy to za pomocą rozmaitych dirty-bitów dokładanych do różnych pierdółek.

Czegoś tu mocno nie rozumiem. Czy mogę zapytać:

  • Czy istotnie zakłada się, że każda zależność może paść i w związku z tym by zapewnić działanie całego systemu wymaga się, by ręcznie reimplementować różne zależności, tak by można było tych reimplementacji użyć gdy "preferowana" zależność padnie?
  • Jeśli nie, to co innego się robi w takich sytuacjach?
4

No ale to jest uczciwe pytanie: co w momencie, jak Rabbit padnie? Jaki jest impakt na użytkownika? Czy jest ryzyko utraty danych? Jaki failover? To jest ryzyko, które należy zmitygować.

Przecież nie możesz zakładać, że Rabbit nigdy się nie wywali, nie padnie broker, worker, nie stanie się coś z siecią etc.

3

Ehh, przecież baza też może paść.

4

Baza to też aplikacja, co robi twoja aplikacja jak padnie baza? Jeśli chodzi o HA to masz clustrowanie, które działa dużo lepiej (w kontekście messagingu), niż klastrowanie standardowej bazy sqlowej. Jeśli zaś rabbit padnie to jedyne co można robić to alternatywne źródło (ale ten sam problem jest w normalnych baza) i/lub wysyłanie w kółko aż się uda modląc się, że rabbit powstanie zanim ktoś zabije naszą aplikację

Jak na moje to trafiłeś na janusza, który ma w głowie baza SQL == niezawodność. Jak będziesz pokazywał jakiekolwiek wahania czy ustępstwa to przegrasz dyskusję.
Inny argument: rabbitmq to też baza danych, tylko dostosowana do innych celów. Tak samo jak w bazie istnieją mechanizmy zapewniające, że operacja się wykonała poprawnie (publisher confirm i ACK/NACK). I tak samo jak bazy danych rabbit nigdy się nie wywala

1

Może koledze architektowi chodziło o transactional outbox? https://microservices.io/patterns/data/transactional-outbox.html

2

Jeśli RabbitMQ padnie

No baza też może paść jak to pisali wyżej. Tylko ja nie rozumiem jednego. Powiedzmy, że mam jakiś serwis do pchania powiadomień, które potem są odbierane z kolejki i wysyłane np. jako email, sms.

Producent jeżeli widzi, że Rabbit odmawia współpracy powinien buforować te nie przekazane wiadomości do momentu, kiedy Rabbit zechce współpracować.

Jeżeli to jest usługa na zewnątrz dla klientów, to też robi się tak, że jak dostaje co inego niż 200, to buforuje komunikaty do czasu kiedy usługa wstanie.

Kolejna kwestia, to że w dobie mikroserwisów jest monitoring, który reaguje na to kiedy dana usługa nie odpowiada.

3

Użyj Kafki, kafka ma klaster więc nie tak łatwo ją położyć. Także będziesz miał argument za.

Co do padania różnych rzeczy zapytaj co się stanie jak SQL padnie :D

Widać że opór jest z góry. Tutaj żadne racjonalne argusy nie pomogą, trzeba lobbować i uprawiać politykę...

3

Ja bym zrobił zapisywanie powiadomień o tym co powinno zostać wysłane do dedykowanej tabeli w SQL (coś w rodzaju listy zadań) i do tego napisałbym dedykowanego joba/background worker, który chodziłby sobie niezależnie od aplikacji i monitorował stan tych zadań. Jeżeli jakieś zadanie będzie miało status Queued to go pobiera i wysyła wiadomość na kolejkę (RabbitMQ). Gdy operacja się powiedzie zmienia status na Processed i przetwarza kolejne z listy. Jeżeli wystąpi błąd bo sieć albo kolejka leży, to ten worker powinien mieć mechanizm ponawiania wysłania wiadomości (Retry Pattern) oraz przerwania tej operacji jeżeli się okaże że problem jest bardziej złożony (Circuit Breaker Pattern). Wtedy może ustawić status zadania na Error z informacją o błędzie i po przywróceniu działania kolejki/sieci spróbować ponowić wysłać te wiadomości ze statusem Error.

To tak na szybko.

0
YetAnohterone napisał(a):

Kilka miesięcy temu było tak:

  • Zauważam, że w dużej mierze korzystamy z SQLa do zadań, do których nadaje się kolejka komunikatów

Baza danych to nie kolejka, kolejka, to nie baza danych. Niby można kosić trawnik nożyczkami do włosów, a włosy strzyc kosiarką, ale nie są to rozwiązania optymalne.

Jeśli RabbitMQ padnie, trzeba zapisywać zadania do odp. tabeli SQL i gdy tylko Rabbit wstanie / zostanie podniesiony, pchnąć je do Rabbita

Nie, trzeba zrobić tak, żeby te kolejki nie padały. Nie wiem jak można to zrobić w RabbitMQ, ale na 100% istnieją kolejki wspierające DR/HA. Dla SQL obsługa tabeli, do której nieustannie ktoś, coś wrzuca, później zmienia, na koniec usuwa ten rekord to mordęga, nieustanne naparzanie po indeksach, rosnący rozmiar bazy, konieczność systematycznego czyszczenia tej tabeli.

Myślę sobie WTF, mamy rabbita po to, by nie robić pseudo-kolejki w SQL, a tu się okazuje że nawet z Rabbitem musi być pseudo-kolejka w SQL, po co ten Rabbit?

Śmiem twierdzić, ze w większości dzisiaj działających systemów wcale nie trzeba SQL, baz relacyjnych używa się bo:

  • nasi dziadowie tak robili i ich dziadowie
  • nie mamy pojęcia co ma robić ten system w przyszłości, a pomimo wszelkich możliwych wad, SQL ma jednak tę zaletę, że wciśnie się w niego wszystko, wyciągnie co potrzeba. Będzie wolno, ale będzie działać (czasami).

Próbuję odpowiedzieć:

Jeśli Rabbit padnie, to nie można po prostu zwrócić HTTP 500?

Nie można:

Nie po to piszemy nasze rozwiązanie, by zmniejszyć niezawodność całego systemu. Zwracanie 500 dlatego, że zależymy od Rabbita, to strzał w stopę!

Zrobić tak, żeby ten rabbit nie padł. Baza SQL też może paść i pada. Oba rozwiązania da się uodpornić na padanie (DR), ale akurat kolejki łatwiej. Generalnie wystarczy mieć spięty drugi serwer kolejek w innej lokalizacji (mirroring), wrzucasz dane do obu, dodatkowo same między sobą gadają, jeżeli wpada jakaś wiadomość, to ląduje w obu lokalizacjach, następnie serwery próbują ją przesłać do siebie wzajemnie i odrzucają, bo id wiadomości (nadane zewnętrznie) jest identyczne.

Realistycznie, jaka jest szansa, że Rabbit padnie? To jest chyba stabilne, dobrze przetestowane oprogramowanie, a jeśli nawet padnie, to winno być automatycznie podniesione przez środowisko?

Może paść, pada, jak nie rabbit, to maszyna na której stoi, albo atom pierdyknie w datacenter.

Niestety, odpowiedź jest niewzruszona:

Rabbit jest aplikacją. Każda aplikacja może paść. Rabbit może paść.

W tym momencie poddałem się i zrezygnowałem z Rabbita. Z mojej perspektywy nie spełniałby on swojego celu, tj. usunięcia pokracznych, chałupniczych pseudo-kolejek implementowanych czy to jako tabele SQL, czy to za pomocą rozmaitych dirty-bitów dokładanych do różnych pierdółek.

I niesłusznie. Z punktu widzenia niezawodności, wyobraź sobie, że wszystkie żądania zmieniające stan danych wpadają w formie komunikatów do kolejki. Sprawa jest banalna, bo ostatecznie masz wystawiony jakiś endpoint, tłumaczysz go na komunikat i wrzucasz do kolejki. Jeżeli zabezpieczysz (np. przez zwielokrotnienie) ten kawałek oprogramowania, który przyjmuje żądanie z zewnątrz i przerabia je na komunikat i zabezpieczysz przed padnięciem kolejkę, to jesteś pewien, że dane nie zginą (wyłączając niewielkie prawdopodobieństwo, że atom pierdyknie jednocześnie we wszystkie datacenter). Jeżeli padnie ci baza, to za chwilę ktoś ją podniesie, komunikaty zostaną przetworzone. Nastąpi to za godzinę, albo rok, ale po podniesieniu się wszystkich elementów systemu, system sam, bez ingerencji z zewnątrz doprowadzi stan danych do właściwego. Wadą jest to, że nie wiesz kiedy to nastąpi (eventual consistency).

  • Czy istotnie zakłada się, że każda zależność może paść

Tak

i w związku z tym by zapewnić działanie całego systemu wymaga się, by ręcznie reimplementować różne zależności, tak by można było tych reimplementacji użyć gdy "preferowana" zależność padnie?

Nie

  • Jeśli nie, to co innego się robi w takich sytuacjach?

Redukujesz do 0 liczbę SPOF, tak jak ci pisałem wyżej - można sprawić, że jedyne krytyczne zależności systemu, to jakiś API gateway, mały kawałek tłumaczący requesta na komunikat i serwer kolejek sam w sobie.
Zajmujesz się tym co zostało, w zależności od rodzaju ryzyka, które chcesz zminimalizować w tym przypadku można np.

  • Umieścić API gatewaye + te "tłumacze" w 2..n różnych datacenter i na poziomie DNS kierować ruch do losowego
  • Wysyłać wiadomości jednocześnie do wszystkich MQ, dodatkowo spiętych w klaster
    Jak padnie ci 1 z tych datacenter (albo nawet niech będzie ten Rabbit), to w najgorszym przypadku 1/n żądań skończy się błędem, ale dane nie zostaną utracone.

Do zera w ten sposób nie zejdziesz, bo jeżeli na poziomie jednego datacenter masz 99% niezawodności, to dodanie drugiego da ci wzrost do 99.99% (prawdopodobieństwo, że oba, całkowicie niezależne od siebie elementy walną w tym samym momencie). Czyli na język biznesu - statystycznie system nie będzie działać przez 53 minuty rocznie.
Natomiast twój szef ma trochę racji w tym, ze dodawanie elementów do systemu (mogą walnąć, wiadomo), podnosi zawodność całego systemu. Ale dzieje się tak, kiedy prawidłowe działanie systemu zależy od obu. Czyli jak masz w 99% niezawodne DB i MQ, ale system wywali się zawsze jeżeli dowolne z nich przestanie działać, to prawdopodobieństwo, że system przestanie działać spada ci do 98.01% Czyli cała zabawa w tym, żeby mnożyć prawdopodobieństwa niedziałania, a nie prawdopodobieństwa działania elementów systemu.
Niestety, patrząc na argumenty, jakimi posługuje się twój szef i opisane w innym wątku sposoby radzenia sobie z problemami w kodzie, nie wróżę tak optymistycznego scenariusza.

2

Tak jak @0xmarcin napisał, jest kafka która jest distributed log, dane są przechowywane na dysku przez okreslony czas, defaultowo 5 dni + jeszcze masz replikacje.

0

logi poza aplikacje aby dało się ewentualnie odzyskać + np. przyjazny format

dump do jsona/sqlite na dysk w razie niepowodzenia zapisu do redisa i gdy wstanie, to próba ponownego wrzucenia przez daną instancje?

1

"walczymy z deadlockami, gdy próbujemy robić blokowanie wierszy tak, żeby ta "kolejka" SQLowa działała"

Przewrotnie zapytam, w co bardziej nie umiecie, w bazy czy w "produkty kolejkowe"? Obstawiam, że kolejnym postem może być coś w stylu "Postawiliśmy RabbitMQ, jak zrobić by ta sama wiadomość nie była przetwarzana przez 2 consumerów? Obecnie mamy //TODO działa tylko na 1 consumerze, poprawić jak będziemy chcieli >1 "

Sporo baz potrafi "SELECT FOR UPDATE <SKIP LOCKED/NOWAIT/WAIT x>" i to podstawa do implementacji kolejki. Może warto naprawić problem z deadlockami zamiast dorzucać nowy komponent, który gdzieś trzeba uruchomić (dodatkowe zasoby; a jak w klastrze, to więcej zasobów), monitorować, rozwiązywać problemy z nim związane.

Czy obecny stan Waszej aplikacji umożliwia przepięcie implementacji kolejki z "sql-self-made" na RabbitMQ/Kafkę/... ? Jeśli nie, to może warto się przygotować do takiej akcji przepinania implementacji kolejki?

5

Wszystko może paść, zarówno baza danych jak i kolejka. Naszym zadaniem jest tak zaprojektować system by sam się uleczył po takim wydarzeniu a jego stan odzyskał spójność. RabitMQ i tak nie obsługuje transakcyjnego umieszczania wiadomości na kolejce, jedynie potwierdzenia. Więc jak nie chcemy gubić wiadomości i osiągnąć jak największą przepustowość to robi się to dokładnie tak jak opisał @markone_dev/@Charles_Ray, za pomocą outbox box pattern, czyli pierw zapisujemy wiadomość do pomocniczej tabeli w bazie, żeby mieć pewność że każda wiadomość zostanie wysłana przynajmniej raz.

2
  1. Baza też może paść, wiec pytanie czy np. teraz juz macie tam failover który trzyma dane w pamięci czekając aż wstanie? ;)
  2. Nie widzę problemu z takim failoverem jaki jest proponowany, bo będzie trywialny i dużo łatwiejszy niż to co teraz macie. Weź pod uwagę że ten failover z pisaniem do bazy kiedy kolejka leży to jest append only więc cały problem z lockami w ogóle odchodzi w niepamięć bo robisz tam tylko i wyłącznie inserty. Jedyny trik jaki widzę to kwestia kolejności, tzn jak kolejka w stanie trzeba najpierw wcisnąć do niej co tam leży w bazie a dopiero potem wrzucać "aktualne" dane ale to w zasadzie tyle.
  3. Moim zdaniem i tak powinien tam być jeszcze jeden failover z jakąś kolejką in-memory. Najprościej będzie jak nowe eventy lecą do kolejki w pamięci, z której inny wątek wyciąga i próbuje wrzucać do Rabbita a jak się nie powiedzie to do bazy, a jak sie nie powiedzie to nie usuwa elementu z pamięci tylko czeka na kolejny cykl.
0

Czym się różni Outbox pattern od Sagi? myślałem że Saga to generalnie zbiór mechanizmów do tej klasy problemów, a zatem outbox pattern byłby po prostu konkretną implementacją?

Jak taki sposób przetwarzania nazwać? Saga4poor?

6

Różnica jest w poziomie abstrakcji na którym operują. Outbox jest bardzo nisko poziomowym wzorcem, którego jedyną odpowiedzialność jest upewnienie się że wiadomość została przynajmniej raz wysłana. Natomiast Saga operuje na wyższym poziomie, na poziomie procesu biznesowego, jej odpowiedzialnością jest koordynowanie przebiegu tego procesu.

2

Czytam sobie jeszcze raz ten wątek i coraz bardziej dochodzę do wniosku, że skupia się on na niewłaściwym problemie. Żeby zastanawiać się, czy użyć rozwiązania X, Y, X+Y, czy może Z warto zastanowić się najpierw co dokładnie chcemy rozwiązać i przed jakimi ryzykami ochronić.
Tutaj (i w poprzednim wątku OP'a) wciąż mowa o "niezawodności systemu" ale nie ma sprecyzowanego celu biznesowego dla tej niezawodności:

  • czy system ma po prostu nie gubić danych (np. system bankowy)
  • czy system ma być zawsze dostępny i co to "zawsze" oznacza (kontroler Air Bag'ów)
  • czy najważniejszym wymaganiem biznesowym jest atomiczność przetwarzania i pewność, że albo udadzą się operacje A i B, albo żadna z nich (znowu bank)
  • jakie znaczenie ma czas przetwarzania tych decyzji (np. komunikator internetowy, serwer gier on-line)

Jak już wiadomo co ma jakie skutki, to można zastanawiać się nad tym, co faktycznie jest ryzykiem i szukać sposobu ograniczenia tego ryzyka. Na razie wymagania wydają się być zdefiniowane na poziomie "ma być dobrze".

0

Inni dostarczyli już argumentów technicznych dlaczego mimo wszystko lepiej użyć rabbit'a zamiast sqla w kontekście bazy danych - podsumowując zapewnia większy uptime systemu, bo sprawność systemu to trochę bardziej skomplikowana sprawa niż x / {liczba_zależności}.

Jak już udowodnisz podobną sprawność systemów, to możesz dowalić argumentem, który zazwyczaj bardzo dobrze trafia do szefów: pienionszkami. Sam napisałeś, że twoim celem jest to, żeby pisać kod łatwiej, nie męczyć się z utrzymywaniem półrozwiązań - czyli klepać szybciej! Implementowanie kolejki jest zadaniem nietrywialnym, i teoretycznie musicie zainwestować w to tyle samo czasu, co twórcy rabbita. Możecie to zrobić, owszem, szefu przecież za to płaci. Albo możecie użyć gotowego (nawet lepszego) rozwiązania, a zyskany czas zainwestować jedyną rzecz, która się liczy dla biznesu: FICZERY.

1

NServiceBus obsługuję transport przez SQL: https://docs.particular.net/transports/sql/
Ale to tylko jeśli jesteś w stosie .NET i MSSQL
Może to będzie argument w dyskusji? :) Chociaż wiem z doświadczenia, że "my to ogarniemy po swojemu i będziemy wiedzieli jak działa" jest czasem nie do przejścia.

5
Grzegorz Kotfis napisał(a):

Chociaż wiem z doświadczenia, że "my to ogarniemy po swojemu i będziemy wiedzieli jak działa" jest czasem nie do przejścia.

Najczęściej dość szybko migruje do stanu "zrobiliśmy po swojemu i nie wiemy dlaczego nie działa" ;)

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