Seedowanie baz danych mikrousług

10

Biorąc pod uwagę usługę A (CMS), która kontroluje model (Produkt, załóżmy, że jedyne pola, które ma, to identyfikator, tytuł, cena) oraz usługi B (Wysyłka) i C (E-mail), które muszą wyświetlać dany model, jakie powinno być podejście zsynchronizować informacje o danym modelu w tych usługach w podejściu do pozyskiwania zdarzeń? Załóżmy, że katalog produktów rzadko się zmienia (ale się zmienia) i że istnieją administratorzy, którzy bardzo często uzyskują dostęp do danych przesyłek i wiadomości e-mail (przykładowe funkcje to: B: display titles of products the order containedi C display content of email about shipping that is going to be sent:). Każda z usług ma własną bazę danych.

Rozwiązanie 1

Wyślij wszystkie wymagane informacje o produkcie w ramach wydarzenia - oznacza to następującą strukturę dla order_placed:

{
    order_id: [guid],
    product: {
        id: [guid],
        title: 'Foo',
        price: 1000
    }
}

W serwisie B i C informacje o produkcie są przechowywane w productatrybucie JSON w orderstabeli

Jako taki, do wyświetlenia niezbędnych informacji wykorzystywane są tylko dane pobrane ze zdarzenia

Problemy : w zależności od tego, jakie inne informacje muszą być prezentowane w B i C, ilość danych w przypadku może wzrosnąć. B i C mogą nie wymagać tych samych informacji o Produkcie, ale wydarzenie będzie musiało zawierać oba (chyba że podzielimy zdarzenia na dwa). Jeśli podane dane nie są obecne w danym zdarzeniu, kod nie może ich użyć - jeśli dodamy opcję koloru do danego Produktu, dla istniejących zamówień w B i C, dany produkt będzie bezbarwny, chyba że zaktualizujemy zdarzenia, a następnie uruchomimy je ponownie .

Rozwiązanie 2

Wyślij tylko identyfikator produktu w ramach wydarzenia - oznacza to następującą strukturę dla order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

W usługach B i C informacje o produkcie są przechowywane w product_idatrybucie w orderstabeli

Informacje o produkcie są pobierane przez usługi B i C, gdy jest to wymagane przez wykonanie wywołania interfejsu API do A/product/[guid]punktu końcowego

Problemy : powoduje to, że B i C zależą od A (przez cały czas). Jeśli schemat zmian produktu w A, należy wprowadzić zmiany we wszystkich usługach, które od nich zależą (nagle)

Rozwiązanie 3

Wyślij tylko identyfikator produktu w ramach zdarzenia - oznacza to następującą strukturę dla order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

W usługach B i C informacje o produkcie są przechowywane w productstabeli; wciąż jest product_idna ordersstole, ale istnieje replikacja productsdanych między A, B i C; B i C mogą zawierać inne informacje o produkcie niż A

Informacje o produkcie są inicjowane podczas tworzenia usług B i C i są aktualizowane za każdym razem, gdy informacje o Produktach ulegają zmianie przez wywołanie A/productpunktu końcowego (który wyświetla wymagane informacje o wszystkich produktach) lub przez bezpośredni dostęp DB do A i skopiowanie niezbędnych informacji o produkcie wymaganych dla danego produktu usługa.

Problemy : powoduje to, że B i C zależą od A (podczas wysiewu). Jeśli schemat zmian produktu w A, należy wprowadzić zmiany we wszystkich usługach, które od nich zależą (podczas inicjowania)


Z mojego zrozumienia, poprawnym podejściem byłoby iść z rozwiązaniem 1 i albo aktualizować historię zdarzeń zgodnie z pewną logiką (jeśli katalog produktów nie zmienił się i chcemy dodać wyświetlany kolor, możemy bezpiecznie zaktualizować historię, aby uzyskać aktualny stan Produktów i uzupełnij brakujące dane w ramach wydarzeń) lub uwzględnij nieistnienie danych (jeśli zmienił się katalog Produktów i chcemy dodać kolor do wyświetlenia, nie możemy być pewni, czy w danym momencie dany Produkt był w przeszłości miał kolor, czy nie - możemy założyć, że wszystkie Produkty z poprzedniego katalogu były czarne i uwzględniono je poprzez aktualizację zdarzeń lub kodu)

