Limang mag-aaral at tatlong ibinahagi na key-value store

O kung paano kami nagsulat ng isang client C++ library para sa ZooKeeper, etcd at Consul KV

Sa mundo ng mga distributed system, mayroong isang bilang ng mga tipikal na gawain: pag-iimbak ng impormasyon tungkol sa komposisyon ng kumpol, pamamahala sa pagsasaayos ng mga node, pag-detect ng mga maling node, pagpili ng pinuno at iba pa. Upang malutas ang mga problemang ito, nilikha ang mga espesyal na ipinamamahaging sistema - mga serbisyo ng koordinasyon. Ngayon kami ay magiging interesado sa tatlo sa kanila: ZooKeeper, etcd at Consul. Sa lahat ng mayamang functionality ng Consul, tututukan namin ang Consul KV.

Limang mag-aaral at tatlong ibinahagi na key-value store

Sa esensya, lahat ng system na ito ay fault-tolerant, linearizable key-value store. Kahit na ang kanilang mga modelo ng data ay may makabuluhang pagkakaiba, na tatalakayin natin sa ibang pagkakataon, nalulutas nila ang parehong mga praktikal na problema. Malinaw, ang bawat application na gumagamit ng serbisyo ng koordinasyon ay nakatali sa isa sa mga ito, na maaaring humantong sa pangangailangan na suportahan ang ilang mga system sa isang data center na lumulutas sa parehong mga problema para sa iba't ibang mga application.

Ang ideya na lutasin ang problemang ito ay nagmula sa isang ahensya ng pagkonsulta sa Australia, at napunta sa amin, isang maliit na pangkat ng mga mag-aaral, na ipatupad ito, na siyang pag-uusapan ko.

Nakagawa kami ng library na nagbibigay ng karaniwang interface para sa pakikipagtulungan sa ZooKeeper, etcd at Consul KV. Ang library ay nakasulat sa C++, ngunit may mga planong i-port ito sa ibang mga wika.

Mga Modelo ng Data

Upang bumuo ng isang karaniwang interface para sa tatlong magkakaibang mga system, kailangan mong maunawaan kung ano ang mayroon sila sa karaniwan at kung paano sila naiiba. Alamin natin ito.

ZooKeeper

Limang mag-aaral at tatlong ibinahagi na key-value store

Ang mga susi ay nakaayos sa isang puno at tinatawag na mga node. Alinsunod dito, para sa isang node maaari kang makakuha ng isang listahan ng mga anak nito. Ang mga operasyon ng paglikha ng isang znode (lumikha) at pagpapalit ng isang halaga (setData) ay pinaghihiwalay: tanging ang mga umiiral na key lamang ang maaaring basahin at baguhin. Ang mga relo ay maaaring ilakip sa mga operasyon ng pagsuri sa pagkakaroon ng isang node, pagbabasa ng isang halaga, at pagkuha ng mga bata. Ang panonood ay isang beses na trigger na gagana kapag nagbago ang bersyon ng kaukulang data sa server. Ang mga ephemeral node ay ginagamit upang makita ang mga pagkabigo. Nakatali sila sa session ng kliyente na lumikha sa kanila. Kapag ang isang kliyente ay nagsara ng isang session o huminto sa pag-abiso sa ZooKeeper ng pagkakaroon nito, ang mga node na ito ay awtomatikong tatanggalin. Sinusuportahan ang mga simpleng transaksyon - isang hanay ng mga operasyon na magtagumpay o mabibigo ang lahat kung hindi ito posible para sa kahit isa sa mga ito.

atbp

Limang mag-aaral at tatlong ibinahagi na key-value store

Ang mga nag-develop ng system na ito ay malinaw na inspirasyon ng ZooKeeper, at samakatuwid ay ginawa ang lahat nang iba. Walang hierarchy ng mga susi, ngunit bumubuo sila ng isang set na nakaayos ayon sa leksikograpiko. Maaari mong makuha o tanggalin ang lahat ng mga key na kabilang sa isang partikular na hanay. Ang istraktura na ito ay maaaring mukhang kakaiba, ngunit ito ay talagang napaka-nagpapahayag, at isang hierarchical view ay madaling tularan sa pamamagitan nito.

