Fem studerende og tre distribuerede nøgleværdibutikker

Eller hvordan vi skrev et klient C++ bibliotek til ZooKeeper, etcd og Consul KV

I en verden af ​​distribuerede systemer er der en række typiske opgaver: lagring af information om klyngens sammensætning, styring af konfigurationen af ​​noder, opdagelse af defekte noder, valg af leder andre. For at løse disse problemer er der skabt specielle distribuerede systemer - koordinationstjenester. Nu vil vi være interesserede i tre af dem: ZooKeeper, etcd og Consul. Ud af al den rige funktionalitet i Consul vil vi fokusere på Consul KV.

Fem studerende og tre distribuerede nøgleværdibutikker

I det væsentlige er alle disse systemer fejltolerante, lineariserbare nøgleværdilagre. Selvom deres datamodeller har betydelige forskelle, som vi vil diskutere senere, løser de de samme praktiske problemer. Det er klart, at hver applikation, der bruger koordinationstjenesten, er knyttet til en af ​​dem, hvilket kan føre til behovet for at understøtte flere systemer i ét datacenter, der løser de samme problemer for forskellige applikationer.

Ideen til at løse dette problem opstod i et australsk konsulentbureau, og det faldt op til os, et lille hold af studerende, at implementere det, hvilket er det, jeg vil tale om.

Det lykkedes at skabe et bibliotek, der giver en fælles grænseflade til at arbejde med ZooKeeper, etcd og Consul KV. Biblioteket er skrevet i C++, men der er planer om at overføre det til andre sprog.

Datamodeller

For at udvikle en fælles grænseflade for tre forskellige systemer skal du forstå, hvad de har til fælles, og hvordan de adskiller sig. Lad os finde ud af det.

Dyrepasser

Fem studerende og tre distribuerede nøgleværdibutikker

Nøglerne er organiseret i et træ og kaldes noder. Derfor kan du for en node få en liste over dens børn. Operationerne med at oprette en znode (opret) og ændre en værdi (setData) er adskilt: kun eksisterende nøgler kan læses og ændres. Ure kan knyttes til operationerne med at kontrollere eksistensen af ​​en node, læse en værdi og få børn. Watch er en engangsudløser, der udløses, når versionen af ​​de tilsvarende data på serveren ændres. Ephemeral noder bruges til at opdage fejl. De er bundet til sessionen for den klient, der oprettede dem. Når en klient lukker en session eller holder op med at underrette ZooKeeper om dens eksistens, slettes disse noder automatisk. Simple transaktioner understøttes - et sæt operationer, der enten alle lykkes eller mislykkes, hvis dette ikke er muligt for mindst én af dem.

osv

Fem studerende og tre distribuerede nøgleværdibutikker

Udviklerne af dette system var tydeligt inspireret af ZooKeeper, og gjorde derfor alting anderledes. Der er ikke noget hierarki af nøgler, men de danner et leksikografisk ordnet sæt. Du kan få eller slette alle nøgler, der hører til et bestemt område. Denne struktur kan virke mærkelig, men den er faktisk meget udtryksfuld, og en hierarkisk opfattelse kan let efterlignes gennem den.

etcd har ikke en standard sammenligne-og-indstil operation, men den har noget bedre: transaktioner. Selvfølgelig findes de i alle tre systemer, men etcd-transaktioner er særligt gode. De består af tre blokke: check, succes, fiasko. Den første blok indeholder et sæt betingelser, den anden og tredje - operationer. Transaktionen udføres atomært. Hvis alle betingelser er sande, udføres succesblokken, ellers udføres fejlblokken. I API 3.3 kan succes- og fiaskoblokke indeholde indlejrede transaktioner. Det vil sige, at det er muligt atomisk at udføre betingede konstruktioner af næsten vilkårligt redeniveau. Du kan lære mere om, hvad kontroller og operationer består af dokumentation.

Ure findes også her, selvom de er lidt mere komplicerede og kan genbruges. Det vil sige, at efter at have installeret et ur på en nøgleserie, vil du modtage alle opdateringer i denne serie indtil du annullerer uret, og ikke kun den første. I etcd er analogen af ​​ZooKeeper-klientsessioner leasing.

Konsul K.V.

Der er heller ingen streng hierarkisk struktur her, men Consul kan skabe det udseende, at den eksisterer: du kan hente og slette alle nøgler med det angivne præfiks, det vil sige arbejde med nøglens "undertræ". Sådanne forespørgsler kaldes rekursive. Derudover kan Consul kun vælge nøgler, der ikke indeholder det angivne tegn efter præfikset, hvilket svarer til at få øjeblikkelig "børn". Men det er værd at huske på, at dette netop er udseendet af en hierarkisk struktur: det er ganske muligt at oprette en nøgle, hvis dens forælder ikke eksisterer, eller slette en nøgle, der har børn, mens børnene fortsat vil blive gemt i systemet.

