Fem studenter og tre distribuerte nøkkelverdibutikker

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

I verden av distribuerte systemer er det en rekke typiske oppgaver: lagring av informasjon om sammensetningen av klyngen, administrere konfigurasjonen av noder, oppdage defekte noder, velge en leder og andre. For å løse disse problemene er det laget spesielle distribuerte systemer - koordineringstjenester. Nå skal vi være interessert i tre av dem: ZooKeeper, etcd og Consul. Av all den rike funksjonaliteten til Consul vil vi fokusere på Consul KV.

Fem studenter og tre distribuerte nøkkelverdibutikker

I hovedsak er alle disse systemene feiltolerante, lineariserbare nøkkelverdilagre. Selv om datamodellene deres har betydelige forskjeller, som vi skal diskutere senere, løser de de samme praktiske problemene. Det er klart at hver applikasjon som bruker koordineringstjenesten er knyttet til en av dem, noe som kan føre til behov for å støtte flere systemer i ett datasenter som løser de samme problemene for forskjellige applikasjoner.

Ideen om å løse dette problemet oppsto i et australsk konsulentbyrå, og det falt på oss, et lite team av studenter, å implementere det, og det er det jeg skal snakke om.

Vi klarte å lage et bibliotek som gir et felles grensesnitt for arbeid med ZooKeeper, etcd og Consul KV. Biblioteket er skrevet i C++, men det er planer om å overføre det til andre språk.

Datamodeller

For å utvikle et felles grensesnitt for tre forskjellige systemer, må du forstå hva de har til felles og hvordan de er forskjellige. La oss finne ut av det.

Dyrepasser

Fem studenter og tre distribuerte nøkkelverdibutikker

Nøklene er organisert i et tre og kalles noder. Følgelig, for en node kan du få en liste over barna. Operasjonene for å lage en znode (opprette) og endre en verdi (setData) er atskilt: bare eksisterende nøkler kan leses og endres. Klokker kan knyttes til operasjonene med å sjekke eksistensen av en node, lese en verdi og skaffe barn. Watch er en engangsutløser som utløses når versjonen av de tilsvarende dataene på serveren endres. Ephemeral noder brukes til å oppdage feil. De er knyttet til økten til klienten som opprettet dem. Når en klient lukker en økt eller slutter å varsle ZooKeeper om dens eksistens, slettes disse nodene automatisk. Enkle transaksjoner støttes - et sett med operasjoner som enten alle lykkes eller mislykkes hvis dette ikke er mulig for minst én av dem.

osv

Fem studenter og tre distribuerte nøkkelverdibutikker

Utviklerne av dette systemet var tydelig inspirert av ZooKeeper, og gjorde derfor alt annerledes. Det er ikke noe hierarki av nøkler, men de danner et leksikografisk ordnet sett. Du kan få eller slette alle nøkler som tilhører et bestemt område. Denne strukturen kan virke merkelig, men den er faktisk veldig uttrykksfull, og et hierarkisk syn kan lett etterlignes gjennom den.

etcd har ikke en standard sammenligning-og-sett-operasjon, men den har noe bedre: transaksjoner. Selvfølgelig finnes de i alle tre systemene, men etcd-transaksjoner er spesielt gode. De består av tre blokker: sjekk, suksess, fiasko. Den første blokken inneholder et sett med betingelser, den andre og tredje - operasjoner. Transaksjonen utføres atomært. Hvis alle betingelsene er sanne, blir suksessblokken utført, ellers blir feilblokken utført. I API 3.3 kan suksess- og fiaskoblokker inneholde nestede transaksjoner. Det vil si at det er mulig å atomisk utføre betingede konstruksjoner av nesten vilkårlig hekkenivå. Du kan lære mer om hva kontroller og operasjoner eksisterer fra dokumentasjon.

Klokker finnes også her, selv om de er litt mer kompliserte og kan gjenbrukes. Det vil si at etter å ha installert en klokke på en nøkkelserie, vil du motta alle oppdateringer i denne serien til du kansellerer klokken, og ikke bare den første. I etcd er analogen til ZooKeeper-klientøkter leasing.

Konsul K.V.

Det er heller ingen streng hierarkisk struktur her, men Consul kan skape inntrykk av at den eksisterer: du kan hente og slette alle nøkler med det spesifiserte prefikset, det vil si jobbe med "undertreet" til nøkkelen. Slike spørringer kalles rekursive. I tillegg kan Consul kun velge nøkler som ikke inneholder det spesifiserte tegnet etter prefikset, som tilsvarer å få umiddelbare "barn". Men det er verdt å huske at dette er nettopp utseendet til en hierarkisk struktur: det er fullt mulig å lage en nøkkel hvis dens forelder ikke eksisterer eller slette en nøkkel som har barn, mens barna vil fortsette å bli lagret i systemet.

