Pięciu studentów i trzy rozproszone sklepy typu klucz-wartość

Albo jak napisaliśmy bibliotekę klienta C++ dla ZooKeeper, etcd i Consul KV

W świecie systemów rozproszonych istnieje szereg typowych zadań: przechowywanie informacji o składzie klastra, zarządzanie konfiguracją węzłów, wykrywanie wadliwych węzłów, wybór lidera inny. Aby rozwiązać te problemy, stworzono specjalne systemy rozproszone – usługi koordynacyjne. Teraz będziemy zainteresowani trzema z nich: ZooKeeper, etcd i Consul. Spośród całej bogatej funkcjonalności Consula skupimy się na Consul KV.

Pięciu studentów i trzy rozproszone sklepy typu klucz-wartość

Zasadniczo wszystkie te systemy są odpornymi na błędy i linearyzowalnymi magazynami klucz-wartość. Chociaż ich modele danych różnią się znacznie, co omówimy później, rozwiązują te same problemy praktyczne. Oczywiście każda aplikacja korzystająca z usługi koordynacji jest powiązana z jedną z nich, co może skutkować koniecznością obsługi w jednym centrum danych kilku systemów, które rozwiązują te same problemy dla różnych aplikacji.

Pomysł rozwiązania tego problemu zrodził się w australijskiej agencji konsultingowej, a jego wdrożenie przypadło nam, małemu zespołowi studentów, i o tym będę mówił.

Udało nam się stworzyć bibliotekę zapewniającą wspólny interfejs do pracy z ZooKeeperem, itp. i Consul KV. Biblioteka jest napisana w C++, ale w planach jest przeniesienie jej na inne języki.

Modele danych

Aby opracować wspólny interfejs dla trzech różnych systemów, należy zrozumieć, co mają wspólnego i czym się różnią. Rozwiążmy to.

Opiekun zoo

Pięciu studentów i trzy rozproszone sklepy typu klucz-wartość

Klucze są zorganizowane w drzewo i nazywane są węzłami. Odpowiednio dla węzła można uzyskać listę jego dzieci. Operacje tworzenia znode (create) i zmiany wartości (setData) są rozdzielone: ​​można czytać i zmieniać tylko istniejące klucze. Zegarki można dołączyć do operacji sprawdzania istnienia węzła, odczytywania wartości i pobierania dzieci. Watch to jednorazowy wyzwalacz uruchamiany w przypadku zmiany wersji odpowiednich danych na serwerze. Węzły efemeryczne służą do wykrywania awarii. Są powiązane z sesją klienta, który je utworzył. Kiedy klient zamyka sesję lub przestaje powiadamiać ZooKeeper o jej istnieniu, węzły te są automatycznie usuwane. Obsługiwane są proste transakcje - zbiór operacji, które kończą się sukcesem lub niepowodzeniem, jeśli nie jest to możliwe w przypadku co najmniej jednej z nich.

itd

Pięciu studentów i trzy rozproszone sklepy typu klucz-wartość

Twórcy tego systemu wyraźnie inspirowali się ZooKeeperem i dlatego zrobili wszystko inaczej. Nie ma hierarchii kluczy, ale tworzą one uporządkowany leksykograficznie zbiór. Możesz pobrać lub usunąć wszystkie klucze należące do określonego zakresu. Struktura ta może wydawać się dziwna, ale w rzeczywistości jest bardzo wyrazista i można za jej pomocą łatwo naśladować pogląd hierarchiczny.

etcd nie ma standardowej operacji porównywania i ustawiania, ale ma coś lepszego: transakcje. Oczywiście istnieją we wszystkich trzech systemach, ale transakcje etcd są szczególnie dobre. Składają się z trzech bloków: kontrola, sukces, porażka. Pierwszy blok zawiera zestaw warunków, drugi i trzeci - operacje. Transakcja jest realizowana niepodzielnie. Jeśli wszystkie warunki są spełnione, wykonywany jest blok sukcesu, w przeciwnym razie wykonywany jest blok błędu. W API 3.3 bloki sukcesu i niepowodzenia mogą zawierać transakcje zagnieżdżone. Oznacza to, że możliwe jest atomowe wykonywanie konstrukcji warunkowych o niemal dowolnym poziomie zagnieżdżenia. Możesz dowiedzieć się więcej o tym, z jakich kontroli i operacji istnieją dokumentacja.

