Pesë studentë dhe tre shpërndanë dyqane me vlerë kyçe

Ose si kemi shkruar një bibliotekë të klientit C++ për ZooKeeper, etcd dhe Consul KV

Në botën e sistemeve të shpërndara, ekzistojnë një sërë detyrash tipike: ruajtja e informacionit në lidhje me përbërjen e grupit, menaxhimi i konfigurimit të nyjeve, zbulimi i nyjeve të gabuara, zgjedhja e një drejtuesi. dhe të tjerët. Për zgjidhjen e këtyre problemeve, janë krijuar sisteme të veçanta të shpërndara - shërbimet e koordinimit. Tani do të na interesojnë tre prej tyre: ZooKeeper, etcd dhe Konsulli. Nga të gjitha funksionalitetet e pasura të Konsullit, ne do të fokusohemi në Konsullin KV.

Pesë studentë dhe tre shpërndanë dyqane me vlerë kyçe

Në thelb, të gjitha këto sisteme janë dyqane me vlera kyçe toleruese, linearizuese. Megjithëse modelet e tyre të të dhënave kanë ndryshime të konsiderueshme, për të cilat ne do t'i diskutojmë më vonë, ato zgjidhin të njëjtat probleme praktike. Natyrisht, çdo aplikacion që përdor shërbimin e koordinimit është i lidhur me njërin prej tyre, gjë që mund të çojë në nevojën për të mbështetur disa sisteme në një qendër të dhënash që zgjidhin të njëjtat probleme për aplikacione të ndryshme.

Ideja për të zgjidhur këtë problem lindi në një agjenci konsulente australiane dhe na ra në dorë neve, një ekipi i vogël studentësh, ta zbatonim atë, për të cilën do të flas.

Arritëm të krijonim një bibliotekë që ofron një ndërfaqe të përbashkët për të punuar me ZooKeeper, etcd dhe Consul KV. Biblioteka është shkruar në C++, por ka plane për ta transferuar atë në gjuhë të tjera.

Modele të të dhënave

Për të zhvilluar një ndërfaqe të përbashkët për tre sisteme të ndryshme, duhet të kuptoni se çfarë kanë të përbashkët dhe si ndryshojnë. Le ta kuptojmë.

Zookeeper

Pesë studentë dhe tre shpërndanë dyqane me vlerë kyçe

Çelësat janë të organizuar në një pemë dhe quhen nyje. Prandaj, për një nyje mund të merrni një listë të fëmijëve të saj. Veprimet e krijimit të një znode (krijoni) dhe ndryshimin e një vlere (setData) janë të ndara: vetëm çelësat ekzistues mund të lexohen dhe ndryshohen. Orët mund t'i bashkëngjiten operacioneve të kontrollit të ekzistencës së një nyje, leximit të një vlere dhe marrjes së fëmijëve. Watch është një aktivizues një herë që ndizet kur ndryshon versioni i të dhënave përkatëse në server. Nyjet kalimtare përdoren për të zbuluar dështimet. Ato janë të lidhura me seancën e klientit që i ka krijuar. Kur një klient mbyll një sesion ose ndalon së njoftuari ZooKeeper për ekzistencën e tij, këto nyje fshihen automatikisht. Mbështeten transaksione të thjeshta - një grup operacionesh që ose të gjitha kanë sukses ose dështojnë nëse kjo nuk është e mundur për të paktën njërin prej tyre.

etj

Pesë studentë dhe tre shpërndanë dyqane me vlerë kyçe

Zhvilluesit e këtij sistemi u frymëzuan qartë nga ZooKeeper, dhe për këtë arsye bënë gjithçka ndryshe. Nuk ka hierarki çelësash, por ata formojnë një grup të renditur leksikografikisht. Mund të merrni ose fshini të gjithë çelësat që i përkasin një diapazoni të caktuar. Kjo strukturë mund të duket e çuditshme, por në fakt është shumë ekspresive dhe një pamje hierarkike mund të imitohet lehtësisht përmes saj.

etcd nuk ka një operacion standard të krahasimit dhe vendosjes, por ka diçka më të mirë: transaksionet. Sigurisht, ato ekzistojnë në të tre sistemet, por transaksionet etcd janë veçanërisht të mira. Ato përbëhen nga tre blloqe: kontrolli, suksesi, dështimi. Blloku i parë përmban një sërë kushtesh, i dyti dhe i treti - operacionet. Transaksioni kryhet në mënyrë atomike. Nëse të gjitha kushtet janë të vërteta, atëherë ekzekutohet blloku i suksesit, përndryshe ekzekutohet blloku i dështimit. Në API 3.3, blloqet e suksesit dhe dështimit mund të përmbajnë transaksione të ndërlidhura. Kjo do të thotë, është e mundur që në mënyrë atomike të ekzekutohen konstruksione të kushtëzuara të nivelit thuajse arbitrar të foleve. Mund të mësoni më shumë se nga cilat kontrolle dhe operacione ekzistojnë dokumentacionin.

