Pet učenika i tri distribuirane pohrane ključ-vrijednosti

Ili kako smo napisali klijentsku C++ biblioteku za ZooKeeper, etcd i Consul KV

U svijetu distribuiranih sustava postoji niz tipičnih zadataka: pohranjivanje informacija o sastavu klastera, upravljanje konfiguracijom čvorova, otkrivanje neispravnih čvorova, odabir voditelja drugi. Za rješavanje ovih problema stvoreni su posebni distribuirani sustavi - usluge koordinacije. Sada će nas zanimati tri od njih: ZooKeeper, etcd i Consul. Od svih bogatih funkcionalnosti Consula, fokusirat ćemo se na Consul KV.

Pet učenika i tri distribuirane pohrane ključ-vrijednosti

U biti, svi ovi sustavi su tolerantni na greške, linearizirajuće pohrane ključ-vrijednosti. Iako njihovi modeli podataka imaju značajne razlike, o čemu ćemo kasnije raspravljati, oni rješavaju iste praktične probleme. Očito je da je svaka aplikacija koja koristi uslugu koordinacije vezana uz jednu od njih, što može dovesti do potrebe za podrškom nekoliko sustava u jednom podatkovnom centru koji rješavaju iste probleme za različite aplikacije.

Ideja za rješavanje ovog problema potekla je od jedne australske konzultantske agencije, a na nama, malom timu studenata, palo je da to realiziramo, o čemu ću sada govoriti.

Uspjeli smo stvoriti biblioteku koja pruža zajedničko sučelje za rad sa ZooKeeperom, etcd i Consul KV. Biblioteka je napisana u C++, ali postoje planovi za prijenos na druge jezike.

Modeli podataka

Da biste razvili zajedničko sučelje za tri različita sustava, morate razumjeti što im je zajedničko i po čemu se razlikuju. Hajdemo shvatiti.

Čuvar zoo vrta

Pet učenika i tri distribuirane pohrane ključ-vrijednosti

Ključevi su organizirani u stablo i nazivaju se čvorovi. U skladu s tim, za čvor možete dobiti popis njegovih potomaka. Operacije stvaranja znodea (create) i promjene vrijednosti (setData) su odvojene: samo postojeći ključevi mogu se čitati i mijenjati. Satovi se mogu pridružiti operacijama provjere postojanja čvora, čitanja vrijednosti i dobivanja djece. Watch je jednokratni okidač koji se aktivira kada se promijeni verzija odgovarajućih podataka na poslužitelju. Efemerni čvorovi koriste se za otkrivanje kvarova. Vezani su uz sesiju klijenta koji ih je kreirao. Kada klijent zatvori sesiju ili prestane obavještavati ZooKeeper o njenom postojanju, ti se čvorovi automatski brišu. Podržane su jednostavne transakcije - skup operacija koje ili sve uspijevaju ili ne uspijevaju ako to nije moguće za barem jednu od njih.

itd

Pet učenika i tri distribuirane pohrane ključ-vrijednosti

Programeri ovog sustava očito su bili inspirirani ZooKeeperom i stoga su sve učinili drugačije. Ne postoji hijerarhija ključeva, već oni čine leksikografski uređen skup. Možete dobiti ili izbrisati sve ključeve koji pripadaju određenom rasponu. Ova se struktura može činiti čudnom, ali zapravo je vrlo izražajna i kroz nju se lako može oponašati hijerarhijski pogled.

etcd nema standardnu ​​operaciju usporedbe i postavljanja, ali ima nešto bolje: transakcije. Naravno, postoje u sva tri sustava, ali etcd transakcije su posebno dobre. Sastoje se od tri bloka: provjera, uspjeh, neuspjeh. Prvi blok sadrži skup uvjeta, drugi i treći - operacije. Transakcija se izvršava atomski. Ako su svi uvjeti istiniti, tada se izvršava blok uspjeha, u protivnom se izvršava blok neuspjeha. U API-ju 3.3 blokovi uspjeha i neuspjeha mogu sadržavati ugniježđene transakcije. To jest, moguće je atomski izvršiti uvjetne konstrukcije gotovo proizvoljne razine ugniježđenja. Možete saznati više o tome iz čega postoje čekovi i operacije dokumentacija.