etcd ay walang karaniwang paghahambing-at-set na operasyon, ngunit mayroon itong mas mahusay: mga transaksyon. Siyempre, umiiral ang mga ito sa lahat ng tatlong mga sistema, ngunit ang mga transaksyon sa etcd ay lalong mabuti. Binubuo sila ng tatlong bloke: tseke, tagumpay, kabiguan. Ang unang bloke ay naglalaman ng isang hanay ng mga kundisyon, ang pangalawa at pangatlo - mga operasyon. Ang transaksyon ay isinasagawa nang atomically. Kung ang lahat ng mga kundisyon ay totoo, kung gayon ang bloke ng tagumpay ay isasagawa, kung hindi, ang bloke ng kabiguan ay ipapatupad. Sa API 3.3, ang mga bloke ng tagumpay at kabiguan ay maaaring maglaman ng mga nested na transaksyon. Iyon ay, posible na atomically execute conditional constructs ng halos di-makatwirang antas ng nesting. Maaari kang matuto nang higit pa tungkol sa kung saan umiiral ang mga pagsusuri at pagpapatakbo dokumentasyon.

Ang mga relo ay umiiral din dito, kahit na ang mga ito ay medyo mas kumplikado at magagamit muli. Ibig sabihin, pagkatapos mag-install ng relo sa isang key range, matatanggap mo ang lahat ng update sa range na ito hanggang sa kanselahin mo ang relo, at hindi lang ang una. Sa etcd, ang analogue ng mga session ng kliyente ng ZooKeeper ay mga lease.

Consul K.V.

Wala ring mahigpit na hierarchical na istraktura dito, ngunit ang Consul ay maaaring lumikha ng hitsura na mayroon ito: maaari mong makuha at tanggalin ang lahat ng mga susi na may tinukoy na prefix, iyon ay, gumana sa "subtree" ng susi. Ang mga naturang query ay tinatawag na recursive. Bilang karagdagan, ang Consul ay maaaring pumili lamang ng mga susi na hindi naglalaman ng tinukoy na karakter pagkatapos ng prefix, na tumutugma sa pagkuha ng agarang "mga bata". Ngunit ito ay nagkakahalaga ng pag-alala na ito ay tiyak na hitsura ng isang hierarchical na istraktura: posible na lumikha ng isang susi kung ang magulang nito ay hindi umiiral o tanggalin ang isang susi na may mga anak, habang ang mga bata ay patuloy na maiimbak sa system.

Limang mag-aaral at tatlong ibinahagi na key-value store
Sa halip na mga relo, hinaharangan ng Consul ang mga kahilingan sa HTTP. Sa esensya, ito ay mga ordinaryong tawag sa paraan ng pagbabasa ng data, kung saan, kasama ng iba pang mga parameter, ang huling kilalang bersyon ng data ay ipinahiwatig. Kung ang kasalukuyang bersyon ng kaukulang data sa server ay mas malaki kaysa sa tinukoy, ang tugon ay ibabalik kaagad, kung hindi - kapag nagbago ang halaga. Mayroon ding mga session na maaaring ikabit sa mga susi anumang oras. Kapansin-pansin na hindi tulad ng etcd at ZooKeeper, kung saan ang pagtanggal ng mga session ay humahantong sa pagtanggal ng mga nauugnay na key, mayroong isang mode kung saan ang session ay na-unlink lamang mula sa kanila. Available mga transaksyon, walang mga sangay, ngunit may lahat ng uri ng mga tseke.

Pinagsasama-sama ang lahat