eithed
źródło
W odniesieniu do updating event history- W przypadku pozyskiwania zdarzeń historia zdarzeń jest Twoim źródłem prawdy i nigdy nie powinna być zmieniana, a jedynie iść do przodu. Jeśli zdarzenia się zmienią, możesz użyć wersji zdarzeń lub podobnych rozwiązań, ale podczas odtwarzania zdarzeń do określonego momentu stan danych powinien być taki, jak w tym momencie.
Nie
Jeśli chodzi o przechowywanie danych (schematów itp.) W celu tworzenia zapytań oraz dodawania / usuwania pól itp., Użyliśmy sami cosmosDB przechowujący dane w JSON w takiej postaci, w jakiej są w tym czasie. Jedyne, co wymaga wtedy wersji, to zdarzenia i / lub polecenia. Musisz także zaktualizować kontrakty punktów końcowych i wycenić obiekty zawierające dane odpowiadające na zapytania klienta (internet, telefon komórkowy itp.). Starsze dane nieposiadające pola będą miały wartość domyślną lub będą puste, co zawsze pasuje do biznesu, ale historia zdarzeń pozostaje taktowna i porusza się tylko do przodu.
Nie
@ updating event historyNie mam na myśli: przejrzyj wszystkie zdarzenia, kopiując je z jednego strumienia (v1) do innego strumienia (v2), aby zachować spójny schemat zdarzeń.
wyjechał
Nawiasem mówiąc, w sferze handlu / e-handlu możesz chcieć uchwycić cenę, jak podano, biorąc pod uwagę, że ceny często się zmieniają. Cena wyświetlana użytkownikowi może być inna w momencie przechwycenia faktycznego zamówienia. Istnieje wiele sposobów rozwiązania problemu, ale należy wziąć pod uwagę ten.
CPerson
@CPerson yup - cena może być jednym z atrybutów przekazywanych w ramach samego zdarzenia. Z drugiej strony adres URL obrazu może istnieć w ramach zdarzenia (reprezentuje zamiar display image at the point when purchase was made) lub nie może (reprezentować zamiar display current image as it within catalog)
eithed

Odpowiedzi:

3

Rozwiązanie nr 3 jest naprawdę blisko właściwego pomysłu.

Sposób na przemyślenie tego: B i C przechowują w pamięci podręcznej „lokalne” kopie potrzebnych im danych. Wiadomości przetwarzane w B (i podobnie w C) wykorzystują lokalnie buforowane informacje. Podobnie raporty są tworzone przy użyciu informacji przechowywanych w pamięci podręcznej lokalnie.

Dane są replikowane ze źródła do pamięci podręcznej za pośrednictwem stabilnego interfejsu API. B i C nie muszą nawet używać tego samego API - używają dowolnego protokołu pobierania, który jest odpowiedni do ich potrzeb. W efekcie definiujemy umowę - protokół i schemat wiadomości - które ograniczają dostawcę i konsumenta. Wówczas każdy konsument dla tej umowy może być podłączony do dowolnego dostawcy. Niekompatybilne wstecz zmiany wymagają nowej umowy.

Usługi wybierają odpowiednią strategię unieważnienia pamięci podręcznej dla swoich potrzeb. Może to oznaczać pobieranie zmian ze źródła zgodnie z harmonogramem lub w odpowiedzi na powiadomienie, że coś mogło się zmienić, a nawet „na żądanie” - działając jak pamięć podręczna odczytu, wracając do przechowywanej kopii danych, gdy źródło nie jest dostępne.

