Zavedení konzistentního heslování
S nástupem distribuovaných architektur se konzistentní hašování stalo hlavním proudem. Co to ale přesně je a jak se liší od standardního hashovacího algoritmu? Jaké jsou jeho přesné motivace?
Nejprve si popíšeme hlavní pojmy. Poté pronikneme do existujících algoritmů, abychom pochopili problémy spojené s konzistentním hashováním.
Hashování
Hashování je proces mapování dat libovolné velikosti na hodnoty pevné velikosti. Každý existující algoritmus má svou vlastní specifikaci:
- MD5 vytváří 128bitové hodnoty hash.
- SHA-1 vytváří 160bitové hodnoty hash.
- atd.
Hashing má v informatice mnoho aplikací. Jednou z těchto aplikací je například tzv. kontrolní součet. K ověření integrity datové sady je možné použít hashovací algoritmus. Server zahesluje datovou sadu a hodnotu hashe sdělí klientovi. Klient pak zahesluje svou verzi datové sady a porovná hodnoty hash. Pokud jsou stejné, měla by být ověřena integrita.
Důležité je zde slovo „měla“. V nejhorším případě dojde ke kolizi. Kolize nastane, když dvě různé části dat mají stejnou hodnotu hash. Uveďme si reálný příklad definováním následující hašovací funkce: Při zadání osoby vrátí datum jejího narození (den & měsíc narození). Narozeninový paradox nám říká, že pokud máme v místnosti pouze 23 osob, je pravděpodobnost, že dvě osoby budou mít stejné datum narození (tedy kolize), více než 50 %. Proto funkce narozenin pravděpodobně není dobrou hašovací funkcí.
Jako rychlý úvod o hašování je důležité pochopit, že hlavní myšlenkou je rozprostření hodnot po doméně. Například:
- MD5 rozprostírá hodnoty po 128bitové prostorové doméně
- Hashovací tabulka (nebo hashmap) podpořená polem o 32 prvcích má vnitřní hashovací funkci, která rozprostírá hodnoty na libovolný index (od 0 do 31).
Rozložení zátěže
Rozložení zátěže lze definovat jako proces rozprostření zátěže po uzlech. Pojem uzel je zde zaměnitelný se serverem nebo instancí. Jedná se o výpočetní jednotku.
Rozdělování zátěže je jedním z příkladů rozdělování zátěže. Jde o rozdělení sady úloh na sadu prostředků. Například rozdělování zátěže používáme k rozdělení požadavků API mezi instance webových serverů.
Pokud jde o data, používáme spíše termín sharding. Databázový shard je horizontální oddíl dat v databázi. Typickým příkladem je databáze rozdělená na tři shardy, kde každý shard má podmnožinu celkových dat.
Vyvažování zátěže a sharding mají některé společné problémy. Například rovnoměrné rozložení dat, které zaručí, že některý uzel nebude ve srovnání s ostatními přetížen. V určitém kontextu je při vyvažování zátěže a shardingu také třeba stále přiřazovat úlohy nebo data ke stejnému uzlu:
- Potřebujeme-li serializovat, postupně zpracovávat operace pro daného spotřebitele, musíme požadavek směrovat na stejný uzel.
- Potřebujeme-li rozdělit data, musíme vědět, který shard je vlastníkem pro určitý klíč.
Zní vám to povědomě? V těchto dvou příkladech šíříme hodnoty napříč doménou. Ať už se jedná o šíření úlohy do uzlů serveru nebo o šíření dat do shardu databáze, nacházíme opět myšlenku spojenou s hashováním. To je důvod, proč lze hashování použít ve spojení s rozložením zátěže. Podívejme se, jak na to.
Mod-n hašování
Princip mod-n hašování je následující. Každý klíč je hashován pomocí hashovací funkce, která transformuje vstup na celé číslo. Poté provedeme modulo na základě počtu uzlů.
Podívejme se na konkrétní příklad se 3 uzly. Zde potřebujeme rozdělit zátěž mezi tyto uzly na základě identifikátoru klíče. Každý klíč zaheslujeme a poté provedeme operaci modulo:
Výhodou tohoto přístupu je jeho bezstavovost. Nemusíme uchovávat žádný stav, který by nám připomínal, že foo
byl směrován do uzlu 2. Přesto potřebujeme vědět, kolik uzlů je nakonfigurováno pro použití operace modulo.
Jak tedy mechanismus funguje v případě škálování (přidávání nebo odebírání uzlů)? Pokud přidáme další uzel, operace modulo je nyní založena na 4 místo 3:
Stejný algoritmus můžeme použít při pohybu vpřed. Přesto by byl klíč přiřazen k uzlu bez ohledu na to, s jakým virtuálním uzlem se setkal.
Zrovna v tomto příkladu by nyní bylo rozložení následující:
-
N0
: 33,3 % -
N1
: 25 % -
N2
: 41,6 %
Čím více virtuálních uzlů na uzel definujeme, tím více by mělo být rozložení rovnoměrné. V průměru při 100 virtuálních uzlech na server je standardní rozložení přibližně 10 %. Při 1000 je to asi 3,2 %.
Tento mechanismus je také užitečný, pokud máme uzly s různou velikostí. Pokud je například uzel nakonfigurován tak, aby teoreticky zvládl dvakrát větší zátěž než ostatní, můžeme roztočit dvakrát více virtuálních uzlů.
Hlavní nevýhodou virtuálních uzlů je však paměťová náročnost. Pokud bychom měli obsluhovat tisíce serverů, vyžadovalo by to megabajty paměti.
Než se přesuneme dál, je zajímavé poznamenat, že někdy lze algoritmus podstatně vylepšit změnou malé části. To je například případ algoritmu zveřejněného společností Google v roce 2017 s názvem konzistentní hashování s omezeným zatížením. Tato verze je založena na stejné myšlence kruhu, kterou jsme popsali. Jejich přístup však spočívá v tom, že při každé aktualizaci (přidání/odstranění nového klíče nebo uzlu) provedou malé vyvážení. Tato verze překonává původní verzi z hlediska směrodatné odchylky s kompromisem nejhorší časové složitosti.
Skokové konzistentní hashování
V roce 2007 publikovali dva inženýři ze společnosti Google skokové konzistentní hashování. Ve srovnání s algoritmem založeným na kroužcích tato implementace „nevyžaduje žádné úložiště, je rychlejší a lépe rozděluje prostor pro klíče mezi buckety a rovnoměrně rozděluje pracovní zátěž při změně počtu bucketů“. Jinak řečeno, zlepšuje rozdělení pracovní zátěže mezi uzly (kbelík je stejný pojem jako uzel) bez jakékoli paměťové režie.
Tady je algoritmus v jazyce C++ (7 řádků kódu 🤯):
Při kruhovém konzistentním hašování byla při 1000 virtuálních uzlech směrodatná odchylka asi 3,2 %. Ve skokově konzistentním hashi již koncept virtuálních uzlů nepotřebujeme. Přesto zůstává směrodatná odchylka menší než 0,0000001 %.
Je zde však jedno omezení. Uzly musí být číslovány postupně. Pokud máme seznam serverů foo
, bar
a baz
, nemůžeme například odstranit bar
. Přesto, pokud nakonfigurujeme datové úložiště, můžeme použít algoritmus pro získání indexu oddílů na základě celkového počtu oddílů. Proto může být skokové konzistentní hashování užitečné v kontextu shardingu, ale ne v kontextu vyvažování zátěže.
Co je to algoritmus dokonalého konzistentního hashování?
Když už máme s konzistentním hašováním nějaké zkušenosti, podívejme se o krok zpět, jaký by byl dokonalý algoritmus:
- Přemístěno by bylo v průměru jen 1/n procent klíčů, kde n je počet uzlů.
- Složitost O(n) prostoru, kde n je počet uzlů.
- Časová složitost O(1) na vložení/odstranění uzlu a na vyhledání klíče.
- Minimální směrodatná odchylka, aby bylo jisté, že uzel není přetížen oproti jinému.
- Umožnil by přiřadit uzlu váhu, aby se vypořádal s různou velikostí uzlů.
- Umožnil by libovolné pojmenování uzlů (nečíslovaných postupně), aby podporoval jak vyrovnávání zátěže, tak sharding.
Naneštěstí tento algoritmus neexistuje. Na základě toho, co jsme viděli:
- Rendezvous má lineární časovou složitost na jedno vyhledávání.
- Kroužkově konzistentní hash má špatnou minimální směrodatnou odchylku bez konceptu virtuálních uzlů. S virtuálními uzly je prostorová složitost O(n*v), přičemž n je počet uzlů a v je počet virtuálních uzlů na uzel.
- Jump consistent hash nemá konstantní časovou složitost a nepodporuje libovolné jméno uzlu.
Téma je stále otevřené a existují nedávné studie, které stojí za pozornost. Například vícesondový konzistentní hash vydaný v roce 2005. Podporuje prostorovou složitost O(1). Přesto k dosažení směrodatné odchylky ε vyžaduje O(1/ε) času na jedno vyhledávání. Chceme-li například dosáhnout směrodatné odchylky menší než 0,5 %, vyžaduje to hašování klíče asi 20krát. Můžeme tedy získat minimální směrodatnou odchylku, ale za cenu vyšší časové složitosti vyhledávání.
Jak jsme uvedli v úvodu, konzistentní hashovací algoritmy se staly hlavním proudem. Nyní se používá v nespočtu systémů, jako jsou MongoDB, Cassandra, Riak, Akka atd. ať už v souvislosti s vyrovnáváním zátěže nebo distribucí dat. Přesto, jak už to v informatice bývá, každé řešení má své kompromisy.
Když už mluvíme o kompromisech, pokud potřebujete navázat, můžete se podívat na vynikající příspěvek, který napsal Damian Gryski:
.
Leave a Reply