Satovi postoje i ovdje, iako su malo kompliciraniji i mogu se višekratno koristiti. Odnosno, nakon instaliranja sata na ključnom rasponu, primat ćete sva ažuriranja u tom rasponu dok ne otkažete sat, a ne samo prvo. U etcd-u, analog ZooKeeper sesija klijenta su zakupi.

Konzul K.V.

Ovdje također ne postoji stroga hijerarhijska struktura, ali Consul može stvoriti izgled da postoji: možete dobiti i izbrisati sve ključeve s navedenim prefiksom, odnosno raditi s "podstablom" ključa. Takvi se upiti nazivaju rekurzivnim. Osim toga, Consul može odabrati samo ključeve koji ne sadrže navedeni znak iza prefiksa, što odgovara dobivanju neposredne "djece". Ali vrijedi zapamtiti da je to upravo izgled hijerarhijske strukture: sasvim je moguće stvoriti ključ ako njegov roditelj ne postoji ili izbrisati ključ koji ima djecu, dok će djeca i dalje biti pohranjena u sustavu.

Pet učenika i tri distribuirane pohrane ključ-vrijednosti
Umjesto satova, Consul ima blokiranje HTTP zahtjeva. U biti, radi se o uobičajenim pozivima metode čitanja podataka, za koje je uz ostale parametre naznačena zadnja poznata verzija podataka. Ako je trenutna verzija odgovarajućih podataka na poslužitelju veća od navedene, odgovor se vraća odmah, inače - kada se vrijednost promijeni. Postoje i sesije koje se mogu pridružiti ključevima u bilo kojem trenutku. Vrijedno je napomenuti da za razliku od etcd-a i ZooKeeper-a, gdje brisanje sesija dovodi do brisanja pridruženih ključeva, postoji način rada u kojem se sesija jednostavno ne povezuje s njima. Dostupno transakcije, bez grana, ali sa svim vrstama provjera.

Sve skupa

ZooKeeper ima najstroži model podataka. Ekspresivni upiti raspona dostupni u etcd-u ne mogu se učinkovito emulirati ni u ZooKeeperu ni u Consulu. Pokušavajući ugraditi najbolje od svih usluga, dobili smo sučelje gotovo jednako ZooKeeper sučelju uz sljedeće značajne iznimke:

  • slijed, spremnik i TTL čvorovi Nije podržano
  • ACL-ovi nisu podržani
  • metoda set kreira ključ ako ne postoji (u ZK setData u ovom slučaju vraća grešku)
  • metode set i cas su odvojene (u ZK su u suštini ista stvar)
  • metoda brisanja briše čvor zajedno s njegovim podstablom (u ZK delete vraća grešku ako čvor ima djecu)
  • Za svaki ključ postoji samo jedna verzija - vrijednosna verzija (u ZK ima ih tri)

Odbijanje sekvencijalnih čvorova je zbog činjenice da etcd i Consul nemaju ugrađenu podršku za njih, a korisnik ih može lako implementirati na vrhu dobivenog sučelja knjižnice.

Implementacija ponašanja sličnog ZooKeeperu prilikom brisanja vrha zahtijevala bi održavanje zasebnog podređenog brojača za svaki ključ u etcd i Consul. Budući da smo pokušali izbjeći pohranjivanje meta informacija, odlučeno je da obrišemo cijelo podstablo.

Suptilnosti implementacije

Pogledajmo pobliže neke aspekte implementacije knjižničnog sučelja u različitim sustavima.

Hijerarhija u itd