Daje to „autonomię” w tym sensie, że B i C mogą nadal dostarczać wartość biznesową, gdy A jest czasowo niedostępne.

Zalecana lektura: Dane z zewnątrz, Dane z wnętrza , Pat Helland 2005.

VoiceOfUnreason
źródło
Tak, całkowicie zgadzam się z tym, co tu napisałeś, a rozwiązanie 3 jest rozwiązaniem goto, które zastosowałem, jednak nie jest to podejście polegające na pozyskiwaniu zdarzeń, ponieważ jeśli odtworzymy zdarzenia, niekoniecznie chcemy użyj aktualnego stanu produktu; chcemy użyć stanu, jaki był w momencie wydarzenia. Oczywiście może to być w porządku (w zależności od wymagań biznesowych). Jeśli jednak chcemy śledzić zmiany w katalogu, co również wymaga pozyskiwania zdarzeń, i zależy od tego, ile to danych, możemy lepiej wrócić do rozwiązania 1.
eithed
1
Myślę, że masz to w / rozwiązanie nr 3. Jeśli potrzebujesz spójności odtwarzania z katalogiem, to również źródło zdarzeń. Musisz ponownie odtworzyć tylko po ponownym uruchomieniu, co jest prawdopodobnie podczas uruchamiania - kiedy już wstaniesz, musisz tylko spojrzeć na nowe wydarzenia, więc ilość danych prawdopodobnie nie jest prawdziwym problemem. Jednak nawet wtedy masz opcję (jeśli to konieczne) korzystania z punktów kontrolnych, tj. „Oto stan od zdarzenia 1000”, więc weź to i teraz musisz odtworzyć tylko bieżące zdarzenie 1,001 zamiast całej historii .
Mike B.
2

W informatyce są dwie trudne rzeczy , a jedną z nich jest unieważnienie pamięci podręcznej.

Rozwiązanie 2 jest absolutnie moją domyślną pozycją i ogólnie powinieneś rozważyć wdrożenie buforowania tylko wtedy, gdy napotkasz jeden z następujących scenariuszy:

  1. Wywołanie interfejsu API do usługi A powoduje problemy z wydajnością.
  2. Koszt usługi A niedziałającej i niemożności odzyskania danych jest znaczący dla firmy.

Problemy z wydajnością są tak naprawdę głównym czynnikiem. Istnieje wiele sposobów rozwiązania nr 2, które nie wymagają buforowania, na przykład zapewnienie wysokiej dostępności Usługi A.

Buforowanie powoduje znaczną złożoność systemu i może tworzyć przypadki brzegowe, które trudno uzasadnić, i błędy, które są bardzo trudne do odtworzenia. Musisz także zmniejszyć ryzyko dostarczenia nieaktualnych danych, gdy istnieją nowsze dane, co może być znacznie gorsze z perspektywy biznesowej niż (na przykład) wyświetlenie komunikatu „Usługa A nie działa - spróbuj ponownie później”.

Z tego doskonałego artykułu Udi Dahana:

Te zależności wkradają się na ciebie powoli, wiążąc sznurowadła razem, stopniowo spowalniając tempo rozwoju, podważając stabilność bazy kodu, w której zmiany w jednej części systemu niszczą inne części. To powolna śmierć o tysiąc cięć, w wyniku czego nikt nie jest pewien, jaką wielką decyzję podjęliśmy, która spowodowała, że ​​wszystko poszło tak źle.

Ponadto, jeśli potrzebujesz zapytania w czasie o dane produktu, powinno to być obsługiwane w sposób, w jaki dane są przechowywane w bazie danych produktu (np. Daty rozpoczęcia / zakończenia), powinny być wyraźnie widoczne w interfejsie API (data wejścia w życie musi być stanowić dane wejściowe dla wywołania API do zapytania danych).

