Pet študentov in tri porazdeljene shrambe ključev in vrednosti

Ali kako smo napisali odjemalsko knjižnico C++ za ZooKeeper itd. in Consul KV

V svetu porazdeljenih sistemov obstaja vrsta tipičnih nalog: shranjevanje informacij o sestavi gruče, upravljanje konfiguracije vozlišč, odkrivanje okvarjenih vozlišč, izbira vodje in drugi. Za rešitev teh težav so bili ustvarjeni posebni porazdeljeni sistemi - koordinacijske storitve. Zdaj nas bodo zanimali trije: ZooKeeper, etcd in Consul. Od vse bogate funkcionalnosti Consula se bomo osredotočili na Consul KV.

Pet študentov in tri porazdeljene shrambe ključev in vrednosti

V bistvu so vsi ti sistemi odporni na napake in jih je mogoče linearizirati. Čeprav imajo njihovi podatkovni modeli pomembne razlike, o katerih bomo razpravljali kasneje, rešujejo enake praktične težave. Očitno je vsaka aplikacija, ki uporablja koordinacijsko storitev, vezana na eno od njih, kar lahko povzroči potrebo po podpori več sistemov v enem podatkovnem centru, ki rešujejo iste probleme za različne aplikacije.

Ideja za rešitev tega problema je nastala v avstralski svetovalni agenciji, izvedba pa je padla na nas, majhno ekipo študentov, o čemer bom govoril.

Uspelo nam je ustvariti knjižnico, ki ponuja skupen vmesnik za delo z ZooKeeper, etcd in Consul KV. Knjižnica je napisana v C++, vendar obstajajo načrti za prenos v druge jezike.

Podatkovni modeli

Če želite razviti skupni vmesnik za tri različne sisteme, morate razumeti, kaj imajo skupnega in v čem se razlikujejo. Ugotovimo.

Oskrbnik živalskega vrta

Pet študentov in tri porazdeljene shrambe ključev in vrednosti

Ključi so organizirani v drevo in se imenujejo vozlišča. V skladu s tem lahko za vozlišče dobite seznam njegovih otrok. Operaciji ustvarjanja znode (create) in spreminjanja vrednosti (setData) sta ločeni: brati in spreminjati je mogoče samo obstoječe ključe. Operacijama preverjanja obstoja vozlišča, branja vrednosti in pridobivanja otrok lahko priključite ure. Watch je enkratni sprožilec, ki se sproži, ko se spremeni različica ustreznih podatkov na strežniku. Efemerna vozlišča se uporabljajo za odkrivanje napak. Vezani so na sejo odjemalca, ki jih je ustvaril. Ko odjemalec zapre sejo ali preneha obveščati ZooKeeper o njenem obstoju, se ta vozlišča samodejno izbrišejo. Podprte so preproste transakcije – nabor operacij, ki bodisi vse uspejo bodisi ne uspejo, če to ni mogoče za vsaj eno od njih.

itdd

Pet študentov in tri porazdeljene shrambe ključev in vrednosti

Razvijalci tega sistema so se očitno zgledovali po ZooKeeperju, zato so vse naredili drugače. Ni hierarhije ključev, ampak tvorijo leksikografsko urejen niz. Lahko dobite ali izbrišete vse ključe, ki pripadajo določenemu obsegu. Ta struktura se morda zdi nenavadna, vendar je v resnici zelo ekspresivna in prek nje je mogoče zlahka posnemati hierarhični pogled.

etcd nima standardne operacije primerjaj in nastavi, ima pa nekaj boljšega: transakcije. Seveda obstajajo v vseh treh sistemih, vendar so transakcije etcd še posebej dobre. Sestavljeni so iz treh blokov: preverjanje, uspeh, neuspeh. Prvi blok vsebuje nabor pogojev, drugi in tretji pa operacije. Transakcija se izvede atomsko. Če so vsi pogoji izpolnjeni, se izvede uspešni blok, v nasprotnem primeru se izvede neuspešni blok. V API-ju 3.3 lahko bloki uspeha in neuspeha vsebujejo ugnezdene transakcije. To pomeni, da je mogoče atomsko izvesti pogojne konstrukte skoraj poljubne ravni gnezdenja. Izvedete lahko več o tem, kaj obstajajo pregledi in operacije dokumentacijo.

