Pět studentů a tři distribuované obchody s páry klíč-hodnota

Nebo jak jsme napsali klientskou knihovnu C++ pro ZooKeeper, etcd a Consul KV

Ve světě distribuovaných systémů existuje řada typických úkolů: ukládání informací o složení clusteru, správa konfigurace uzlů, detekce vadných uzlů, výběr vedoucího další. K řešení těchto problémů byly vytvořeny speciální distribuované systémy - koordinační služby. Nyní nás budou zajímat tři z nich: ZooKeeper, etcd a Consul. Ze všech bohatých funkcí Consul se zaměříme na Consul KV.

Pět studentů a tři distribuované obchody s páry klíč-hodnota

V podstatě všechny tyto systémy jsou odolné proti chybám, linearizovatelné úložiště párů klíč-hodnota. Přestože mají jejich datové modely značné rozdíly, o kterých budeme hovořit později, řeší stejné praktické problémy. Je zřejmé, že každá aplikace využívající koordinační službu je svázána s jednou z nich, což může vést k potřebě podporovat několik systémů v jednom datovém centru, které řeší stejné problémy pro různé aplikace.

Myšlenka vyřešit tento problém vznikla v jedné australské poradenské agentuře a na nás, malém týmu studentů, jsme ji implementovali, o čemž budu mluvit.

Podařilo se nám vytvořit knihovnu, která poskytuje společné rozhraní pro práci se ZooKeeperem, etcd a Consul KV. Knihovna je napsána v C++, ale plánuje se její portování do jiných jazyků.

Datové modely

Chcete-li vyvinout společné rozhraní pro tři různé systémy, musíte pochopit, co mají společného a jak se liší. Pojďme na to přijít.

ZooKeeper

Pět studentů a tři distribuované obchody s páry klíč-hodnota

Klíče jsou uspořádány do stromu a nazývají se uzly. V souladu s tím můžete pro uzel získat seznam jeho potomků. Operace vytvoření uzlu (create) a změny hodnoty (setData) jsou odděleny: lze číst a měnit pouze existující klíče. Watches lze připojit k operacím kontroly existence uzlu, čtení hodnoty a získávání dětí. Watch je jednorázový spouštěč, který se spustí, když se změní verze odpovídajících dat na serveru. K detekci poruch se používají efemérní uzly. Jsou vázány na relaci klienta, který je vytvořil. Když klient zavře relaci nebo přestane upozorňovat ZooKeeper na její existenci, tyto uzly se automaticky odstraní. Jsou podporovány jednoduché transakce – sada operací, které buď všechny uspějí, nebo selžou, pokud to alespoň u jedné z nich není možné.

atd

Pět studentů a tři distribuované obchody s páry klíč-hodnota

Vývojáři tohoto systému se jednoznačně inspirovali ZooKeeperem, a proto udělali vše jinak. Neexistuje žádná hierarchie klíčů, ale tvoří lexikograficky uspořádanou množinu. Můžete získat nebo odstranit všechny klíče patřící do určitého rozsahu. Tato struktura se může zdát zvláštní, ale ve skutečnosti je velmi expresivní a lze přes ni snadno napodobit hierarchický pohled.

etcd nemá standardní operaci porovnání a nastavení, ale má něco lepšího: transakce. Samozřejmě existují ve všech třech systémech, ale transakce etcd jsou obzvláště dobré. Skládají se ze tří bloků: kontrola, úspěch, neúspěch. První blok obsahuje sadu podmínek, druhý a třetí - operace. Transakce se provádí atomicky. Pokud jsou splněny všechny podmínky, provede se blok úspěchu, jinak se provede blok selhání. V API 3.3 mohou bloky úspěchu a selhání obsahovat vnořené transakce. To znamená, že je možné atomicky provádět podmíněné konstrukce téměř libovolné úrovně vnoření. Můžete se dozvědět více o tom, z čeho existují kontroly a operace dokumentace.

Hodinky existují i ​​zde, i když jsou trochu složitější a jsou opakovaně použitelné. To znamená, že po instalaci hodinek na klíčovou řadu budete dostávat všechny aktualizace v tomto rozsahu, dokud hodinky nezrušíte, a to nejen tu první. V etcd jsou analogem klientských relací ZooKeeper pronájmy.

Konzul K.V.

Zde také neexistuje žádná přísná hierarchická struktura, ale Consul může vytvořit zdání, že existuje: můžete získat a odstranit všechny klíče se zadanou předponou, to znamená pracovat s „podstromem“ klíče. Takové dotazy se nazývají rekurzivní. Kromě toho může Consul vybrat pouze klíče, které neobsahují zadaný znak za předponou, což odpovídá získání bezprostředních „dětí“. Ale stojí za to připomenout, že toto je přesně vzhled hierarchické struktury: je docela možné vytvořit klíč, pokud jeho rodič neexistuje, nebo odstranit klíč, který má děti, zatímco děti budou nadále uloženy v systému.