Orët ekzistojnë edhe këtu, megjithëse janë pak më të ndërlikuara dhe janë të ripërdorshme. Kjo do të thotë, pasi të keni instaluar një orë në një gamë kyçe, do të merrni të gjitha përditësimet në këtë gamë derisa të anuloni orën, dhe jo vetëm të parën. Në etjd, analoge e sesioneve të klientit ZooKeeper janë qiratë.

Konsulli K.V.

Nuk ka gjithashtu një strukturë të rreptë hierarkike këtu, por Konsulli mund të krijojë pamjen se ekziston: ju mund të merrni dhe fshini të gjithë çelësat me prefiksin e specifikuar, domethënë, të punoni me një "nënpemë" të çelësit. Pyetje të tilla quhen rekursive. Për më tepër, Konsulli mund të zgjedhë vetëm çelësat që nuk përmbajnë karakterin e specifikuar pas prefiksit, i cili korrespondon me marrjen e "fëmijëve" të menjëhershëm. Por ia vlen të kujtojmë se kjo është pikërisht pamja e një strukture hierarkike: është mjaft e mundur të krijosh një çelës nëse prindi i tij nuk ekziston ose të fshish një çelës që ka fëmijë, ndërsa fëmijët do të vazhdojnë të ruhen në sistem.

Pesë studentë dhe tre shpërndanë dyqane me vlerë kyçe
Në vend të orëve, Konsulli ka bllokuar kërkesat HTTP. Në thelb, këto janë thirrje të zakonshme në metodën e leximit të të dhënave, për të cilat, së bashku me parametrat e tjerë, tregohet versioni i fundit i njohur i të dhënave. Nëse versioni aktual i të dhënave përkatëse në server është më i madh se ai i specifikuar, përgjigja kthehet menjëherë, përndryshe - kur vlera ndryshon. Ka gjithashtu sesione që mund t'u bashkëngjiten çelësave në çdo kohë. Vlen të përmendet se ndryshe nga etcd dhe ZooKeeper, ku fshirja e seancave çon në fshirjen e çelësave të lidhur, ekziston një mënyrë në të cilën seanca thjesht shkëputet prej tyre. Në dispozicion transaksionet, pa degë, por me të gjitha llojet e kontrolleve.

Duke i bashkuar të gjitha

Zookeeper ka modelin më të rreptë të të dhënave. Pyetjet e intervalit ekspresiv të disponueshëm në ETCD nuk mund të emulohen në mënyrë efektive në Zookeeper ose Konsull. Duke u përpjekur të inkorporojmë më të mirën nga të gjitha shërbimet, ne përfunduam me një ndërfaqe pothuajse ekuivalente me ndërfaqen ZooKeeper me përjashtimet e mëposhtme të rëndësishme:

  • sekuenca, kontejneri dhe nyjet TTL nuk mbështetet
  • ACL-të nuk mbështeten
  • metoda set krijon një çelës nëse nuk ekziston (në ZK setData kthen një gabim në këtë rast)
  • Metodat set dhe cas janë të ndara (në ZK ato janë në thelb e njëjta gjë)
  • metoda e fshirjes fshin një nyje së bashku me nënpemën e saj (në ZK delete kthen një gabim nëse nyja ka fëmijë)
  • Për çdo çelës ekziston vetëm një version - versioni i vlerës (në ZK janë tre prej tyre)

Refuzimi i nyjeve sekuenciale është për shkak të faktit se etcd dhe Konsulli nuk kanë mbështetje të integruar për to, dhe ato mund të zbatohen lehtësisht nga përdoruesi në krye të ndërfaqes së bibliotekës që rezulton.

Zbatimi i sjelljes së ngjashme me ZooKeeper gjatë fshirjes së një kulmi do të kërkonte mbajtjen e një numëruesi të veçantë të fëmijëve për çdo çelës në etcd dhe Konsul. Meqenëse u përpoqëm të shmangnim ruajtjen e informacionit meta, u vendos që të fshihej e gjithë nënpema.

Hollësitë e zbatimit

Le të hedhim një vështrim më të afërt në disa aspekte të zbatimit të ndërfaqes së bibliotekës në sisteme të ndryshme.

Hierarkia në etj