Ure obstajajo tudi pri nas, čeprav so malo bolj zapletene in jih je mogoče ponovno uporabiti. To pomeni, da boste po namestitvi ure na ključni obseg prejeli vse posodobitve v tem obsegu, dokler ne prekličete ure, in ne le prve. V etcd so analogne odjemalske seje ZooKeeper zakupi.

Konzul K.V.

Tukaj tudi ni stroge hierarhične strukture, vendar lahko Consul ustvari videz, da obstaja: lahko dobite in izbrišete vse ključe z določeno predpono, to je delo s "poddrevesom" ključa. Take poizvedbe imenujemo rekurzivne. Poleg tega lahko Consul izbere samo ključe, ki ne vsebujejo določenega znaka za predpono, kar ustreza pridobivanju takojšnjih "otrokov". Vendar je vredno zapomniti, da je to ravno videz hierarhične strukture: povsem mogoče je ustvariti ključ, če njegov starš ne obstaja, ali izbrisati ključ, ki ima otroke, medtem ko bodo otroci še naprej shranjeni v sistemu.

Pet študentov in tri porazdeljene shrambe ključev in vrednosti
Namesto ur ima Consul blokiranje zahtev HTTP. V bistvu so to običajni klici metode branja podatkov, za katere je poleg ostalih parametrov navedena zadnja znana različica podatkov. Če je trenutna različica ustreznih podatkov na strežniku večja od navedene, se odgovor vrne takoj, drugače - ko se vrednost spremeni. Obstajajo tudi seje, ki jih je mogoče kadar koli pripeti na ključe. Treba je omeniti, da za razliko od etcd in ZooKeeper, kjer brisanje sej povzroči izbris povezanih ključev, obstaja način, v katerem seja preprosto prekine povezavo z njimi. Na voljo transakcije, brez podružnic, vendar z vsemi vrstami pregledov.

Vse skupaj

ZooKeeper ima najstrožji podatkovni model. Izrazitih poizvedb obsega, ki so na voljo v etcd, ni mogoče učinkovito posnemati niti v ZooKeeperju niti v Consulu. Ker smo poskušali vključiti najboljše iz vseh storitev, smo na koncu dobili vmesnik, ki je skoraj enakovreden vmesniku ZooKeeper z naslednjimi pomembnimi izjemami:

  • zaporedje, vsebnik in vozlišča TTL ne podpira
  • ACL-ji niso podprti
  • metoda set ustvari ključ, če ta ne obstaja (v ZK setData v tem primeru vrne napako)
  • metodi set in cas sta ločeni (v ZK sta v bistvu ista stvar)
  • metoda brisanja izbriše vozlišče skupaj z njegovim poddrevesom (v ZK delete vrne napako, če ima vozlišče otroke)
  • Za vsak ključ obstaja le ena različica - vrednostna (v ZK trije so)

Zavrnitev zaporednih vozlišč je posledica dejstva, da etcd in Consul nimata vgrajene podpore zanju in ju lahko uporabnik enostavno implementira na vrhu nastalega knjižničnega vmesnika.

Izvajanje vedenja, podobnega ZooKeeperju, pri brisanju vozlišča bi zahtevalo vzdrževanje ločenega podrejenega števca za vsak ključ v etcd in Consul. Ker smo se poskušali izogniti shranjevanju meta informacij, smo se odločili, da izbrišemo celotno poddrevo.

Tankosti izvedbe

Oglejmo si podrobneje nekatere vidike implementacije knjižničnega vmesnika v različne sisteme.

Hierarhija v itd

