Interfejs API Gateway (REST) ​​+ Mikrousług sterowane zdarzeniami

16

Mam kilka mikrousług, których funkcjonalność udostępniam za pośrednictwem interfejsu API REST zgodnie ze wzorcem bramy API. Ponieważ te mikrousług są aplikacjami Spring Boot, używam Spring AMQP, aby osiągnąć synchroniczną komunikację w stylu RPC między tymi mikrousługami. Jak dotąd wszystko szło gładko. Im więcej jednak czytam o architekturach mikrousług opartych na zdarzeniach i patrzę na projekty, takie jak Spring Cloud Stream, tym bardziej jestem przekonany, że mogę robić rzeczy w niewłaściwy sposób z RPC, podejście synchroniczne (szczególnie dlatego, że będę musiał to skalować w celu odpowiedzi na setki lub tysiące żądań na sekundę z aplikacji klienckich).

Rozumiem sens architektury opartej na zdarzeniach. Nie do końca rozumiem, jak właściwie użyć takiego wzorca, siedząc za modelem (REST), który oczekuje odpowiedzi na każde żądanie. Na przykład, jeśli mam bramę API jako mikrousługę i inną mikrousługę, która przechowuje użytkowników i zarządza nimi, w jaki sposób mogę modelować coś takiego, jak GET /users/1w przypadku zdarzenia?

Tony E. Stark
źródło

Odpowiedzi:

9

Powtarzaj za mną:

REST i zdarzenia asynchroniczne nie są alternatywami. Są całkowicie ortogonalne.

Możesz mieć jedno, drugie, jedno i drugie lub jedno i drugie. Są to całkowicie różne narzędzia dla zupełnie różnych domen problemowych. W rzeczywistości komunikacja typu żądanie-odpowiedź ogólnego przeznaczenia jest absolutnie w stanie być asynchroniczna, sterowana zdarzeniami i odporna na uszkodzenia .


Jako trywialny przykład protokół AMQP wysyła wiadomości przez połączenie TCP. W TCP każdy pakiet musi być potwierdzony przez odbiornik . Jeśli nadawca pakietu nie otrzyma potwierdzenia ACK dla tego pakietu, wysyła go ponownie do momentu potwierdzenia lub do momentu, gdy warstwa aplikacji „zrezygnuje” i porzuci połączenie. Jest to oczywiście nietolerancyjny model żądanie-odpowiedź, ponieważ każde „żądanie wysłania pakietu” musi mieć towarzyszącą „odpowiedź na potwierdzenie pakietu”, a brak odpowiedzi skutkuje niepowodzeniem całego połączenia. Jednak AMQP, znormalizowany i powszechnie przyjęty protokół asynchronicznego przesyłania komunikatów odpornych na uszkodzenia, jest komunikowany przez TCP! Co daje?

Podstawową koncepcją w tym przypadku jest to, że skalowalne luźno powiązane, odporne na awarie wiadomości są definiowane przez to, jakie wiadomości wysyłasz , a nie jak je wysyłasz . Innymi słowy, luźne połączenie jest definiowane na warstwie aplikacyjnej .

Spójrzmy na dwie strony komunikujące się bezpośrednio z RESTful HTTP lub pośrednio z brokerem komunikatów AMQP. Załóżmy, że strona A chce przesłać obraz JPEG do strony B, która wyostrzy, skompresuje lub w inny sposób poprawi obraz. Grupa A nie potrzebuje natychmiast przetworzonego obrazu, ale wymaga odwołania do niego w celu przyszłego wykorzystania i pobrania. Oto jeden ze sposobów, który może iść w REST:

  • Strona A wysyła POSTkomunikat żądania HTTP do Strony B za pomocąContent-Type: image/jpeg
  • Strona B przetwarza obraz (przez długi czas, jeśli jest duży), podczas gdy Strona A czeka, prawdopodobnie robiąc inne rzeczy
  • Strona B wysyła 201 Createdkomunikat odpowiedzi HTTP do Strony A z Content-Location: <url>nagłówkiem, który prowadzi do przetworzonego obrazu
  • Strona A uważa swoją pracę za wykonaną, ponieważ ma teraz odniesienie do przetworzonego obrazu
  • W przyszłości, gdy strona A potrzebuje przetworzonego obrazu, Pobiera go za pomocą linku z wcześniejszego Content-Locationnagłówka

201 CreatedKod odpowiedzi mówi klientowi, że nie tylko ich wniosek udany, stworzyła również nowy zasób. W odpowiedzi 201 Content-Locationnagłówek jest linkiem do utworzonego zasobu. Jest to określone w sekcjach 6.3.2 i 3.1.4.2 RFC 7231.

