Pet studenata i tri distribuirane prodavnice ključ/vrijednost

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

U svijetu distribuiranih sistema postoji niz tipičnih zadataka: pohranjivanje informacija o sastavu klastera, upravljanje konfiguracijom čvorova, otkrivanje neispravnih čvorova, odabir lidera i drugi. Za rješavanje ovih problema stvoreni su posebni distribuirani sistemi - službe za koordinaciju. Sada će nas zanimati tri od njih: ZooKeeper, etcd i Consul. Od svih bogatih funkcionalnosti Consula, fokusiraćemo se na Consul KV.

Pet studenata i tri distribuirane prodavnice ključ/vrijednost

U suštini, svi ovi sistemi su otporna na greške, linearizabilna skladišta ključ/vrijednost. Iako njihovi modeli podataka imaju značajne razlike, o kojima ćemo govoriti kasnije, oni rješavaju iste praktične probleme. Očigledno je da je svaka aplikacija koja koristi uslugu koordinacije vezana za jednu od njih, što može dovesti do potrebe za podrškom više sistema u jednom data centru koji rješavaju iste probleme za različite aplikacije.

Ideja za rješavanje ovog problema potekla je u jednoj australskoj konsultantskoj agenciji, a na nama je, malom timu studenata, da je implementiramo, o čemu ću i govoriti.

Uspjeli smo napraviti biblioteku koja pruža zajednički interfejs za rad sa ZooKeeperom, etcd i Consul KV. Biblioteka je napisana na C++, ali postoje planovi da se prenese na druge jezike.

Modeli podataka

Da biste razvili zajedničko sučelje za tri različita sistema, morate razumjeti šta im je zajedničko i po čemu se razlikuju. Hajde da to shvatimo.

ZooKeeper

Pet studenata i tri distribuirane prodavnice ključ/vrijednost

Ključevi su organizirani u stablo i zovu se čvorovi. Shodno tome, za čvor možete dobiti listu njegovih djece. Operacije kreiranja znode-a (create) i promjene vrijednosti (setData) su odvojene: samo postojeći ključevi se mogu čitati i mijenjati. Satovi se mogu priključ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 serveru. Efemerni čvorovi se koriste za otkrivanje kvarova. Oni su vezani za sesiju klijenta koji ih je kreirao. Kada klijent zatvori sesiju ili prestane da obaveštava ZooKeeper o njenom postojanju, ovi čvorovi se automatski brišu. Podržane su jednostavne transakcije - skup operacija koje ili uspijevaju ili ne uspijevaju ako to nije moguće za barem jednu od njih.

itdd

Pet studenata i tri distribuirane prodavnice ključ/vrijednost

Programeri ovog sistema su očigledno bili inspirisani ZooKeeperom, pa su stoga sve uradili 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 struktura može izgledati čudno, ali je zapravo vrlo ekspresivna i kroz nju se lako može oponašati hijerarhijski pogled.

etcd nema standardnu ​​operaciju poredi i postavljanja, ali ima nešto bolje: transakcije. Naravno, postoje u sva tri sistema, ali etcd transakcije su posebno dobre. Sastoje se od tri bloka: provjera, uspjeh, neuspjeh. Prvi blok sadrži skup uslova, drugi i treći - operacije. Transakcija se izvršava atomski. Ako su svi uslovi tačni, tada se izvršava blok uspjeha, u suprotnom 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 uslovne konstrukcije gotovo proizvoljnog nivoa ugniježđenja. Možete saznati više o tome od čega postoje provjere i operacije dokumentaciju.

Satovi postoje i ovdje, iako su malo složeniji i za višekratnu upotrebu. Odnosno, nakon instaliranja sata na raspon ključeva, primat ćete sva ažuriranja u ovom rasponu dok ne otkažete sat, a ne samo prvo. U etcd-u, analogni klijentskim sesijama ZooKeeper-a 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 sa navedenim prefiksom, odnosno raditi sa „podstablom“ ključa. Takvi upiti se nazivaju rekurzivni. Osim toga, Consul može odabrati samo ključeve koji ne sadrže navedeni znak iza prefiksa, što odgovara neposrednom dobivanju “djece”. Ali vrijedi zapamtiti da je to upravo izgled hijerarhijske strukture: sasvim je moguće kreirati ključ ako njegov roditelj ne postoji ili izbrisati ključ koji ima djecu, dok će djeca i dalje biti pohranjena u sistemu.