Mbajtja e një pamje hierarkike në etjd doli të ishte një nga detyrat më interesante. Pyetjet e diapazonit e bëjnë të lehtë marrjen e një liste çelësash me një parashtesë të caktuar. Për shembull, nëse keni nevojë për gjithçka që fillon "/foo", ju po kërkoni një gamë ["/foo", "/fop"). Por kjo do të kthente të gjithë nënpemën e çelësit, e cila mund të mos jetë e pranueshme nëse nënpema është e madhe. Në fillim kemi planifikuar të përdorim një mekanizëm kyç përkthimi, zbatuar në zetcd. Ai përfshin shtimin e një bajt në fillim të çelësit, të barabartë me thellësinë e nyjes në pemë. Më lejoni t'ju jap një shembull.

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

Pastaj merrni të gjithë fëmijët e menjëhershëm të çelësit "/foo" e mundur duke kërkuar një gamë ["u02/foo/", "u02/foo0"). Po, në ASCII "0" qëndron menjëherë pas "/".

Por si të zbatohet heqja e një kulm në këtë rast? Rezulton se ju duhet të fshini të gjitha vargjet e llojit ["uXX/foo/", "uXX/foo0") për xx nga 01 në ff. Dhe pastaj u futëm në Kufiri i numrit të funksionimit brenda një transaksioni.

Si rezultat, u shpik një sistem i thjeshtë i konvertimit të çelësave, i cili bëri të mundur zbatimin në mënyrë efektive si fshirjen e një çelësi, ashtu edhe marrjen e një liste të fëmijëve. Mjafton të shtoni një karakter të veçantë përpara shenjës së fundit. Për shembull:

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

Pastaj duke fshirë çelësin "/very" kthehet në fshirje "/u00very" dhe varg ["/very/", "/very0"), dhe marrja e të gjithë fëmijëve - në një kërkesë për çelësa nga diapazoni ["/very/u00", "/very/u01").

Heqja e një çelësi në ZooKeeper

Siç e përmenda tashmë, në ZooKeeper nuk mund të fshish një nyje nëse ka fëmijë. Ne duam të fshijmë çelësin së bashku me nënpemën. Cfare duhet te bej? Ne e bëjmë këtë me optimizëm. Së pari, ne përshkojmë në mënyrë rekursive nënpemën, duke marrë fëmijët e secilës kulm me një pyetje të veçantë. Pastaj ne ndërtojmë një transaksion që përpiqet të fshijë të gjitha nyjet e nënpemës në rendin e duhur. Sigurisht, ndryshimet mund të ndodhin midis leximit të një nënpeme dhe fshirjes së saj. Në këtë rast, transaksioni do të dështojë. Për më tepër, nënpema mund të ndryshojë gjatë procesit të leximit. Një kërkesë për fëmijët e nyjës tjetër mund të kthejë një gabim nëse, për shembull, kjo nyje tashmë është fshirë. Në të dyja rastet, ne e përsërisim të gjithë procesin përsëri.

Kjo qasje e bën fshirjen e një çelësi shumë të paefektshëm nëse ka fëmijë, dhe aq më tepër nëse aplikacioni vazhdon të punojë me nënpemën, duke fshirë dhe krijuar çelësa. Megjithatë, kjo na lejoi të shmangnim ndërlikimin e zbatimit të metodave të tjera në etjd dhe Konsull.

vendosur në ZooKeeper

Në ZooKeeper ka metoda të veçanta që punojnë me strukturën e pemës (krijoni, fshini, merrniChildren) dhe që punojnë me të dhëna në nyje (setData, getData).Për më tepër, të gjitha metodat kanë parakushte strikte: krijimi do të kthejë një gabim nëse nyja tashmë ka është krijuar, fshirë ose vendosurData – nëse nuk ekziston tashmë. Na duhej një metodë e caktuar që mund të thirret pa menduar për praninë e një çelësi.

Një opsion është të keni një qasje optimiste, si me fshirjen. Kontrolloni nëse ekziston një nyje. Nëse ekziston, thirrni setData, përndryshe krijoni. Nëse metoda e fundit ktheu një gabim, përsërisni përsëri. Gjëja e parë që duhet theksuar është se testi i ekzistencës është i pakuptimtë. Mund të telefononi menjëherë krijimin. Përfundimi i suksesshëm do të thotë që nyja nuk ekzistonte dhe ajo u krijua. Përndryshe, krijimi do të kthejë gabimin e duhur, pas së cilës duhet të telefononi setData. Sigurisht, midis thirrjeve, një kulm mund të fshihet nga një thirrje konkurruese dhe setData gjithashtu do të kthente një gabim. Në këtë rast, ju mund ta bëni përsëri të gjithë, por a ia vlen?