Zegarki też tu istnieją, chociaż są trochę bardziej skomplikowane i nadają się do wielokrotnego użytku. Oznacza to, że po zainstalowaniu zegarka na kluczowym zakresie będziesz otrzymywać wszystkie aktualizacje w tym zakresie, dopóki nie anulujesz zegarka, a nie tylko pierwszą. W etcd odpowiednikiem sesji klienta ZooKeeper są dzierżawy.

Konsul K.V.

Nie ma tu również ścisłej struktury hierarchicznej, ale Consul może stworzyć wrażenie, że istnieje: możesz pobierać i usuwać wszystkie klucze z określonym prefiksem, czyli pracować z „poddrzewem” klucza. Takie zapytania nazywane są rekurencyjnymi. Ponadto Consul może wybierać tylko klucze, które nie zawierają określonego znaku po przedrostku, co odpowiada uzyskaniu natychmiastowych „dzieci”. Warto jednak pamiętać, że to jest właśnie wygląd struktury hierarchicznej: całkiem możliwe jest utworzenie klucza, jeśli jego rodzic nie istnieje lub usunięcie klucza, który ma dzieci, podczas gdy dzieci będą nadal przechowywane w systemie.

Pięciu studentów i trzy rozproszone sklepy typu klucz-wartość
Zamiast zegarków Consul blokuje żądania HTTP. W istocie są to zwykłe wywołania metody odczytu danych, dla których wraz z innymi parametrami wskazywana jest ostatnia znana wersja danych. Jeżeli aktualna wersja odpowiednich danych na serwerze jest większa od podanej, odpowiedź jest zwracana natychmiast, w przeciwnym razie - w przypadku zmiany wartości. Istnieją również sesje, które można w dowolnym momencie podpiąć do kluczy. Warto zauważyć, że w przeciwieństwie do etcd i ZooKeepera, gdzie usunięcie sesji prowadzi do usunięcia powiązanych kluczy, istnieje tryb, w którym sesja jest po prostu od nich odłączona. Dostępny transakcje, bez oddziałów, ale z wszelkiego rodzaju kontrolami.

Kładąc wszystko razem

ZooKeeper ma najbardziej rygorystyczny model danych. Zapytania o zakres ekspresyjny dostępne w etcd nie mogą być skutecznie emulowane ani w ZooKeeper, ani w Consul. Próbując połączyć to, co najlepsze ze wszystkich usług, otrzymaliśmy interfejs prawie równoważny interfejsowi ZooKeeper, z następującymi istotnymi wyjątkami:

  • węzły sekwencyjne, kontenerowe i TTL niewspierany
  • Listy ACL nie są obsługiwane
  • metoda set tworzy klucz jeśli nie istnieje (w ZK setData zwraca w tym przypadku błąd)
  • metody set i cas są rozdzielone (w ZK to w zasadzie to samo)
  • metoda kasowania usuwa węzeł wraz z jego poddrzewem (w ZK usuwanie zwraca błąd, jeśli węzeł ma dzieci)
  • Dla każdego klucza istnieje tylko jedna wersja – wersja wartościowa (w ZK jest ich trzech)

Odrzucenie węzłów sekwencyjnych wynika z faktu, że ettd i Consul nie mają dla nich wbudowanej obsługi i użytkownik może je łatwo zaimplementować na wierzchu powstałego interfejsu biblioteki.

Implementacja zachowania podobnego do ZooKeepera podczas usuwania wierzchołka wymagałaby utrzymania oddzielnego licznika podrzędnego dla każdego klucza w etcd i Consul. Ponieważ próbowaliśmy uniknąć przechowywania metainformacji, zdecydowano się usunąć całe poddrzewo.

Subtelności wykonania

Przyjrzyjmy się bliżej niektórym aspektom implementacji interfejsu bibliotecznego w różnych systemach.

Hierarchia w itp