Zobaczmy teraz, jak ta interakcja działa w oparciu o hipotetyczny protokół RPC na platformie AMQP:

  • Strona A wysyła brokerowi wiadomości AMQP (nazywając go Messengerem) wiadomość zawierającą obraz i instrukcje, aby skierować go do Strony B w celu przetworzenia, a następnie odpowiedzieć stronie A z jakimś adresem dla obrazu
  • Strona A czeka, prawdopodobnie robiąc inne rzeczy
  • Messenger wysyła oryginalną wiadomość Strony A do Strony B.
  • Strona B przetwarza wiadomość
  • Strona B wysyła komunikatorowi wiadomość zawierającą adres przetworzonego obrazu i instrukcje, aby skierować tę wiadomość do strony A.
  • Messenger wysyła stronie A wiadomość ze strony B zawierającą adres przetworzonego obrazu
  • Strona A uważa swoją pracę za wykonaną, ponieważ ma teraz odniesienie do przetworzonego obrazu
  • Kiedyś w przyszłości, gdy strona A potrzebuje obrazu, pobiera obraz za pomocą adresu (prawdopodobnie wysyłając wiadomości do innej osoby)

Czy widzisz tutaj problem? W obu przypadkach, Partia A nie może uzyskać adresu obrazu aż po Partia B przetwarza obraz . Jednak strona A nie potrzebuje obrazu od razu i, zgodnie z wszelkimi prawami, nie obchodzi go mniej, jeśli przetwarzanie zostanie zakończone!

Możemy to dość łatwo naprawić w przypadku AMQP, każąc Stronie B powiedzieć A, że B zaakceptował obraz do przetworzenia, podając A adres, pod którym będzie znajdować się obraz po zakończeniu przetwarzania. Następnie Strona B może wysłać A wiadomość w przyszłości, wskazującą, że przetwarzanie obrazu zostało zakończone. Wiadomości AMQP na ratunek!

Z wyjątkiem zgadnij, co: możesz osiągnąć to samo dzięki REST . W przykładzie AMQP zmieniliśmy komunikat „oto przetworzony obraz” na „obraz jest przetwarzany, możesz go zdobyć później”. Aby to zrobić w RESTful HTTP, użyjemy 202 Acceptedkodu i Content-Locationponownie:

  • Strona A wysyła POSTwiadomość HTTP do Strony B za pomocąContent-Type: image/jpeg
  • Strona B natychmiast odsyła 202 Acceptedodpowiedź, która zawiera rodzaj „operacji asynchronicznej”, która opisuje, czy przetwarzanie zostało zakończone i gdzie obraz będzie dostępny po zakończeniu przetwarzania. Uwzględniono również Content-Location: <link>nagłówek, który w 202 Acceptedodpowiedzi stanowi łącze do zasobu reprezentowanego przez dowolny element odpowiedzi. W tym przypadku oznacza to, że jest to link do naszej operacji asynchronicznej!
  • Strona A uważa swoją pracę za wykonaną, ponieważ ma teraz odniesienie do przetworzonego obrazu
  • W przyszłości, gdy strona A potrzebuje przetworzonego obrazu, najpierw Pobiera zasób operacji asynchronicznej połączony z Content-Locationnagłówkiem, aby ustalić, czy przetwarzanie zostało zakończone. Jeśli tak, Partia A następnie używa łącza w samej operacji asynchronicznej, aby uzyskać przetworzony obraz.

Jedyna różnica polega na tym, że w modelu AMQP strona B informuje stronę A, kiedy przetwarzanie obrazu jest zakończone. Ale w modelu REST strona A sprawdza, czy przetwarzanie jest wykonywane tuż przed faktycznym potrzebowaniem obrazu. Te podejścia są równoważnie skalowalne . W miarę powiększania się systemu liczba wiadomości wysyłanych zarówno w asynchronicznej strategii AMQP, jak i asynchronicznej strategii REST rośnie wraz z równoważną złożonością asymptotyczną. Jedyną różnicą jest to, że klient wysyła dodatkową wiadomość zamiast serwera.

Ale podejście REST ma jeszcze kilka sztuczek: dynamiczne wykrywanie i negocjowanie protokołu . Zastanów się, jak zaczęły się interakcje synchronizacji i asynchronizacji REST. Strona A wysłała dokładnie tę samą prośbę do Strony B, przy czym jedyną różnicą jest szczególny rodzaj komunikatu o sukcesie, na który Strona B odpowiedziała. Co jeśli Partia A chciałaby wybrać, czy przetwarzanie obrazu będzie synchroniczne czy asynchroniczne? Co się stanie, jeśli Partia A nie wie, czy Partia B może nawet przetwarzać asynchronicznie?