Fem studerende og tre distribuerede nøgleværdibutikker
I stedet for ure har Consul blokerende HTTP-anmodninger. I bund og grund er der tale om almindelige opkald til datalæsningsmetoden, for hvilke sammen med andre parametre den sidst kendte version af dataene er angivet. Hvis den aktuelle version af de tilsvarende data på serveren er større end den angivne, returneres svaret med det samme, ellers - når værdien ændres. Der er også sessioner, der til enhver tid kan knyttes til nøgler. Det er værd at bemærke, at i modsætning til etcd og ZooKeeper, hvor sletning af sessioner fører til sletning af tilknyttede nøgler, er der en tilstand, hvor sessionen simpelthen fjernes fra dem. Ledig transaktioner, uden grene, men med alle former for checks.

Samler det hele

ZooKeeper har den mest stringente datamodel. De ekspressive rækkeviddeforespørgsler, der er tilgængelige i etcd, kan ikke effektivt emuleres i hverken ZooKeeper eller Consul. I et forsøg på at inkorporere det bedste fra alle tjenesterne, endte vi med en grænseflade, der næsten svarer til ZooKeeper-grænsefladen med følgende væsentlige undtagelser:

  • sekvens, container og TTL noder ikke understøttet
  • ACL'er understøttes ikke
  • sætmetoden opretter en nøgle, hvis den ikke eksisterer (i ZK returnerer setData en fejl i dette tilfælde)
  • sæt og cas metoder er adskilt (i ZK er de i det væsentlige det samme)
  • slettemetoden sletter en node sammen med dens undertræ (i ZK returnerer slet en fejl, hvis noden har børn)
  • For hver nøgle er der kun én version - værdiversionen (i ZK der er tre af dem)

Afvisningen af ​​sekventielle noder skyldes, at etcd og Consul ikke har indbygget support til dem, og de kan nemt implementeres af brugeren oven på den resulterende biblioteksgrænseflade.

Implementering af adfærd svarende til ZooKeeper ved sletning af et vertex ville kræve at opretholde en separat underordnet tæller for hver nøgle i etcd og Consul. Da vi forsøgte at undgå at gemme metainformation, blev det besluttet at slette hele undertræet.

Finesser af implementering

Lad os se nærmere på nogle aspekter af implementering af biblioteksgrænsefladen i forskellige systemer.

Hierarki i etcd

At opretholde et hierarkisk syn i etcd viste sig at være en af ​​de mest interessante opgaver. Områdeforespørgsler gør det nemt at hente en liste over nøgler med et specificeret præfiks. For eksempel hvis du skal bruge alt, hvad der starter med "/foo", du beder om en rækkevidde ["/foo", "/fop"). Men dette ville returnere hele undertræet af nøglen, hvilket måske ikke er acceptabelt, hvis undertræet er stort. Først planlagde vi at bruge en nøgleoversættelsesmekanisme, implementeret i zetcd. Det involverer tilføjelse af en byte i begyndelsen af ​​nøglen, svarende til dybden af ​​noden i træet. Lad mig give dig et eksempel.

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

Så få alle umiddelbare børn af nøglen "/foo" muligt ved at anmode om en rækkevidde ["u02/foo/", "u02/foo0"). Ja, i ASCII "0" står lige efter "/".

Men hvordan implementerer man fjernelse af et vertex i dette tilfælde? Det viser sig, at du skal slette alle intervaller af typen ["uXX/foo/", "uXX/foo0") for XX fra 01 til FF. Og så løb vi ind operations antal grænse inden for én transaktion.

Som et resultat blev et simpelt nøglekonverteringssystem opfundet, som gjorde det muligt effektivt at implementere både sletning af en nøgle og opnåelse af en liste over børn. Det er nok at tilføje et særligt tegn før det sidste token. For eksempel:

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

Slet derefter nøglen "/very" bliver til sletning "/u00very" og rækkevidde ["/very/", "/very0"), og få alle børn - i en anmodning om nøgler fra sortimentet ["/very/u00", "/very/u01").

Fjernelse af en nøgle i ZooKeeper

Som jeg allerede har nævnt, kan du i ZooKeeper ikke slette en node, hvis den har børn. Vi ønsker at slette nøglen sammen med undertræet. Hvad skal jeg gøre? Det gør vi med optimisme. Først krydser vi undertræet rekursivt, hvorved vi opnår børnene i hvert hjørne med en separat forespørgsel. Derefter bygger vi en transaktion, der forsøger at slette alle noder i undertræet i den rigtige rækkefølge. Selvfølgelig kan der ske ændringer mellem at læse et undertræ og at slette det. I dette tilfælde vil transaktionen mislykkes. Desuden kan undertræet ændre sig under læseprocessen. En anmodning til børnene til den næste node kan returnere en fejl, hvis for eksempel denne node allerede er blevet slettet. I begge tilfælde gentager vi hele processen igen.

Denne tilgang gør sletning af en nøgle meget ineffektiv, hvis den har børn, og endnu mere, hvis applikationen fortsætter med at arbejde med undertræet, sletning og oprettelse af nøgler. Dette gav os dog mulighed for at undgå at komplicere implementeringen af ​​andre metoder i etcd og Consul.