Vzdrževanje hierarhičnega pogleda v etcd se je izkazalo za eno najzanimivejših nalog. Poizvedbe obsega olajšajo pridobivanje seznama ključev z določeno predpono. Na primer, če potrebujete vse, kar se začne "/foo", sprašujete po obsegu ["/foo", "/fop"). Toda to bi vrnilo celotno poddrevo ključa, kar morda ni sprejemljivo, če je poddrevo veliko. Sprva smo načrtovali uporabo ključnega prevajalskega mehanizma, implementirano v zetcd. Vključuje dodajanje enega bajta na začetku ključa, ki je enak globini vozlišča v drevesu. Naj vam povem primer.

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

Nato pridobite vse neposredne potomce ključa "/foo" možno z zahtevo po obsegu ["u02/foo/", "u02/foo0"). Da, v ASCII "0" stoji takoj za "/".

Toda kako v tem primeru izvesti odstranitev vrha? Izkazalo se je, da morate izbrisati vse obsege vrste ["uXX/foo/", "uXX/foo0") za XX od 01 do FF. In potem smo naleteli omejitev števila operacij znotraj ene transakcije.

Posledično je bil izumljen preprost sistem pretvorbe ključev, ki je omogočil učinkovito izvedbo tako brisanja ključa kot pridobivanja seznama otrok. Dovolj je dodati poseben znak pred zadnji žeton. Na primer:

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

Nato brisanje ključa "/very" spremeni v brisanje "/u00very" in obseg ["/very/", "/very0"), in pridobivanje vseh otrok - v zahtevi za ključe iz območja ["/very/u00", "/very/u01").

Odstranitev ključa v ZooKeeper

Kot sem že omenil, v ZooKeeperju ne morete izbrisati vozlišča, če ima podrejene. Ključ želimo izbrisati skupaj s poddrevesom. Kaj naj naredim? To delamo z optimizmom. Najprej rekurzivno prečkamo poddrevo in pridobimo otroke vsakega vozlišča z ločeno poizvedbo. Nato zgradimo transakcijo, ki poskuša izbrisati vsa vozlišča poddrevesa v pravilnem vrstnem redu. Seveda lahko pride do sprememb med branjem poddrevesa in njegovim brisanjem. V tem primeru transakcija ne bo uspela. Poleg tega se lahko poddrevo spremeni med procesom branja. Zahteva za otroke naslednjega vozlišča lahko vrne napako, če je na primer to vozlišče že izbrisano. V obeh primerih ponovimo celoten postopek.

Zaradi tega pristopa je brisanje ključa zelo neučinkovito, če ima podrejene, še bolj pa, če aplikacija še naprej deluje s poddrevesom, briše in ustvarja ključe. Vendar nam je to omogočilo, da se izognemo zapletu pri implementaciji drugih metod v etcd in Consul.

nastavite v ZooKeeperju

V ZooKeeperju obstajajo ločene metode, ki delujejo z drevesno strukturo (create, delete, getChildren) in ki delajo s podatki v vozliščih (setData, getData). Poleg tega imajo vse metode stroge predpogoje: create bo vrnil napako, če je vozlišče že je bil ustvarjen, izbrišite ali nastaviteData – če še ne obstaja. Potrebovali smo nastavljeno metodo, ki jo je mogoče poklicati, ne da bi razmišljali o prisotnosti ključa.

Ena možnost je optimističen pristop, kot pri brisanju. Preverite, ali vozlišče obstaja. Če obstaja, pokličite setData, sicer ustvarite. Če je zadnja metoda vrnila napako, jo ponovite znova. Najprej je treba opozoriti, da je preizkus obstoja nesmiseln. Takoj lahko pokličete create. Uspešen zaključek bo pomenil, da vozlišče ni obstajalo in je bilo ustvarjeno. V nasprotnem primeru bo create vrnil ustrezno napako, po kateri morate poklicati setData. Seveda bi lahko med klici točko izbrisal konkurenčni klic in setData bi prav tako vrnil napako. V tem primeru lahko vse ponovite znova, a ali je vredno?