Pět studentů a tři distribuované obchody s páry klíč-hodnota
Místo hodinek má Consul blokování požadavků HTTP. V podstatě jde o obyčejná volání metody čtení dat, u kterých je spolu s dalšími parametry uvedena poslední známá verze dat. Pokud je aktuální verze odpovídajících dat na serveru větší než zadaná, je odpověď vrácena okamžitě, v opačném případě - když se hodnota změní. Existují také relace, které lze ke klíčům kdykoli připojit. Stojí za zmínku, že na rozdíl od etcd a ZooKeeper, kde odstranění relací vede ke smazání přidružených klíčů, existuje režim, ve kterém je relace jednoduše odpojena od nich. Dostupný transakce, bez poboček, ale se všemi druhy kontrol.

Dát to všechno dohromady

ZooKeeper má nejpřísnější datový model. Výrazné dotazy na rozsah dostupné v etcd nelze efektivně emulovat ani v ZooKeeper, ani v Consul. Ve snaze začlenit to nejlepší ze všech služeb jsme skončili s rozhraním téměř ekvivalentním rozhraní ZooKeeper s následujícími významnými výjimkami:

  • sekvence, kontejner a uzly TTL není podporováno
  • ACL nejsou podporovány
  • metoda set vytvoří klíč, pokud neexistuje (v ZK setData vrátí chybu v tomto případě)
  • metody set a cas jsou odděleny (v ZK jsou v podstatě to samé)
  • metoda erase odstraní uzel spolu s jeho podstromem (v ZK delete vrátí chybu, pokud má uzel potomky)
  • Pro každý klíč existuje pouze jedna verze - verze hodnoty (v ZK jsou tři)

Odmítnutí sekvenčních uzlů je způsobeno tím, že etcd a Consul pro ně nemají vestavěnou podporu a uživatel je může snadno implementovat nad výsledné rozhraní knihovny.

Implementace chování podobného ZooKeeperu při mazání vrcholu by vyžadovalo udržování samostatného podřízeného čítače pro každý klíč v etcd a Consul. Protože jsme se snažili vyhnout ukládání meta informací, bylo rozhodnuto smazat celý podstrom.

Jemnosti implementace

Podívejme se blíže na některé aspekty implementace rozhraní knihovny v různých systémech.

Hierarchie v etcd

Udržování hierarchického pohledu v etcd se ukázalo jako jeden z nejzajímavějších úkolů. Rozsahové dotazy usnadňují načtení seznamu klíčů se zadanou předponou. Například pokud potřebujete vše, co začíná "/foo", žádáte o rozsah ["/foo", "/fop"). To by však vrátilo celý podstrom klíče, což nemusí být přijatelné, pokud je podstrom velký. Nejprve jsme plánovali použít klíčový překladový mechanismus, implementováno v zetcd. Zahrnuje přidání jednoho bajtu na začátek klíče, který se rovná hloubce uzlu ve stromu. Dovolte mi uvést příklad.

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

Pak získejte všechny bezprostřední potomky klíče "/foo" možné po vyžádání rozsahu ["u02/foo/", "u02/foo0"). Ano, v ASCII "0" stojí hned po "/".

Jak ale v tomto případě implementovat odstranění vrcholu? Ukazuje se, že musíte odstranit všechny rozsahy typu ["uXX/foo/", "uXX/foo0") pro XX od 01 do FF. A pak jsme narazili limit počtu operací v rámci jedné transakce.

V důsledku toho byl vynalezen jednoduchý systém konverze klíčů, který umožnil efektivně implementovat jak smazání klíče, tak získání seznamu dětí. Před poslední žeton stačí přidat speciální znak. Například:

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

Poté klíč smažte "/very" změní na smazání "/u00very" a rozsah ["/very/", "/very0"), a získání všech dětí - v žádosti o klíče od sortimentu ["/very/u00", "/very/u01").

Odebrání klíče v ZooKeeper

Jak jsem již zmínil, v ZooKeeper nemůžete odstranit uzel, pokud má děti. Chceme odstranit klíč spolu s podstromem. Co bych měl dělat? Děláme to s optimismem. Nejprve rekurzivně procházíme podstrom a získáváme potomky každého vrcholu pomocí samostatného dotazu. Poté vytvoříme transakci, která se pokusí odstranit všechny uzly podstromu ve správném pořadí. Mezi čtením podstromu a jeho vymazáním samozřejmě může dojít ke změnám. V tomto případě se transakce nezdaří. Navíc se podstrom může během procesu čtení změnit. Požadavek na potomky dalšího uzlu může vrátit chybu, pokud byl například tento uzel již smazán. V obou případech celý proces opakujeme znovu.

Tento přístup činí odstranění klíče velmi neefektivním, pokud má potomky, a ještě více, pokud aplikace nadále pracuje s podstromem, maže a vytváří klíče. To nám však umožnilo vyhnout se komplikaci implementace jiných metod v etcd a Consul.