Fem studenter og tre distribuerte nøkkelverdibutikker
I stedet for klokker har Consul blokkerende HTTP-forespørsler. I hovedsak er dette vanlige anrop til datalesemetoden, for hvilke, sammen med andre parametere, den siste kjente versjonen av dataene er indikert. Hvis den gjeldende versjonen av de tilsvarende dataene på serveren er større enn den spesifiserte, returneres svaret umiddelbart, ellers - når verdien endres. Det er også økter som kan festes til nøkler når som helst. Det er verdt å merke seg at i motsetning til etcd og ZooKeeper, der sletting av økter fører til sletting av tilknyttede nøkler, er det en modus der økten ganske enkelt kobles fra dem. Tilgjengelig transaksjoner, uten grener, men med alle slags sjekker.

Sette alt sammen

ZooKeeper har den mest strenge datamodellen. De ekspressive rekkeviddespørringene som er tilgjengelige i etcd kan ikke emuleres effektivt verken i ZooKeeper eller Consul. For å prøve å innlemme det beste fra alle tjenestene, endte vi opp med et grensesnitt nesten tilsvarende ZooKeeper-grensesnittet med følgende betydelige unntak:

  • sekvens, container og TTL noder ikke støttet
  • ACLer støttes ikke
  • settmetoden oppretter en nøkkel hvis den ikke eksisterer (i ZK returnerer setData en feil i dette tilfellet)
  • sett- og cas-metoder er atskilt (i ZK er de i hovedsak det samme)
  • slettemetoden sletter en node sammen med undertreet (i ZK returnerer sletting en feil hvis noden har barn)
  • For hver nøkkel er det bare én versjon - verdiversjonen (i ZK det er tre av dem)

Avvisningen av sekvensielle noder skyldes det faktum at etcd og Consul ikke har innebygd støtte for dem, og de kan enkelt implementeres av brukeren på toppen av det resulterende bibliotekgrensesnittet.

Implementering av atferd som ligner på ZooKeeper når du sletter et toppunkt vil kreve å opprettholde en separat underordnet teller for hver nøkkel i etcd og Consul. Siden vi prøvde å unngå å lagre metainformasjon, ble det besluttet å slette hele undertreet.

Finesser i implementeringen

La oss se nærmere på noen aspekter ved implementering av bibliotekgrensesnittet i forskjellige systemer.

Hierarki i etcd

Å opprettholde et hierarkisk syn i etcd viste seg å være en av de mest interessante oppgavene. Områdespørringer gjør det enkelt å hente en liste over nøkler med et spesifisert prefiks. For eksempel hvis du trenger alt som starter med "/foo", ber du om en rekkevidde ["/foo", "/fop"). Men dette vil returnere hele undertreet til nøkkelen, noe som kanskje ikke er akseptabelt hvis undertreet er stort. Først planla vi å bruke en viktig oversettelsesmekanisme, implementert i zetcd. Det innebærer å legge til én byte i begynnelsen av nøkkelen, lik dybden til noden i treet. La meg gi deg et eksempel.

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

Så få alle umiddelbare barn av nøkkelen "/foo" mulig ved å be om en rekkevidde ["u02/foo/", "u02/foo0"). Ja, i ASCII "0" står rett etter "/".

Men hvordan implementere fjerning av et toppunkt i dette tilfellet? Det viser seg at du må slette alle områder av typen ["uXX/foo/", "uXX/foo0") for XX fra 01 til FF. Og så løp vi inn operasjonsnummergrense innenfor én transaksjon.

Som et resultat ble et enkelt nøkkelkonverteringssystem oppfunnet, som gjorde det mulig å effektivt implementere både sletting av en nøkkel og få en liste over barn. Det er nok å legge til et spesialtegn før siste token. For eksempel:

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

Deretter sletter du nøkkelen "/very" blir til sletting "/u00very" og rekkevidde ["/very/", "/very0"), og få alle barn - i en forespørsel om nøkler fra serien ["/very/u00", "/very/u01").

Fjerne en nøkkel i ZooKeeper

Som jeg allerede har nevnt, i ZooKeeper kan du ikke slette en node hvis den har barn. Vi ønsker å slette nøkkelen sammen med undertreet. Hva burde jeg gjøre? Vi gjør dette med optimisme. Først går vi rekursivt gjennom undertreet, og får barna til hvert toppunkt med en separat spørring. Deretter bygger vi en transaksjon som prøver å slette alle noder i undertreet i riktig rekkefølge. Det kan selvfølgelig skje endringer mellom å lese et undertre og å slette det. I dette tilfellet vil transaksjonen mislykkes. Dessuten kan undertreet endres under leseprosessen. En forespørsel for barna til neste node kan returnere en feil hvis for eksempel denne noden allerede er slettet. I begge tilfeller gjentar vi hele prosessen på nytt.

Denne tilnærmingen gjør sletting av en nøkkel svært ineffektiv hvis den har barn, og enda mer hvis applikasjonen fortsetter å jobbe med undertreet, sletting og opprettelse av nøkler. Dette tillot oss imidlertid å unngå å komplisere implementeringen av andre metoder i etcd og Consul.