Ang ZooKeeper ay may pinaka mahigpit na modelo ng data. Ang mga nagpapahayag na hanay ng mga query na available sa etcd ay hindi maaaring epektibong tularan sa alinman sa ZooKeeper o Consul. Sinusubukang isama ang pinakamahusay mula sa lahat ng mga serbisyo, napunta kami sa isang interface na halos katumbas ng interface ng ZooKeeper na may mga sumusunod na makabuluhang pagbubukod:

  • sequence, container at TTL node Hindi suportado
  • Ang mga ACL ay hindi suportado
  • ang set na paraan ay lumilikha ng isang susi kung wala ito (sa ZK setData ay nagbabalik ng isang error sa kasong ito)
  • set at cas na mga pamamaraan ay pinaghiwalay (sa ZK sila ay mahalagang parehong bagay)
  • ang paraan ng pagbubura ay nagtatanggal ng isang node kasama ang subtree nito (sa ZK delete ay nagbabalik ng isang error kung ang node ay may mga anak)
  • Para sa bawat key mayroon lamang isang bersyon - ang bersyon ng halaga (sa ZK tatlo sila)

Ang pagtanggi sa mga sunud-sunod na node ay dahil sa ang katunayan na ang etcd at Consul ay walang built-in na suporta para sa kanila, at madali silang maipatupad ng user sa ibabaw ng nagresultang interface ng library.

Ang pagpapatupad ng gawi na katulad ng ZooKeeper kapag nagde-delete ng vertex ay mangangailangan ng pagpapanatili ng hiwalay na child counter para sa bawat key sa etcd at Consul. Dahil sinubukan naming iwasang mag-imbak ng meta information, napagpasyahan na tanggalin ang buong subtree.

Mga subtleties ng pagpapatupad

Tingnan natin ang ilang aspeto ng pagpapatupad ng interface ng library sa iba't ibang system.

Hierarchy sa etcd

Ang pagpapanatili ng isang hierarchical view sa etcd ay naging isa sa mga pinaka-kagiliw-giliw na gawain. Pinapadali ng mga query sa hanay ang pagkuha ng listahan ng mga key na may tinukoy na prefix. Halimbawa, kung kailangan mo ang lahat na nagsisimula sa "/foo", humihingi ka ng range ["/foo", "/fop"). Ngunit ibabalik nito ang buong subtree ng susi, na maaaring hindi katanggap-tanggap kung malaki ang subtree. Noong una ay binalak naming gumamit ng isang pangunahing mekanismo ng pagsasalin, ipinatupad sa zetcd. Kabilang dito ang pagdaragdag ng isang byte sa simula ng key, katumbas ng lalim ng node sa puno. Bigyan kita ng isang halimbawa.

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

Pagkatapos ay kunin ang lahat ng agarang anak ng susi "/foo" posible sa pamamagitan ng paghiling ng hanay ["u02/foo/", "u02/foo0"). Oo, sa ASCII "0" nakatayo pagkatapos "/".

Ngunit paano ipatupad ang pag-alis ng isang vertex sa kasong ito? Lumalabas na kailangan mong tanggalin ang lahat ng mga saklaw ng uri ["uXX/foo/", "uXX/foo0") para sa XX mula 01 hanggang FF. At saka kami nasagasaan limitasyon ng numero ng operasyon sa loob ng isang transaksyon.

Bilang resulta, naimbento ang isang simpleng key conversion system, na naging posible upang epektibong ipatupad ang parehong pagtanggal ng susi at pagkuha ng listahan ng mga bata. Ito ay sapat na upang magdagdag ng isang espesyal na character bago ang huling token. Halimbawa:

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

Pagkatapos ay tanggalin ang susi "/very" nagiging pagtanggal "/u00very" at saklaw ["/very/", "/very0"), at pagkuha ng lahat ng bata - sa isang kahilingan para sa mga susi mula sa hanay ["/very/u00", "/very/u01").

Pag-alis ng susi sa ZooKeeper