Nëse të dyja metodat kthejnë një gabim, atëherë ne e dimë me siguri se ka ndodhur një fshirje konkurruese. Le të imagjinojmë se kjo fshirje ka ndodhur pas thirrjes set. Atëherë çfarëdo kuptimi që po përpiqemi të krijojmë është fshirë tashmë. Kjo do të thotë që mund të supozojmë se grupi është ekzekutuar me sukses, edhe nëse në fakt asgjë nuk është shkruar.

Më shumë detaje teknike

Në këtë pjesë do të bëjmë një pushim nga sistemet e shpërndara dhe do të flasim për kodimin.
Një nga kërkesat kryesore të klientit ishte ndër-platforma: të paktën një nga shërbimet duhet të mbështetet në Linux, MacOS dhe Windows. Fillimisht, ne u zhvilluam vetëm për Linux, dhe filluam testimin në sisteme të tjera më vonë. Kjo shkaktoi shumë probleme, të cilat për disa kohë ishin krejtësisht të paqarta se si të afroheshin. Si rezultat, të tre shërbimet e koordinimit tani mbështeten në Linux dhe MacOS, ndërsa vetëm Consul KV mbështetet në Windows.

Që në fillim u përpoqëm të përdornim biblioteka të gatshme për të aksesuar shërbimet. Në rastin e ZooKeeper, zgjedhja ra ZooKeeper C++, e cila përfundimisht dështoi të përpilohej në Windows. Megjithatë, kjo nuk është për t'u habitur: biblioteka pozicionohet vetëm si linux. Për Konsullin e vetmja mundësi ishte ppkonsull. Mbështetja duhej të shtohej në të seancat и transaksionet. Për etcd, një bibliotekë e plotë që mbështet versionin më të fundit të protokollit nuk u gjet, kështu që ne thjesht Klienti i gjeneruar GRPC.

Të frymëzuar nga ndërfaqja asinkrone e bibliotekës ZooKeeper C++, vendosëm të implementojmë gjithashtu një ndërfaqe asinkrone. ZooKeeper C++ përdor primitivët e së ardhmes/premtimit për këtë. Në STL, për fat të keq, ato zbatohen në mënyrë shumë modeste. Për shembull, jo pastaj metodë, i cili zbaton funksionin e kaluar në rezultatin e së ardhmes kur ai të bëhet i disponueshëm. Në rastin tonë, një metodë e tillë është e nevojshme për të kthyer rezultatin në formatin e bibliotekës sonë. Për të kapërcyer këtë problem, na u desh të zbatonim grupin tonë të thjeshtë të fijeve, pasi me kërkesë të klientit nuk mund të përdornim biblioteka të rënda të palëve të treta si Boost.

Zbatimi ynë i atëhershëm funksionon kështu. Kur thirret, krijohet një çift shtesë premtimi/e ardhmja. E ardhmja e re kthehet dhe e kaluara vendoset së bashku me funksionin përkatës dhe një premtim shtesë në radhë. Një fill nga grupi zgjedh disa të ardhme nga radha dhe i anketon ato duke përdorur wait_for. Kur një rezultat bëhet i disponueshëm, thirret funksioni përkatës dhe vlera e tij e kthimit i kalohet premtimit.

Ne përdorëm të njëjtin grup thread për të ekzekutuar pyetje për etcd dhe Consul. Kjo do të thotë që bibliotekat themelore mund të aksesohen nga fije të shumta të ndryshme. ppconsul nuk është i sigurt me fije, kështu që thirrjet në të mbrohen me bravë.
Ju mund të punoni me grpc nga fije të shumta, por ka hollësi. Në etcd orët zbatohen nëpërmjet rrymave grpc. Këto janë kanale dydrejtimëshe për mesazhe të një lloji të caktuar. Biblioteka krijon një fije të vetme për të gjitha orët dhe një fije të vetme që përpunon mesazhet hyrëse. Pra, grpc ndalon shkrimet paralele në transmetim. Kjo do të thotë që kur inicializoni ose fshini një orë, duhet të prisni derisa kërkesa e mëparshme të përfundojë dërgimin përpara se të dërgoni një tjetër. Ne përdorim për sinkronizim variablat e kushtëzuar.

Total

Shihni vetë: libofkv.

Ekipi ynë: Raed Romanov, Ivan Glushenkov, Dmitry kamaldinov, Victor Krapivensky, Vitali Ivanin.

Burimi: www.habr.com

Shto një koment