satt i ZooKeeper

I ZooKeeper er det egne metoder som fungerer med trestrukturen (create, delete, getChildren) og som jobber med data i noder (setData, getData) Dessuten har alle metoder strenge forutsetninger: create vil returnere en feil hvis noden allerede har blitt opprettet, slettet eller setData – hvis det ikke allerede eksisterer. Vi trengte en fast metode som kan kalles uten å tenke på tilstedeværelsen av en nøkkel.

Et alternativ er å ha en optimistisk tilnærming, som med sletting. Sjekk om det finnes en node. Hvis det finnes, ring setData, ellers opprett. Hvis den siste metoden returnerte en feil, gjenta den på nytt. Det første å merke seg er at eksistenstesten er meningsløs. Du kan umiddelbart ringe opprette. Vellykket fullføring vil bety at noden ikke eksisterte og den ble opprettet. Ellers vil create returnere den aktuelle feilen, hvoretter du må ringe setData. Selvfølgelig, mellom anrop, kan et toppunkt slettes av et konkurrerende anrop, og setData vil også returnere en feil. I dette tilfellet kan du gjøre alt på nytt, men er det verdt det?

Hvis begge metodene returnerer en feil, vet vi med sikkerhet at en konkurrerende sletting fant sted. La oss forestille oss at denne slettingen skjedde etter anropssett. Da er den meningen vi prøver å etablere allerede slettet. Dette betyr at vi kan anta at settet ble utført vellykket, selv om ingenting faktisk ble skrevet.

Flere tekniske detaljer

I denne delen tar vi en pause fra distribuerte systemer og snakker om koding.
Et av hovedkravene til kunden var på tvers av plattformer: minst én av tjenestene må støttes på Linux, MacOS og Windows. I utgangspunktet utviklet vi kun for Linux, og begynte å teste på andre systemer senere. Dette førte til mange problemer, som i en periode var helt uklart hvordan de skulle forholde seg. Som et resultat er alle tre koordineringstjenestene nå støttet på Linux og MacOS, mens kun Consul KV støttes på Windows.

Helt fra starten prøvde vi å bruke ferdige biblioteker for å få tilgang til tjenester. I tilfellet ZooKeeper falt valget på ZooKeeper C++, som til slutt ikke klarte å kompilere på Windows. Dette er imidlertid ikke overraskende: biblioteket er posisjonert som kun linux. For konsul var det eneste alternativet ppkonsul. Det måtte legges til støtte økter и transaksjoner. For etcd ble ikke et fullverdig bibliotek som støtter den nyeste versjonen av protokollen funnet, så vi generert grpc-klient.

Inspirert av det asynkrone grensesnittet til ZooKeeper C++-biblioteket, bestemte vi oss for også å implementere et asynkront grensesnitt. ZooKeeper C++ bruker fremtids-/løfteprimitiver for dette. I STL er de dessverre implementert svært beskjedent. For eksempel, nei deretter metode, som bruker den beståtte funksjonen på resultatet av fremtiden når den blir tilgjengelig. I vårt tilfelle er en slik metode nødvendig for å konvertere resultatet til formatet til biblioteket vårt. For å omgå dette problemet måtte vi implementere vår egen enkle trådpool, siden vi på kundens forespørsel ikke kunne bruke tunge tredjepartsbiblioteker som Boost.

Vår daværende implementering fungerer slik. Når det kalles opp, opprettes et ekstra løfte/fremtid-par. Den nye fremtiden returneres, og den beståtte plasseres sammen med tilhørende funksjon og et ekstra løfte i køen. En tråd fra bassenget velger flere futures fra køen og poller dem ved å bruke wait_for. Når et resultat blir tilgjengelig, kalles den tilsvarende funksjonen og dens returverdi sendes til løftet.

Vi brukte den samme trådpoolen for å utføre spørringer til etcd og Consul. Dette betyr at de underliggende bibliotekene kan nås av flere forskjellige tråder. ppconsul er ikke trådsikker, så anrop til den er beskyttet av låser.
Du kan jobbe med grpc fra flere tråder, men det er finesser. I etcd implementeres klokker via grpc-strømmer. Dette er toveis kanaler for meldinger av en bestemt type. Biblioteket oppretter en enkelt tråd for alle klokker og en enkelt tråd som behandler innkommende meldinger. Så grpc forbyr parallellskriving til strømming. Dette betyr at når du initialiserer eller sletter en klokke, må du vente til forrige forespørsel er fullført før du sender den neste. Vi bruker til synkronisering betingede variabler.

Total

Se selv: liboffkv.

Vårt team: Raed Romanov, Ivan Glushenkov, Dmitrij Kamaldinov, Victor Krapivensky, Vitaly Ivanin.

Kilde: www.habr.com

Legg til en kommentar