Minimalizowanie koordynacji w celu osiągnięcia skalowalności
Większość aplikacji w chmurze składa się z wielu usług aplikacji — frontonów internetowych, baz danych, procesów biznesowych, raportowania i analizy itd. Aby można było osiągnąć skalowalność i niezawodność, każda z tych usług powinna działać w wielu wystąpieniach.
Niezordowane systemy, w których praca może być obsługiwana niezależnie bez konieczności przekazywania komunikatów między maszynami, są zwykle prostsze do skalowania. Koordynacja zwykle nie jest stanem binarnym, ale spektrum. Koordynacja występuje w różnych warstwach, takich jak dane lub obliczenia.
Co się stanie, gdy dwa wystąpienia spróbują przeprowadzić operacje współbieżne wpływające na jakiś wspólny stan? W niektórych przypadkach musi istnieć koordynacja między węzłami, na przykład w celu zachowania gwarancji niepodzielności, spójności, izolacji i trwałości. Na tym diagramie węzeł Node2
oczekuje, aż węzeł Node1
zwolni blokadę bazy danych:
Koordynacja ogranicza zalety skalowania w poziomie i przyczynia się do powstawania wąskich gardeł. W tym przykładzie w miarę skalowania aplikacji i dodawania kolejnych wystąpień wzrośnie rywalizacja o blokady. W najgorszym przypadku wystąpienia frontonu będą spędzać większość czasu na oczekiwaniu na blokady.
Semantyka „dokładnie jeden raz” to inne częste źródło koordynacji. Na przykład zamówienie musi zostać przetworzone dokładnie jeden raz. Dwa procesy robocze oczekują na nadejście nowych zamówień. Proces roboczy Worker1
pobiera zamówienie do przetworzenia. Aplikacja musi zapewnić, że proces roboczy Worker2
nie zduplikuje pracy, ale też że w razie awarii procesu Worker1
zamówienie nie zostanie usunięte.
Można użyć wzorca, takiego jak Agent harmonogramu — nadzorca, do koordynowania procesów roboczych, ale w tym przypadku lepszym rozwiązaniem może być podzielenie pracy. Każdemu procesowi roboczemu zostanie przypisany pewien zakres zamówień (na przykład według regionów rozliczeń). Jeśli proces roboczy ulegnie awarii, nowe wystąpienie wznowi pracę w miejscu przerwania jej przez poprzednie wystąpienie, ale nie będzie rywalizacji między wieloma wystąpieniami.
Zalecenia
Użyj składników rozdzielonych, które komunikują się asynchronicznie. Składniki powinny najlepiej używać zdarzeń do komunikowania się ze sobą.
Uwzględniaj spójność ostateczną. Gdy dane są rozproszone, koordynacja jest wymagana do wymuszania gwarancji silnej spójności. Załóżmy na przykład, że operacja aktualizuje dwie bazy danych. Zamiast stosować zakres pojedynczej transakcji, system powinien raczej być w stanie obsłużyć spójność ostateczną, być może przy użyciu wzorca transakcji kompensowania w celu logicznego wycofania po awarii.
Synchronizuj stan za pomocą zdarzeń domeny. Zdarzenie domeny to zdarzenie, które jest rejestrowane, gdy w domenie wydarzyło się coś istotnego. Zainteresowane usługi mogą nasłuchiwać zdarzeń, więc nie trzeba stosować transakcji globalnej do koordynowania wielu usług. Jeśli ta metoda jest używana, system musi tolerować spójność ostateczną (zobacz poprzednią pozycję).
Rozważ wzorce, takie jak CQRS i określanie źródła zdarzeń. Te dwa wzorce mogą pomóc w zmniejszeniu rywalizacji między obciążeniami odczytu i obciążeniami zapisu.
Wzorzec CQRS oddziela operacje odczytu od operacji zapisu. W niektórych implementacjach dane odczytu są fizycznie oddzielone od danych zapisu.
We wzorcu określania źródła zdarzeń zmiany stanu są rejestrowane jako szereg zdarzeń w magazynie danych, do którego można tylko dołączać dane. Dołączenie zdarzenia do strumienia jest operacją niepodzielną, wymagającą minimalnego blokowania.
Te dwa wzorce uzupełniają się wzajemnie. Jeśli magazyn tylko do zapisu we wzorcu CQRS używa określania źródła zdarzeń, magazyn ten może nasłuchiwać w oczekiwaniu na te same zdarzenia w celu utworzenia czytelnej migawki stanu bieżącego, zoptymalizowanej pod kątem zapytań. Przed wdrożeniem CQRS lub określania źródła zdarzeń należy jednak rozważyć wyzwania związane z tą metodą.
Partycjonowanie danych i stanu. Unikaj umieszczania wszystkich danych w jednym schemacie danych, który jest współużytkowany przez wiele usług aplikacji. Architektura mikrousług wymusza tę zasadę, czyniąc każdą usługę odpowiedzialną za jej własny magazyn danych. W ramach pojedynczej bazy danych partycjonowanie danych na fragmenty może zwiększyć współbieżność, ponieważ usługa zapisująca w jednym fragmencie nie wpływa na usługę zapisującą w innym fragmencie. Mimo że partycjonowanie dodaje pewien stopień koordynacji, można użyć partycjonowania w celu zwiększenia równoległości w celu zwiększenia skalowalności. Podziel stan monolityczny na mniejsze fragmenty, aby dane mogły być zarządzane niezależnie.
Projektuj operacje idempotentne. Gdy tylko to możliwe, projektuj operacje tak, aby były idempotentne. Umożliwi to obsługiwanie ich za pomocą semantyki „dokładnie jeden raz”. Na przykład można umieścić elementy robocze w kolejce. Jeśli w trakcie wykonywania operacji wystąpi awaria procesu roboczego, element roboczy zostanie po prostu przejęty przez inny proces roboczy. Jeśli proces roboczy musi zaktualizować dane, a także emitować inne komunikaty w ramach logiki, należy użyć wzorca przetwarzania komunikatów idempotentnych.
Używaj optymistycznej współbieżności, gdy tylko to możliwe. Mechanizm kontroli pesymistycznej współbieżności używa blokady bazy danych, aby zapobiec konfliktom. Może to powodować słabą wydajność i zmniejszać dostępność. W przypadku mechanizmu kontroli optymistycznej współbieżności każda transakcja modyfikuje kopię lub migawkę danych. Gdy transakcja zostanie zatwierdzona, aparat bazy danych weryfikuje ją i odrzuca wszelkie transakcje, które mogłyby wpłynąć na spójność bazy danych.
Usługa Azure SQL Database i program SQL Server obsługują optymistyczną współbieżność za pośrednictwem izolacji migawek. Niektóre usługi magazynu platformy Azure, w tym Azure Cosmos DB i Azure Storage, obsługują optymistyczną współbieżność przy użyciu elementów Etag.
Rozważ stosowanie rozwiązania MapReduce lub innych równoległych algorytmów rozproszonych. W zależności od danych i typu pracy do wykonania można podzielić pracę na niezależne zadania, które mogą być wykonywane przez wiele węzłów działających równolegle. Zobacz Big compute architecture style (Styl architektury dużych obliczeń).
Używaj wyboru lidera na potrzeby koordynacji. W przypadkach, w których trzeba koordynować operacje, upewnij się, że koordynator nie stanie się pojedynczym punktem awarii w aplikacji. Gdy jest stosowany wzorzec wyboru lidera, w każdym momencie jedno z wystąpień jest liderem i pełni rolę koordynatora. W przypadku awarii lidera do pełnienia roli lidera zostaje wybrane nowe wystąpienie.