Phil Sandler
źródło
1
@SavvasKleanthous „Sieć jest niezawodna” to jeden z błędów przetwarzania rozproszonego. Ale odpowiedzią na ten błąd nie powinno być „buforowanie każdego kawałka danych z każdej usługi w każdej innej usłudze” (zdaję sobie sprawę, że to trochę hiperboliczne). Oczekuj, że usługa może być niedostępna, i traktuj ją jako warunek błędu. Jeśli masz rzadką sytuację, w której zerwanie Usługi A ma poważny wpływ na działalność, rozważ (ostrożnie!) Inne opcje.
Phil Sandler,
1
@SavvasKleanthous uważa także (jak wspomniałem w mojej odpowiedzi), że zwracanie nieaktualnych danych w wielu przypadkach może być znacznie gorsze niż zgłaszanie błędu.
Phil Sandler,
1
@eithed Odniosłem się do tego komentarza: „Jeśli jednak chcemy śledzić zmiany w katalogu, wymaga to również pozyskiwania zdarzeń”. W każdym razie masz dobry pomysł - usługa produktu powinna być odpowiedzialna za śledzenie zmian w czasie, a nie usługi niższego szczebla.
Phil Sandler,
1
Ponadto przechowywanie obserwowanych danych, chociaż ma pewne podobieństwa do buforowania, nie powoduje takich samych problemów. W szczególności unieważnienie nie jest potrzebne; otrzymujesz nową wersję danych, kiedy to nastąpi. To, czego doświadczasz, to opóźniona spójność. Jednak nawet przy użyciu żądania internetowego istnieje okno niespójności (choć niewielkie).
Savvas Kleanthous
1
@SavvasKleanthous W każdym razie moim głównym celem nie jest próbowanie rozwiązywania problemów, które jeszcze nie istnieją, szczególnie w przypadku rozwiązań, które niosą ze sobą własne problemy i ryzyko. Opcja 2 jest najprostszym rozwiązaniem i powinna być domyślnym wyborem, dopóki nie spełni wymagań biznesowych . Jeśli uważasz, że wybór najprostszego rozwiązania, które może działać, jest (jak to ująłeś) „naprawdę zły”, to po prostu się nie zgadzamy.
Phil Sandler,
2

Bardzo trudno jest po prostu powiedzieć, że jedno rozwiązanie jest lepsze od drugiego. Wybór jednego spośród Rozwiązania nr 2 i nr 3 zależy od innych czynników (czas trwania pamięci podręcznej, tolerancja spójności, ...)

Moje 2 centy:

Unieważnienie pamięci podręcznej może być trudne, ale w opisie problemu wspomniano, że katalog produktów zmienia się rzadko. Ten fakt sprawia, że ​​dane produktu są dobrym kandydatem do buforowania

Rozwiązanie nr 1 (NOK)

  • Dane są powielane w wielu systemach

Rozwiązanie nr 2 (OK)

  • Oferuje silną konsystencję
  • Działa tylko wtedy, gdy usługa produktu jest wysoce dostępna i oferuje dobrą wydajność
  • Jeśli usługa e-mail przygotuje podsumowanie (z dużą ilością produktów), ogólny czas odpowiedzi może być dłuższy

Rozwiązanie nr 3 (złożone, ale preferowane)

  • Preferuj podejście API zamiast bezpośredniego dostępu do bazy danych, aby uzyskać informacje o produkcie
  • Odporność na usługi konsumujące nie ma wpływu, gdy usługa produktu jest wyłączona
  • Aplikacje konsumenckie (usługi wysyłkowe i pocztowe) pobierają szczegółowe informacje o produkcie natychmiast po opublikowaniu wydarzenia. Możliwość obniżenia poziomu usług w ciągu tych kilku milisekund jest bardzo odległa.
Sudhir
źródło
1