Pet studenata i tri distribuirane prodavnice ključ/vrijednost
Umjesto satova, Consul ima blokiranje HTTP zahtjeva. U suštini, to su obični pozivi metode čitanja podataka, za koje je, uz ostale parametre, naznačena posljednja poznata verzija podataka. Ako je trenutna verzija odgovarajućih podataka na serveru veća od navedene, odgovor se vraća odmah, u suprotnom - kada se vrijednost promijeni. Postoje i sesije koje se mogu priključiti ključevima u bilo koje vrijeme. Vrijedi napomenuti da za razliku od etcd-a i ZooKeepera, gdje brisanje sesija dovodi do brisanja pridruženih ključeva, postoji način u kojem se sesija jednostavno odvaja od njih. Dostupan transakcije, bez filijala, ali sa svim vrstama provjera.

Stavljajući sve zajedno

ZooKeeper ima najrigorozniji model podataka. Ekspresivni upiti raspona dostupni u etcd-u ne mogu se efikasno emulirati ni u ZooKeeperu ni u Consulu. Pokušavajući da inkorporiramo najbolje od svih usluga, na kraju smo dobili sučelje gotovo ekvivalentno sučelju ZooKeeper sa sljedećim značajnim izuzecima:

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

Odbijanje sekvencijalnih čvorova je zbog činjenice da etcd i Consul nemaju ugrađenu podršku za njih, te ih korisnik može lako implementirati na vrhu rezultujućeg interfejsa biblioteke.

Implementacija ponašanja sličnog ZooKeeper-u prilikom brisanja temena zahtijevalo bi održavanje zasebnog brojača djece za svaki ključ u etcd i Consul. Budući da smo pokušali izbjeći pohranjivanje meta informacija, odlučeno je da se izbriše cijelo podstablo.

Suptilnosti implementacije

Pogledajmo bliže neke aspekte implementacije bibliotečkog interfejsa u različitim sistemima.

Hijerarhija u itd

Ispostavilo se da je održavanje hijerarhijskog pogleda u etcd-u jedan od najzanimljivijih zadataka. Upiti opsega olakšavaju dohvat liste ključeva sa specificiranim prefiksom. Na primjer, ako vam treba sve što počinje "/foo", tražite raspon ["/foo", "/fop"). Ali ovo bi vratilo cijelo podstablo ključa, što možda neće biti prihvatljivo ako je podstablo veliko. U početku smo planirali da koristimo ključni mehanizam za prevođenje, implementiran u zetcd. To uključuje dodavanje jednog bajta na početak ključa, jednako dubini čvora u stablu. Dozvolite mi da vam dam primjer.

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

Zatim nabavite svu neposrednu djecu ključa "/foo" moguće zahtjevom za raspon ["u02/foo/", "u02/foo0"). Da, u ASCII "0" stoji odmah posle "/".

Ali kako implementirati uklanjanje vrha u ovom slučaju? Ispostavilo se da morate izbrisati sve opsege tog tipa ["uXX/foo/", "uXX/foo0") za XX od 01 do FF. A onda smo naleteli ograničenje broja operacija u okviru jedne transakcije.

Kao rezultat toga, izmišljen je jednostavan sistem konverzije ključeva, koji je omogućio efikasnu implementaciju i brisanja ključa i dobijanja liste djece. Dovoljno je dodati poseban znak prije posljednjeg tokena. 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 dobijanje 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 da izbrišemo ključ zajedno sa podstablom. Sta da radim? Ovo radimo sa optimizmom. Prvo, rekurzivno prelazimo kroz podstablo, dobijajući potomke svakog vrha sa posebnim upitom. Zatim gradimo transakciju koja pokušava izbrisati sve čvorove podstabla u ispravnom redoslijedu. Naravno, može doći do promjena između čitanja podstabla i brisanja. U tom slučaju transakcija neće uspjeti. Štaviše, podstablo se može promijeniti tokom procesa čitanja. Zahtjev za djecu sljedećeg čvora može vratiti grešku ako je, na primjer, ovaj čvor već obrisan. U oba slučaja ponavljamo cijeli postupak.