Če obe metodi vrneta napako, potem zagotovo vemo, da je prišlo do konkurenčnega izbrisa. Predstavljajmo si, da je do tega izbrisa prišlo po klicu set. Potem je kateri koli pomen, ki ga poskušamo vzpostaviti, že izbrisan. To pomeni, da lahko domnevamo, da je bil niz uspešno izveden, tudi če v resnici ni bilo nič zapisanega.

Več tehničnih podrobnosti

V tem razdelku si bomo vzeli odmor od porazdeljenih sistemov in govorili o kodiranju.
Ena glavnih zahtev naročnika je bila medplatformnost: vsaj ena od storitev mora biti podprta v sistemih Linux, MacOS in Windows. Sprva smo razvijali samo za Linux, kasneje pa smo začeli testirati na drugih sistemih. To je povzročilo nemalo težav, do katerih je bilo nekaj časa popolnoma nejasno, kako pristopiti. Posledično so vse tri koordinacijske storitve zdaj podprte v sistemih Linux in MacOS, medtem ko je v sistemu Windows podprt samo Consul KV.

Že od vsega začetka smo poskušali za dostop do storitev uporabljati že pripravljene knjižnice. V primeru ZooKeeperja je izbira padla ZooKeeper C++, ki se nazadnje ni uspel prevesti v sistemu Windows. To pa ni presenetljivo: knjižnica je postavljena samo za linux. Za konzula je bila edina možnost ppconsul. Temu je bilo treba dodati podporo sej и transakcije. Za etcd ni bilo mogoče najti polne knjižnice, ki bi podpirala najnovejšo različico protokola, zato smo preprosto ustvarjen odjemalec grpc.

Po navdihu asinhronega vmesnika knjižnice ZooKeeper C++ smo se odločili implementirati tudi asinhroni vmesnik. ZooKeeper C++ za to uporablja primitive future/promise. V STL so žal implementirani zelo skromno. Na primer, št nato metoda, ki preneseno funkcijo uporabi za rezultat v prihodnosti, ko bo na voljo. V našem primeru je taka metoda potrebna za pretvorbo rezultata v format naše knjižnice. Da bi se izognili tej težavi, smo morali implementirati lastno preprosto skupino niti, saj na zahtevo stranke nismo mogli uporabiti težkih knjižnic tretjih oseb, kot je Boost.

Naša takratna implementacija deluje takole. Ob klicu se ustvari dodatni par obljuba/prihodnost. Nova prihodnost je vrnjena, prestala pa je skupaj z ustrezno funkcijo in dodatno obljubo postavljena v čakalno vrsto. Nit iz skupine izbere več terminskih pogodb iz čakalne vrste in jih anketira z uporabo wait_for. Ko je rezultat na voljo, se pokliče ustrezna funkcija in njena vrnjena vrednost se posreduje obljubi.

Uporabili smo isto skupino niti za izvajanje poizvedb za etcd in Consul. To pomeni, da lahko do temeljnih knjižnic dostopa več različnih niti. ppconsul ni varen za niti, zato so klici nanj zaščiteni s ključavnicami.
Z grpc lahko delate iz več niti, vendar obstajajo podrobnosti. V etcd so ure implementirane prek tokov grpc. To so dvosmerni kanali za sporočila določene vrste. Knjižnica ustvari eno nit za vse oglede in eno nit, ki obdeluje dohodna sporočila. Torej grpc prepoveduje vzporedno pisanje v tok. To pomeni, da morate pri inicializaciji ali brisanju ure počakati, da prejšnja zahteva zaključi pošiljanje, preden pošljete naslednjo. Uporabljamo za sinhronizacijo pogojne spremenljivke.

Skupaj

Prepričajte se sami: liboffkv.

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

Vir: www.habr.com

Dodaj komentar