Ogólnie rzecz biorąc, zdecydowanie odradzam opcję 2 ze względu na czasowe połączenie między tymi dwiema usługami (chyba że komunikacja między tymi usługami jest bardzo stabilna i niezbyt częsta). Sprzężenie czasowe jest tym, co opisujesz this makes B and C dependant upon A (at all times)i oznacza, że ​​jeśli A jest w dół lub nieosiągalny z B lub C, B i C nie mogą spełniać swojej funkcji.

Osobiście uważam, że obie opcje 1 i 3 mają sytuacje, w których są one ważnymi opcjami.

Jeśli komunikacja między A i B i C jest tak wysoka lub ilość danych potrzebnych do wejścia na wydarzenie jest wystarczająco duża, aby wzbudzić obawy, to opcja 3 jest najlepszą opcją, ponieważ obciążenie sieci jest znacznie mniejsze , a opóźnienie operacji zmniejszy się wraz ze zmniejszeniem rozmiaru wiadomości. Inne kwestie do rozważenia tutaj to:

  1. Stabilność kontraktu: jeśli umowa wysłania wiadomości A zmienia się często, wówczas umieszczenie wielu właściwości w wiadomości spowoduje wiele zmian w konsumentach. Jednak w tym przypadku uważam, że nie jest to duży problem, ponieważ:
    1. Wspomniałeś, że system A to CMS. Oznacza to, że pracujesz w stabilnej domenie i dlatego nie sądzę, abyś często widział zmiany
    2. Ponieważ B i C są wysyłane i wysyłane pocztą e-mail, a Ty otrzymujesz dane od A, uważam, że będziesz doświadczać addytywnych zmian zamiast łamania tych, które można bezpiecznie dodawać za każdym razem, gdy odkryjesz je bez przeróbek.
  2. Sprzęganie: tutaj jest bardzo mało sprzęgła lub nie ma go wcale. Po pierwsze, ponieważ komunikacja odbywa się za pośrednictwem wiadomości, nie ma sprzężenia między usługami oprócz krótkiej czasowej podczas inicjowania danych i umowy tej operacji (która nie jest łączeniem, którego można lub należy unikać)

Opcja 1 nie jest jednak czymś, co odrzucam. Sprzężenie jest takie samo, ale pod względem programistycznym powinno być łatwe do wykonania (nie trzeba specjalnych działań), a stabilność domeny powinna oznaczać, że nie będą się one często zmieniać (jak już wspomniałem).

Inną opcją, którą zasugerowałem, jest niewielka odmiana w stosunku do 3, która nie polega na uruchomieniu procesu podczas uruchamiania, ale zamiast tego obserwuj zdarzenie „ProductAdded i„ ProductDetailsChanged ”na B i C, przy czym istnieje zmiana w katalogu produktów w A. Dzięki temu wdrożenia będą szybsze (a więc łatwiej naprawić problem / błąd, jeśli go znajdziesz).


Edytuj 2020-03-03

Przy ustalaniu podejścia integracyjnego mam określoną kolejność priorytetów:

  1. Jaki jest koszt spójności? Czy możemy zaakceptować kilka milisekund niespójności między rzeczami zmienionymi w A i odzwierciedlonymi w B&C?
  2. Czy potrzebujesz zapytań w czasie (zwanych również zapytaniami czasowymi)?
  3. Czy dane mają jakieś źródło prawdy? Usługa, która jest ich właścicielem i jest uważana za wcześniejszą?
  4. Jeśli istnieje właściciel / pojedyncze źródło prawdy, czy to jest stabilne? A może oczekujemy częstych przełomowych zmian?

Jeśli koszt niespójności jest wysoki (w zasadzie dane produktu w A muszą być możliwie jak najszybciej spójne z produktem buforowanym w B i C), nie można uniknąć konieczności zaakceptowania niedostępności i złożenia żądania synchronicznego (jak w sieci / rest request) z B & C do A, aby pobrać dane. Być świadomym! To nadal nie oznacza zgodnej transakcyjnie, ale po prostu minimalizuje okna niespójności. Jeśli absolutnie, pozytywnie musisz być natychmiast konsekwentny, musisz zmienić swoje granice usług. Jednak bardzo mocno wierzę, że nie powinno to stanowić problemu. Z doświadczenia wynika, że ​​tak naprawdę niezwykle rzadko firma nie jest w stanie zaakceptować kilku sekund niespójności, więc nie trzeba nawet wysyłać żądań synchronicznych.