Održavanje hijerarhijskog pogleda u etcd pokazalo se jednim od najzanimljivijih zadataka. Upiti raspona olakšavaju dohvaćanje popisa ključeva s određenim prefiksom. Na primjer, ako trebate sve ono s čim počinje "/foo", tražite raspon ["/foo", "/fop"). Ali to bi vratilo cijelo podstablo ključa, što možda nije prihvatljivo ako je podstablo veliko. Isprva smo planirali koristiti ključni mehanizam prevođenja, implementiran u zetcd. To uključuje dodavanje jednog bajta na početku ključa, jednakog dubini čvora u stablu. Dat ću vam primjer.

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

Zatim uzmite sve neposredne potomke ključa "/foo" moguće zahtjevom za raspon ["u02/foo/", "u02/foo0"). Da, u ASCII "0" stoji odmah nakon "/".

Ali kako implementirati uklanjanje vrha u ovom slučaju? Ispada da trebate izbrisati sve raspone tipa ["uXX/foo/", "uXX/foo0") za XX od 01. do FF. A onda smo naletjeli ograničenje broja operacija unutar jedne transakcije.

Kao rezultat toga, izumljen je jednostavan sustav pretvorbe ključa, koji je omogućio učinkovitu implementaciju i brisanja ključa i dobivanja popisa djece. Prije posljednjeg tokena dovoljno je dodati poseban znak. Na primjer:

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

Zatim brisanje ključa "/very" pretvara u brisanje "/u00very" i domet ["/very/", "/very0"), i dobivanje svih djece - u zahtjevu za ključeve iz asortimana ["/very/u00", "/very/u01").

Uklanjanje ključa u ZooKeeperu

Kao što sam već spomenuo, u ZooKeeperu ne možete izbrisati čvor ako ima djecu. Želimo izbrisati ključ zajedno s podstablom. Što da napravim? Činimo to s optimizmom. Prvo, rekurzivno prelazimo podstablo, dobivajući djecu svakog vrha posebnim upitom. Zatim gradimo transakciju koja pokušava izbrisati sve čvorove podstabla ispravnim redoslijedom. Naravno, promjene se mogu dogoditi između čitanja podstabla i njegovog brisanja. U tom slučaju transakcija neće uspjeti. Štoviše, podstablo se može promijeniti tijekom procesa čitanja. Zahtjev za potomcima sljedećeg čvora može vratiti pogrešku ako je, na primjer, ovaj čvor već izbrisan. U oba slučaja ponavljamo cijeli postupak.

Ovaj pristup čini brisanje ključa vrlo neučinkovitim ako ima djecu, a još više ako aplikacija nastavi raditi s podstablom, brišući i stvarajući ključeve. Međutim, to nam je omogućilo da izbjegnemo kompliciranje implementacije drugih metoda u etcd i Consul.

postavljen u ZooKeeperu

U ZooKeeperu postoje zasebne metode koje rade sa strukturom stabla (create, delete, getChildren) i koje rade s podacima u čvorovima (setData, getData). Štoviše, sve metode imaju stroge preduvjete: create će vratiti pogrešku ako je čvor već stvoreno, izbrišite ili postavite podatke – ako već ne postoji. Trebala nam je metoda skupa koja se može pozvati bez razmišljanja o prisutnosti ključa.

Jedna je mogućnost zauzeti optimističan pristup, kao kod brisanja. Provjerite postoji li čvor. Ako postoji, pozovite setData, inače kreirajte. Ako je posljednja metoda vratila pogrešku, ponovite sve iznova. Prvo što treba napomenuti je da je test postojanja besmislen. Možete odmah nazvati create. Uspješan završetak značit će da čvor nije postojao i da je stvoren. U suprotnom, create će vratiti odgovarajuću grešku, nakon čega trebate pozvati setData. Naravno, između poziva, vrh bi mogao biti izbrisan konkurentskim pozivom, a setData bi također vratio pogrešku. U ovom slučaju, možete sve ponoviti, ali isplati li se?