foregår i ZooKeeper

I ZooKeeper er der separate metoder, der arbejder med træstrukturen (create, delete, getChildren) og som arbejder med data i noder (setData, getData) Desuden har alle metoder strenge forudsætninger: create vil returnere en fejl, hvis noden allerede har blevet oprettet, slettet eller setData – hvis det ikke allerede eksisterer. Vi havde brug for en fast metode, der kan kaldes uden at tænke på tilstedeværelsen af ​​en nøgle.

En mulighed er at tage en optimistisk tilgang, som med sletning. Tjek om der findes en node. Hvis det findes, kald setData, ellers opret. Hvis den sidste metode returnerede en fejl, skal du gentage den igen. Den første ting at bemærke er, at eksistenstesten er meningsløs. Du kan straks kalde opret. Succesfuld afslutning vil betyde, at noden ikke eksisterede, og den blev oprettet. Ellers vil create returnere den relevante fejl, hvorefter du skal kalde setData. Selvfølgelig, mellem opkald, kunne et vertex blive slettet af et konkurrerende opkald, og setData ville også returnere en fejl. I dette tilfælde kan du gøre det hele igen, men er det det værd?

Hvis begge metoder returnerer en fejl, så ved vi med sikkerhed, at en konkurrerende sletning fandt sted. Lad os forestille os, at denne sletning skete efter opkaldssæt. Så er den mening, vi prøver at etablere, allerede slettet. Dette betyder, at vi kan antage, at sættet blev udført med succes, selvom der faktisk ikke blev skrevet noget.

Flere tekniske detaljer

I dette afsnit vil vi tage en pause fra distribuerede systemer og tale om kodning.
Et af kundens hovedkrav var cross-platform: mindst én af tjenesterne skal understøttes på Linux, MacOS og Windows. I starten udviklede vi kun til Linux og begyndte at teste på andre systemer senere. Det gav en masse problemer, som i nogen tid var helt uklare, hvordan de skulle gribe det an. Som følge heraf understøttes alle tre koordinationstjenester nu på Linux og MacOS, mens kun Consul KV understøttes på Windows.

Helt fra begyndelsen forsøgte vi at bruge færdige biblioteker til at få adgang til tjenester. I tilfældet med ZooKeeper faldt valget på ZooKeeper C++, som i sidste ende ikke kompilerede på Windows. Dette er dog ikke overraskende: biblioteket er placeret som linux-kun. For konsul var den eneste mulighed ppkonsul. Der skulle føjes støtte til det sessioner и transaktioner. For etcd blev der ikke fundet et fuldgyldigt bibliotek, der understøtter den seneste version af protokollen, så vi genereret grpc klient.

Inspireret af den asynkrone grænseflade i ZooKeeper C++-biblioteket besluttede vi også at implementere en asynkron grænseflade. ZooKeeper C++ bruger fremtids/løfte primitiver til dette. I STL er de desværre implementeret meget beskedent. For eksempel nej derefter metode, som anvender den beståede funktion på fremtidens resultat, når den bliver tilgængelig. I vores tilfælde er en sådan metode nødvendig for at konvertere resultatet til formatet på vores bibliotek. For at komme uden om dette problem var vi nødt til at implementere vores egen simple trådpulje, da vi på kundens anmodning ikke kunne bruge tunge tredjepartsbiblioteker såsom Boost.

Vores daværende implementering fungerer sådan. Når der kaldes op, oprettes et ekstra løfte/fremtid-par. Den nye fremtid returneres, og den beståede placeres sammen med den tilsvarende funktion og et ekstra løfte i køen. En tråd fra puljen vælger flere futures fra køen og poller dem ved hjælp af wait_for. Når et resultat bliver tilgængeligt, kaldes den tilsvarende funktion, og dens returværdi overføres til løftet.

Vi brugte den samme trådpulje til at udføre forespørgsler til etcd og Consul. Dette betyder, at de underliggende biblioteker kan tilgås af flere forskellige tråde. ppconsul er ikke trådsikker, så opkald til den er beskyttet af låse.
Du kan arbejde med grpc fra flere tråde, men der er finesser. I etcd implementeres ure via grpc-streams. Disse er tovejskanaler for beskeder af en bestemt type. Biblioteket opretter en enkelt tråd til alle ure og en enkelt tråd, der behandler indgående beskeder. Så grpc forbyder parallelskrivning til stream. Det betyder, at når du initialiserer eller sletter et ur, skal du vente, indtil den forrige anmodning er afsluttet afsendelsen, før du sender den næste. Vi bruger til synkronisering betingede variable.

Total

Se selv: liboffkv.

Vores hold: Raed Romanov, Ivan Glushenkov, Dmitry Kamaldinov, Victor Krapivensky, Vitaly Ivanin.

Kilde: www.habr.com

Tilføj en kommentar