Cóż, HTTP faktycznie ma do tego już ustandaryzowany protokół! To się nazywa Preferencje HTTP, w szczególności respond-asyncpreferencja RFC 7240, sekcja 4.1. Jeśli strona A chce odpowiedzi asynchronicznej, zawiera Prefer: respond-asyncnagłówek z początkowym żądaniem POST. Jeśli Strona B postanowi uszanować tę prośbę, odsyła 202 Acceptedodpowiedź zawierającą Preference-Applied: respond-async. W przeciwnym razie Party B po prostu ignoruje Prefernagłówek i odsyła, 201 Createdjak zwykle.

Pozwala to Stronie A negocjować z serwerem, dynamicznie dostosowując się do dowolnej implementacji przetwarzania obrazu, z którą się rozmawia. Co więcej, użycie jawnych linków oznacza, że ​​strona A nie musi wiedzieć o żadnych podmiotach innych niż B: brak brokera komunikatów AMQP, brak tajemniczej strony C, która wie, jak faktycznie przekształcić adres obrazu w dane obrazu, nie ma drugiej B-Async imprezuj, jeśli trzeba wysyłać żądania synchroniczne i asynchroniczne itp. Po prostu opisuje, czego potrzebuje, co opcjonalnie by chciał, a następnie reaguje na kody stanu, treść odpowiedzi i łącza. DodaćCache-Control nagłówki w celu uzyskania wyraźnych instrukcji, kiedy zachować lokalne kopie danych, a teraz serwery mogą negocjować z klientami, których zasobów klienci mogą przechowywać lokalne (a nawet offline!) Kopie. W ten sposób budujesz luźno sprzężone odporne na uszkodzenia mikrousługi w REST.

Jacek
źródło
1

To, czy musisz kierować się wyłącznie zdarzeniami, zależy oczywiście od konkretnego scenariusza. Zakładając, że naprawdę musisz, możesz rozwiązać problem przez:

Przechowywanie lokalnej kopii danych tylko do odczytu poprzez nasłuchiwanie różnych zdarzeń i przechwytywanie informacji w ich ładunkach. Chociaż zapewnia to szybkie (er) odczyty tych danych, zapisanych w formie odpowiedniej dla tej konkretnej aplikacji, oznacza to również, że Twoje dane będą ostatecznie spójne we wszystkich usługach.

Modelować GET /users/1z tym podejściem, można nasłuchiwać UserCreatedi UserUpdatedwydarzeń, a przechowywanie przydatnych podzbiór danych użytkowników w serwisie. Gdy następnie potrzebujesz uzyskać informacje o użytkownikach, możesz po prostu przesłać zapytanie do lokalnego magazynu danych.

Przez chwilę załóżmy, że usługa udostępniająca /users/punkt końcowy nie publikuje żadnych zdarzeń. W tym przypadku można osiągnąć podobną rzecz, po prostu buforując odpowiedzi na złożone żądania HTTP, co neguje potrzebę wykonania więcej niż 1 żądania HTTP na użytkownika w określonym przedziale czasowym.

Andy Hunt
źródło
Rozumiem. Ale co z obsługą błędów (i raportowaniem) klientom w tym scenariuszu?
Tony E. Stark
Mam na myśli, w jaki sposób mogę zgłaszać klientom REST błędy występujące podczas obsługi UserCreatedzdarzenia (na przykład zduplikowana nazwa użytkownika lub wiadomość e-mail lub awaria bazy danych).
Tony E. Stark
To zależy od tego, gdzie wykonujesz akcję. Jeśli jesteś w systemie użytkownika, możesz przeprowadzić całą weryfikację, zapisać tam magazyn danych, a następnie opublikować zdarzenie. W przeciwnym razie uważam za całkowicie akceptowalne wykonanie standardowego żądania HTTP do /users/punktu końcowego i pozwolić temu systemowi opublikować swoje zdarzenie, jeśli się powiedzie, i odpowiedzieć na żądanie nową jednostką
Andy Hunt
0

W przypadku systemu opartego na zdarzeniach asynchroniczne aspekty zwykle wchodzą w grę, gdy zmieni się coś, co reprezentuje stan, być może baza danych lub zagregowany widok niektórych danych. Korzystając z Twojego przykładu, wywołanie GET / api / users może po prostu zwrócić odpowiedź z usługi, która ma aktualną reprezentację listy użytkowników w systemie. W innym scenariuszu żądanie GET / api / users może spowodować, że usługa użyje strumienia zdarzeń od ostatniej migawki użytkowników, aby zbudować kolejną migawkę i po prostu zwrócić wyniki. System sterowany zdarzeniami niekoniecznie jest czysto asynchroniczny od żądania do odpowiedzi, ale zwykle znajduje się na poziomie, na którym usługi muszą wchodzić w interakcje z innymi usługami. Często nie ma sensu asynchroniczne zwracanie żądania GET, więc możesz po prostu zwrócić odpowiedź usługi,

Lloyd Moore
źródło