Utrzymanie widoku hierarchicznego w etcd okazało się jednym z najciekawszych zadań. Zapytania zakresowe ułatwiają pobranie listy kluczy z określonym prefiksem. Na przykład, jeśli potrzebujesz wszystkiego, co zaczyna się od "/foo", pytasz o zakres ["/foo", "/fop"). Ale to zwróci całe poddrzewo klucza, co może nie być akceptowalne, jeśli poddrzewo jest duże. Początkowo planowaliśmy zastosować kluczowy mechanizm tłumaczenia, zaimplementowany w zetcd. Polega na dodaniu jednego bajtu na początku klucza, równego głębokości węzła w drzewie. Dam ci przykład.

"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"

Następnie zdobądź wszystkie bezpośrednie dzieci klucza "/foo" możliwe, prosząc o zakres ["u02/foo/", "u02/foo0"). Tak, w ASCII "0" stoi zaraz po "/".

Ale jak w tym przypadku wdrożyć usunięcie wierzchołka? Okazuje się, że musisz usunąć wszystkie zakresy tego typu ["uXX/foo/", "uXX/foo0") dla XX od 01 do FF. A potem wpadliśmy limit liczby operacji w ramach jednej transakcji.

W rezultacie wynaleziono prosty system konwersji kluczy, który pozwolił skutecznie wdrożyć zarówno usunięcie klucza, jak i uzyskanie listy dzieci. Wystarczy dodać znak specjalny przed ostatnim tokenem. Na przykład:

"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"

Następnie usuń klucz "/very" zamienia się w usunięcie "/u00very" i zasięg ["/very/", "/very0"), a otrzymanie wszystkich dzieci - w zapytaniu o klucze z asortymentu ["/very/u00", "/very/u01").

Usuwanie klucza w ZooKeeperze

Jak już wspomniałem, w ZooKeeperze nie można usunąć węzła, jeśli ma on dzieci. Chcemy usunąć klucz wraz z poddrzewem. Co powinienem zrobić? Robimy to z optymizmem. Najpierw rekurencyjnie przemierzamy poddrzewo, uzyskując dzieci każdego wierzchołka za pomocą osobnego zapytania. Następnie budujemy transakcję, która próbuje usunąć wszystkie węzły poddrzewa w odpowiedniej kolejności. Oczywiście mogą wystąpić zmiany pomiędzy odczytaniem poddrzewa a jego usunięciem. W takim przypadku transakcja nie powiedzie się. Co więcej, poddrzewo może ulec zmianie podczas procesu odczytu. Zapytanie o dzieci następnego węzła może zwrócić błąd, jeśli na przykład ten węzeł został już usunięty. W obu przypadkach cały proces powtarzamy jeszcze raz.

Takie podejście sprawia, że ​​usuwanie klucza jest bardzo nieefektywne, jeśli ma on dzieci, a tym bardziej, jeśli aplikacja w dalszym ciągu pracuje z poddrzewem, usuwając i tworząc klucze. Pozwoliło nam to jednak uniknąć komplikowania implementacji innych metod w narzędziach etcd i Consul.

ustawione w ZooKeeperze

W ZooKeeperze istnieją osobne metody pracujące ze strukturą drzewa (create, Delete, getChildren) i działające z danymi w węzłach (setData, getData).Ponadto wszystkie metody mają ścisłe warunki wstępne: create zwróci błąd, jeśli węzeł już został utworzony, usuń go lub ustawData – jeśli jeszcze nie istnieje. Potrzebowaliśmy ustalonej metody, którą można wywołać bez myślenia o obecności klucza.

Jedną z opcji jest przyjęcie optymistycznego podejścia, tak jak w przypadku usuwania. Sprawdź, czy węzeł istnieje. Jeśli istnieje, wywołaj setData, w przeciwnym razie utwórz. Jeśli ostatnia metoda zwróciła błąd, powtórz ją jeszcze raz. Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że test istnienia jest bezcelowy. Możesz natychmiast wywołać funkcję create. Pomyślne zakończenie będzie oznaczać, że węzeł nie istniał i został utworzony. W przeciwnym razie create zwróci odpowiedni błąd, po czym należy wywołać setData. Oczywiście pomiędzy wywołaniami wierzchołek może zostać usunięty przez konkurencyjne wywołanie, a setData również zwróci błąd. W takim przypadku można zrobić to wszystko od nowa, ale czy warto?