Ako obje metode vrate pogrešku, tada sa sigurnošću znamo da je došlo do natjecateljskog brisanja. Zamislimo da se ovo brisanje dogodilo nakon pozivanja skupa. Tada je bilo koje značenje koje pokušavamo uspostaviti već izbrisano. To znači da možemo pretpostaviti da je skup uspješno izvršen, čak i ako zapravo ništa nije zapisano.

Više tehničkih detalja

U ovom dijelu ćemo se odmoriti od distribuiranih sustava i razgovarati o kodiranju.
Jedan od glavnih zahtjeva kupca bila je višeplatformska platforma: barem jedna od usluga mora biti podržana na Linuxu, MacOS-u i Windowsu. U početku smo razvijali samo za Linux, a kasnije smo počeli testirati na drugim sustavima. To je izazvalo mnogo problema, kojima je neko vrijeme bilo potpuno nejasno kako pristupiti. Kao rezultat toga, sve tri usluge koordinacije sada su podržane na Linuxu i MacOS-u, dok je samo Consul KV podržan na Windowsima.

Od samog početka nastojali smo koristiti gotove knjižnice za pristup uslugama. U slučaju ZooKeepera izbor je pao na ZooKeeper C++, koji se naposljetku nije uspio kompilirati u sustavu Windows. To, međutim, nije iznenađujuće: biblioteka je pozicionirana samo kao linux. Za konzula je jedina opcija bila ppkonzul. Trebalo mu je dodati podršku sjednice и transakcije. Za etcd nije pronađena potpuna biblioteka koja podržava najnoviju verziju protokola, pa smo jednostavno generirani grpc klijent.

Inspirirani asinkronim sučeljem ZooKeeper C++ biblioteke, odlučili smo implementirati i asinkrono sučelje. ZooKeeper C++ za to koristi primitive future/promise. U STL-u su, nažalost, vrlo skromno implementirani. Na primjer, br zatim metoda, koji primjenjuje proslijeđenu funkciju na rezultat budućnosti kada postane dostupan. U našem slučaju, takva metoda je neophodna za pretvaranje rezultata u format naše knjižnice. Kako bismo zaobišli ovaj problem, morali smo implementirati vlastiti jednostavni skup niti, jer na zahtjev kupca nismo mogli koristiti teške biblioteke trećih strana kao što je Boost.

Naša tadašnja implementacija funkcionira ovako. Prilikom poziva, stvara se dodatni par obećanje/budućnost. Nova budućnost se vraća, a prošla se stavlja zajedno s odgovarajućom funkcijom i dodatnim obećanjem u red čekanja. Nit iz skupa odabire nekoliko budućnosti iz reda čekanja i ispituje ih koristeći wait_for. Kada rezultat postane dostupan, poziva se odgovarajuća funkcija i njezina povratna vrijednost prosljeđuje se obećanju.

Koristili smo isti skup niti za izvršavanje upita za etcd i Consul. To znači da temeljnim bibliotekama može pristupiti više različitih niti. ppconsul nije niti siguran, pa su pozivi prema njemu zaštićeni bravama.
Možete raditi s grpc iz više niti, ali postoje suptilnosti. U etcd satovi su implementirani putem grpc tokova. To su dvosmjerni kanali za poruke određene vrste. Knjižnica stvara jednu nit za sve nadzore i jednu nit koja obrađuje dolazne poruke. Dakle grpc zabranjuje paralelno pisanje u stream. To znači da kada inicijalizirate ili brišete sat, morate pričekati dok prethodni zahtjev ne završi slanje prije slanja sljedećeg. Koristimo za sinkronizaciju uvjetne varijable.

Ukupan

Uvjerite se sami: liboffkv.

Naš tim: Raed Romanov, Ivan Glušenkov, Dmitrij Kamaldinov, Viktor Krapivenski, Vitalij Ivanin.

Izvor: www.habr.com

Dodajte komentar