Tulad ng nabanggit ko na, sa ZooKeeper hindi mo matatanggal ang isang node kung mayroon itong mga anak. Gusto naming tanggalin ang susi kasama ang subtree. Anong gagawin ko? Ginagawa namin ito nang may optimismo. Una, paulit-ulit naming binabagtas ang subtree, na kinukuha ang mga bata ng bawat vertex na may hiwalay na query. Pagkatapos ay bumuo kami ng isang transaksyon na sumusubok na tanggalin ang lahat ng mga node ng subtree sa tamang pagkakasunud-sunod. Siyempre, maaaring mangyari ang mga pagbabago sa pagitan ng pagbabasa ng subtree at pagtanggal nito. Sa kasong ito, mabibigo ang transaksyon. Bukod dito, maaaring magbago ang subtree sa panahon ng proseso ng pagbabasa. Ang isang kahilingan para sa mga anak ng susunod na node ay maaaring magbalik ng error kung, halimbawa, ang node na ito ay natanggal na. Sa parehong mga kaso, inuulit namin muli ang buong proseso.

Ang diskarte na ito ay ginagawang hindi epektibo ang pagtanggal ng isang susi kung ito ay may mga anak, at higit pa kung ang application ay patuloy na gumagana sa subtree, pagtanggal at paglikha ng mga susi. Gayunpaman, ito ay nagbigay-daan sa amin upang maiwasan na kumplikado ang pagpapatupad ng iba pang mga pamamaraan sa etcd at Consul.

itinakda sa ZooKeeper

Sa ZooKeeper mayroong mga hiwalay na pamamaraan na gumagana sa istraktura ng puno (lumikha, magtanggal, getChildren) at gumagana sa data sa mga node (setData, getData Bukod dito, ang lahat ng mga pamamaraan ay may mahigpit na mga paunang kondisyon: ang paglikha ay magbabalik ng isang error kung ang node ay mayroon na). ginawa, tanggalin o itakda angData – kung hindi pa ito umiiral. Kailangan namin ng isang set na paraan na maaaring tawagan nang hindi iniisip ang pagkakaroon ng isang susi.

Ang isang pagpipilian ay ang kumuha ng isang optimistikong diskarte, tulad ng pagtanggal. Suriin kung mayroong isang node. Kung mayroon, tawagan ang setData, kung hindi man ay gumawa. Kung nagbalik ng error ang huling paraan, ulitin itong muli. Ang unang bagay na dapat tandaan ay ang pagsubok sa pagkakaroon ay walang kabuluhan. Maaari mong tawagan kaagad ang lumikha. Ang matagumpay na pagkumpleto ay nangangahulugan na ang node ay hindi umiiral at ito ay nilikha. Kung hindi, ibabalik ng create ang naaangkop na error, pagkatapos nito kailangan mong tawagan ang setData. Siyempre, sa pagitan ng mga tawag, ang isang vertex ay maaaring tanggalin ng isang nakikipagkumpitensyang tawag, at ang setData ay magbabalik din ng isang error. Sa kasong ito, magagawa mo itong muli, ngunit sulit ba ito?

Kung ang parehong mga pamamaraan ay nagbabalik ng isang error, pagkatapos ay alam namin na sigurado na ang isang nakikipagkumpitensyang pagtanggal ay naganap. Isipin natin na ang pagtanggal na ito ay naganap pagkatapos ng pagtawag sa set. Kung gayon ang anumang kahulugan na sinusubukan nating itatag ay nabura na. Nangangahulugan ito na maaari nating ipagpalagay na matagumpay na naisakatuparan ang set, kahit na sa katunayan ay walang naisulat.

Higit pang mga teknikal na detalye