Ovaj pristup čini brisanje ključa vrlo neefikasnim ako ima potomke, a još više ako aplikacija nastavi raditi sa podstablom, brisanjem i kreiranjem ključeva. Međutim, to nam je omogućilo da izbjegnemo kompliciranje implementacije drugih metoda u etcd i Consul.

postavljeno u ZooKeeper

U ZooKeeper-u postoje odvojene metode koje rade sa strukturom stabla (create, delete, getChildren) i koje rade sa podacima u čvorovima (setData, getData).Štaviše, sve metode imaju stroge preduslove: create će vratiti grešku ako je čvor već imao je kreiran, izbrišite ili setData – ako već ne postoji. Trebao nam je set metod koji se može pozvati bez razmišljanja o prisutnosti ključa.

Jedna opcija je da se zauzme optimističan pristup, kao kod brisanja. Provjerite postoji li čvor. Ako postoji, pozovite setData, u suprotnom kreirajte. Ako je posljednja metoda vratila grešku, ponovite sve iznova. Prva stvar koju treba primijetiti je da je test postojanja besmislen. Možete odmah pozvati kreiranje. Uspješan završetak će značiti da čvor nije postojao i da je kreiran. U suprotnom, create će vratiti odgovarajuću grešku, nakon čega morate pozvati setData. Naravno, između poziva, vrh može biti obrisan konkurentskim pozivom, a setData bi također vratio grešku. U ovom slučaju možete sve ponoviti, ali da li se isplati?

Ako obje metode vrate grešku, tada sigurno znamo da je došlo do konkurentskog brisanja. Zamislimo da se ovo brisanje dogodilo nakon pozivanja skupa. Tada je ono 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 u stvari ništa nije napisano.

Više tehničkih detalja

U ovom dijelu ćemo se odmoriti od distribuiranih sistema i razgovarati o kodiranju.
Jedan od glavnih zahtjeva korisnika bio je višeplatforma: 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 započeli testiranje na drugim sistemima. 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 Windows-u.

Od samog početka trudili smo se da koristimo gotove biblioteke za pristup servisima. U slučaju ZooKeeper-a, izbor je pao ZooKeeper C++, koji se na kraju nije uspio kompajlirati na Windows-u. To, međutim, nije iznenađujuće: biblioteka je pozicionirana kao samo za linux. Za Konzula je jedina opcija bila ppconsul. Trebalo je tome dodati podršku sesije и transakcije. Za etcd nije pronađena punopravna biblioteka koja podržava najnoviju verziju protokola, pa smo jednostavno generirani grpc klijent.

Inspirisani asinhronim interfejsom ZooKeeper C++ biblioteke, odlučili smo da implementiramo i asinhroni interfejs. ZooKeeper C++ za ovo koristi future/promise primitive. U STL-u se, nažalost, implementiraju vrlo skromno. Na primjer, ne zatim metod, 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 biblioteke. Da bismo zaobišli ovaj problem, morali smo implementirati vlastiti jednostavan skup niti, budući da na zahtjev kupca nismo mogli koristiti teške biblioteke trećih strana kao što je Boost.

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

Koristili smo isti skup niti za izvršavanje upita za etcd i Consul. To znači da osnovnim bibliotekama može pristupiti više različitih niti. ppconsul nije siguran niti, tako da su pozivi na njega zaštićeni bravama.
Možete raditi sa grpc-om iz više niti, ali postoje suptilnosti. U etcd satovi se implementiraju preko grpc streamova. Ovo su dvosmjerni kanali za poruke određene vrste. Biblioteka kreira jednu nit za sve satove i jednu nit koja obrađuje dolazne poruke. Dakle, grpc zabranjuje paralelno upisivanje u tok. To znači da kada inicijalizirate ili brišete sat, morate pričekati dok prethodni zahtjev ne završi slanje prije nego što pošaljete sljedeći. Koristimo za sinhronizaciju uslovne varijable.

Rezultat

Pogledajte za sebe: liboffkv.

Naš tim: Raed Romanov, Ivan Glušenkov, Dmitry Kamaldinov, Victor Krapivensky, Vitalij Ivanin.

izvor: www.habr.com

Dodajte komentar