Jeśli obie metody zwrócą błąd, wówczas wiemy na pewno, że miało miejsce konkurencyjne usunięcie. Wyobraźmy sobie, że to usunięcie nastąpiło po wywołaniu set. Wtedy jakiekolwiek znaczenie, które próbujemy ustalić, jest już wymazane. Oznacza to, że możemy założyć, że zestaw został wykonany pomyślnie, nawet jeśli w rzeczywistości nic nie zostało zapisane.

Więcej szczegółów technicznych

W tej sekcji zrobimy sobie przerwę od systemów rozproszonych i porozmawiamy o kodowaniu.
Jednym z głównych wymagań klienta była wieloplatformowość: przynajmniej jedna z usług musi być obsługiwana na systemach Linux, MacOS i Windows. Początkowo tworzyliśmy tylko dla Linuksa, a później zaczęliśmy testować na innych systemach. Powodowało to mnóstwo problemów, do których przez jakiś czas zupełnie nie było wiadomo, jak podejść. W rezultacie wszystkie trzy usługi koordynacyjne są teraz obsługiwane w systemach Linux i MacOS, natomiast w systemie Windows obsługiwany jest tylko Consul KV.

Od samego początku staraliśmy się korzystać z gotowych bibliotek dostępu do usług. W przypadku ZooKeepera wybór padł Zookeeper C++, którego ostatecznie nie udało się skompilować w systemie Windows. Nie jest to jednak zaskakujące: biblioteka jest pozycjonowana jako przeznaczona wyłącznie dla systemu Linux. Dla Konsula jedyną opcją było ppkonsul. Trzeba było do tego dodać wsparcie sesje и transakcje. W przypadku etcd nie znaleziono pełnoprawnej biblioteki obsługującej najnowszą wersję protokołu, więc po prostu wygenerowany klient grpc.

Zainspirowani asynchronicznym interfejsem biblioteki ZooKeeper C++, postanowiliśmy zaimplementować również interfejs asynchroniczny. ZooKeeper C++ używa do tego prymitywów przyszłości/obietnicy. W STL są one niestety realizowane bardzo skromnie. Na przykład nie następnie metoda, który stosuje przekazaną funkcję do wyniku w przyszłości, gdy stanie się ona dostępna. W naszym przypadku taka metoda jest konieczna do konwersji wyniku do formatu naszej biblioteki. Aby obejść ten problem, musieliśmy zaimplementować własną, prostą pulę wątków, ponieważ na życzenie klienta nie mogliśmy korzystać z ciężkich bibliotek innych firm, takich jak Boost.

Nasza ówczesna implementacja działa w ten sposób. Po wywołaniu tworzona jest dodatkowa para obietnica/przyszłość. Nowa przyszłość zostaje zwrócona, a miniona zostaje umieszczona w kolejce wraz z odpowiednią funkcją i dodatkową obietnicą. Wątek z puli wybiera kilka kontraktów futures z kolejki i odpytuje je za pomocą funkcji wait_for. Gdy wynik staje się dostępny, wywoływana jest odpowiednia funkcja, a jej wartość zwracana jest przekazywana do obietnicy.

Użyliśmy tej samej puli wątków do wykonywania zapytań do etcd i Consul. Oznacza to, że dostęp do podstawowych bibliotek można uzyskać za pośrednictwem wielu różnych wątków. ppconsul nie jest bezpieczny dla wątków, więc wywołania do niego są chronione blokadami.
Możesz pracować z grpc z wielu wątków, ale są pewne subtelności. W etcd zegarki są realizowane poprzez strumienie grpc. Są to dwukierunkowe kanały dla wiadomości określonego typu. Biblioteka tworzy jeden wątek dla wszystkich zegarków i jeden wątek przetwarzający wiadomości przychodzące. Zatem grpc zabrania równoległego zapisu do strumienia. Oznacza to, że podczas inicjowania lub usuwania zegarka należy poczekać, aż poprzednie żądanie zakończy wysyłanie, przed wysłaniem kolejnego. Używamy do synchronizacji zmienne warunkowe.

Łączny

Zobaczcie sami: liboffkv.

Nasz zespół: Raed Romanow, Iwan Głuszenkow, Dmitrij Kamaldinow, Wiktor Krapivensky, Witalij Iwanin.

Źródło: www.habr.com

Dodaj komentarz