Sa seksyong ito kami ay magpapahinga mula sa mga distributed system at pag-uusapan ang coding.
Ang isa sa mga pangunahing kinakailangan ng customer ay cross-platform: kahit isa sa mga serbisyo ay dapat na suportado sa Linux, MacOS at Windows. Sa una, binuo lang namin para sa Linux, at nagsimulang mag-test sa ibang mga system sa ibang pagkakataon. Nagdulot ito ng maraming problema, na sa loob ng ilang panahon ay ganap na hindi malinaw kung paano lapitan. Bilang resulta, lahat ng tatlong serbisyo ng koordinasyon ay sinusuportahan na ngayon sa Linux at MacOS, habang ang Consul KV lang ang sinusuportahan sa Windows.

Sa simula pa lang, sinubukan naming gumamit ng mga handa na aklatan para ma-access ang mga serbisyo. Sa kaso ng ZooKeeper, ang pagpili ay nahulog sa ZooKeeper C++, na sa huli ay nabigong mag-compile sa Windows. Ito, gayunpaman, ay hindi nakakagulat: ang library ay nakaposisyon bilang linux-only. Para sa Consul ang tanging pagpipilian ay ppconsul. Kinailangang idagdag ang suporta dito mga session ΠΈ mga transaksyon. Para sa etcd, ang isang ganap na library na sumusuporta sa pinakabagong bersyon ng protocol ay hindi nakita, kaya kami ay simple nabuong grpc client.

Dahil sa inspirasyon ng asynchronous na interface ng ZooKeeper C++ library, nagpasya kaming magpatupad din ng asynchronous na interface. Gumagamit ang ZooKeeper C++ ng future/promise primitives para dito. Sa STL, sa kasamaang palad, ang mga ito ay ipinatupad nang napakahinhin. Halimbawa, hindi pagkatapos pamamaraan, na inilalapat ang naipasa na function sa resulta ng hinaharap kapag naging available na ito. Sa aming kaso, ang ganitong paraan ay kinakailangan upang i-convert ang resulta sa format ng aming library. Upang malutas ang problemang ito, kinailangan naming ipatupad ang aming sariling simpleng thread pool, dahil sa kahilingan ng customer hindi namin magagamit ang mabibigat na third-party na library gaya ng Boost.

Ang aming pagpapatupad noon ay gumagana tulad nito. Kapag tinawag, isang karagdagang pangako/hinaharap na pares ay nilikha. Ang bagong hinaharap ay ibinalik, at ang naipasa ay inilalagay kasama ng kaukulang function at isang karagdagang pangako sa pila. Ang isang thread mula sa pool ay pumipili ng ilang hinaharap mula sa queue at i-poll ang mga ito gamit ang wait_for. Kapag naging available ang isang resulta, tatawagin ang kaukulang function at ipapasa ang return value nito sa promise.

Ginamit namin ang parehong thread pool upang magsagawa ng mga query sa etcd at Consul. Nangangahulugan ito na ang mga pinagbabatayan na aklatan ay maaaring ma-access ng maraming magkakaibang mga thread. Ang ppconsul ay hindi ligtas sa thread, kaya ang mga tawag dito ay protektado ng mga kandado.
Maaari kang magtrabaho sa grpc mula sa maraming mga thread, ngunit may mga subtleties. Sa etcd relo ay ipinatupad sa pamamagitan ng grpc stream. Ito ay mga bidirectional na channel para sa mga mensahe ng isang partikular na uri. Lumilikha ang library ng isang thread para sa lahat ng relo at isang thread na nagpoproseso ng mga papasok na mensahe. Kaya ipinagbabawal ng grpc ang mga parallel na pagsusulat sa stream. Nangangahulugan ito na kapag sinisimulan o tinatanggal ang isang relo, dapat kang maghintay hanggang sa makumpleto ng nakaraang kahilingan ang pagpapadala bago ipadala ang susunod. Ginagamit namin para sa pag-synchronize mga conditional variable.

Kabuuan

Tingnan ang para sa iyong sarili: liboffkv.

Ang aming koponan: Raed Romanov, Ivan Glushenkov, Dmitry Kamaldinov, Victor Krapivensky, Vitaly Ivanin.

Pinagmulan: www.habr.com

Magdagdag ng komento