Jeśli potrzebujesz zapytań w czasie (których nie zauważyłem w twoim pytaniu, a zatem nie uwzględniłem powyżej, być może błędnie), koszt utrzymania tego w usługach końcowych jest tak wysoki (musisz powielić wewnętrzna logika projekcji zdarzeń we wszystkich usługach niższego rzędu), która wyjaśnia decyzję: należy pozostawić własność na rzecz A i przesłać zapytanie A-hoc do żądania internetowego (lub podobnego), a A powinien użyć źródła zdarzeń, aby odzyskać wszystkie zdarzenia, o których wiedziałeś w tym czasie, aby wyświetlić stan i zwrócić go. Wydaje mi się, że może to być opcja 2 (jeśli dobrze zrozumiałem?), Ale koszty są takie, że chociaż połączenie czasowe jest lepsze niż koszty utrzymania zdublowanych zdarzeń i logiki projekcji.

Jeśli nie potrzebujesz punktu w czasie, a nie ma jednoznacznego, pojedynczego właściciela danych (który w mojej początkowej odpowiedzi założyłem na podstawie twojego pytania), bardzo rozsądnym wzorcem byłoby posiadanie reprezentacji produktu w każdej usłudze osobno. Gdy aktualizujesz dane dla produktów, aktualizujesz A, B i C równolegle, wysyłając równoległe żądania internetowe do każdego z nich, lub masz interfejs API poleceń, który wysyła wiele poleceń do każdego z A, B i C. B & C używa ich lokalna wersja danych do wykonania zadania, która może, ale nie musi być nieaktualna. Nie jest to żadna z powyższych opcji (chociaż można by uczynić ją zbliżoną do opcji 3), ponieważ dane w A, B i C mogą się różnić, a „całość” produktu może być kompozycją wszystkich trzech danych źródła.

Wiedza, czy źródłem prawdy jest stała umowa, jest przydatna, ponieważ można jej użyć do wykorzystania domeny / zdarzeń wewnętrznych (lub zdarzeń przechowywanych w źródle zdarzeń jako wzorzec przechowywania w A) do integracji w A i usługach B i C. Jeśli umowa jest stabilna, możesz zintegrować zdarzenia domeny. Masz jednak dodatkowe obawy w przypadku częstych zmian lub gdy umowa przesłania wiadomości jest wystarczająco duża, aby utrudnić transport.

Jeśli masz wyraźnego właściciela i antykoncepcję, która ma być stabilna, najlepszą opcją byłaby opcja 1; zamówienie zawierałoby wszystkie niezbędne informacje, a następnie B i C wykonywałyby swoją funkcję przy użyciu danych w zdarzeniu.

Jeśli zgodnie z opcją 3 istnieje możliwość zmiany lub zerwania umowy, to powrót do żądań internetowych w celu pobrania danych produktu jest w rzeczywistości lepszą opcją, ponieważ o wiele łatwiej jest utrzymać wiele wersji. Tak więc B złożyłby wniosek dotyczący v3 produktu.

Savvas Kleanthous
źródło
Tak, zgadzam się. Chociaż ProductAddedlub ProductDetailsChangeddodajemy złożoność śledzenia zmian katalogu produktów, musimy w pewien sposób synchronizować te dane między bazami danych, na wypadek, gdyby zdarzenia były odtwarzane i potrzebowaliśmy dostępu do danych katalogu z przeszłości.
wyjechał
@eithed Zaktualizowałem odpowiedź, aby rozwinąć niektóre założenia, które poczyniłem.
Savvas Kleanthous