nastavit v ZooKeeper

V ZooKeeper jsou samostatné metody, které pracují se stromovou strukturou (create, delete, getChildren) a které pracují s daty v uzlech (setData, getData).Všechny metody navíc mají striktní předpoklady: create vrátí chybu, pokud uzel již byla vytvořena, smažte nebo nastavteData – pokud již neexistují. Potřebovali jsme metodu sady, kterou lze volat bez přemýšlení o přítomnosti klíče.

Jednou z možností je zaujmout optimistický přístup, jako u mazání. Zkontrolujte, zda uzel existuje. Pokud existuje, zavolejte setData, jinak vytvořte. Pokud poslední metoda vrátila chybu, opakujte ji znovu. První věc, kterou je třeba poznamenat, je, že test existence je zbytečný. Můžete okamžitě volat vytvoření. Úspěšné dokončení bude znamenat, že uzel neexistoval a byl vytvořen. V opačném případě create vrátí příslušnou chybu, po které je třeba zavolat setData. Samozřejmě mezi hovory by mohl být vrchol odstraněn konkurenčním voláním a setData by také vrátilo chybu. V tomto případě to můžete udělat znovu, ale stojí to za to?

Pokud obě metody vrátí chybu, pak s jistotou víme, že došlo ke konkurenčnímu smazání. Představme si, že k tomuto smazání došlo po volání set. Pak je jakýkoli význam, který se snažíme nastolit, již vymazán. To znamená, že můžeme předpokládat, že sada byla úspěšně provedena, i když ve skutečnosti nebylo nic zapsáno.

Další technické detaily

V této části si dáme pauzu od distribuovaných systémů a povíme si něco o kódování.
Jedním z hlavních požadavků zákazníka byla multiplatformnost: alespoň jedna ze služeb musí být podporována na Linuxu, MacOS a Windows. Zpočátku jsme vyvíjeli pouze pro Linux a později jsme začali testovat na jiných systémech. To způsobilo spoustu problémů, ke kterým bylo nějakou dobu zcela nejasné, jak k nim přistupovat. V důsledku toho jsou nyní všechny tři koordinační služby podporovány v systémech Linux a MacOS, zatímco v systému Windows je podporován pouze Consul KV.

Od samého začátku jsme se snažili pro přístup ke službám používat hotové knihovny. V případě ZooKeeper padla volba na ZooKeeper C++, který se nakonec nepodařilo zkompilovat na Windows. To však není překvapivé: knihovna je umístěna pouze pro linux. Pro konzula byla jediná možnost ppkonzul. K tomu bylo třeba přidat podporu sezení и transakce. Pro etcd nebyla nalezena plnohodnotná knihovna podporující nejnovější verzi protokolu, tak jsme jednoduše vygenerovaný klient grpc.

Inspirováni asynchronním rozhraním knihovny ZooKeeper C++ jsme se rozhodli implementovat také asynchronní rozhraní. ZooKeeper C++ k tomu používá primitiva future/promise. V STL jsou bohužel implementovány velmi skromně. Například ne pak metoda, který aplikuje předávanou funkci na výsledek budoucnosti, až bude k dispozici. V našem případě je taková metoda nezbytná pro převod výsledku do formátu naší knihovny. Abychom tento problém vyřešili, museli jsme implementovat vlastní jednoduchý fond vláken, protože na žádost zákazníka jsme nemohli používat těžké knihovny třetích stran, jako je Boost.

Naše tehdejší implementace funguje takto. Po zavolání se vytvoří další dvojice slib/budoucí. Nová budoucnost je vrácena a předaná budoucnost je umístěna spolu s odpovídající funkcí a dalším příslibem do fronty. Vlákno z fondu vybere několik futures z fronty a dotazuje je pomocí wait_for. Když je výsledek dostupný, je zavolána odpovídající funkce a její návratová hodnota je předána příslibu.

Použili jsme stejný fond vláken k provádění dotazů na etcd a Consul. To znamená, že k základním knihovnám může přistupovat více různých vláken. ppconsul není bezpečný pro vlákna, takže volání do něj jsou chráněna zámky.
S grpc můžete pracovat z více vláken, ale jsou tu jemnosti. V etcd jsou hodinky implementovány prostřednictvím grpc streamů. Jedná se o obousměrné kanály pro zprávy určitého typu. Knihovna vytvoří jedno vlákno pro všechna sledování a jedno vlákno, které zpracovává příchozí zprávy. Takže grpc zakazuje paralelní zápisy do streamu. To znamená, že při inicializaci nebo mazání hodinek musíte před odesláním dalšího počkat, dokud předchozí požadavek nedokončí odeslání. Používáme pro synchronizaci podmíněné proměnné.

Celkový

Přesvědčte se sami: liboffkv.

Náš tým: Raed Romanov, Ivan Glušenkov, Dmitrij Kamaldinov, Viktor Krapivenskij, Vitalij Ivanin.

Zdroj: www.habr.com

Přidat komentář