Vijf studenten en drie gedistribueerde key-value-winkels

Of hoe we een client C++-bibliotheek schreven voor ZooKeeper, etcd en Consul KV

In de wereld van gedistribueerde systemen zijn er een aantal typische taken: het opslaan van informatie over de samenstelling van het cluster, het beheren van de configuratie van knooppunten, het detecteren van defecte knooppunten, het kiezen van een leider ander. Om deze problemen op te lossen zijn speciale gedistribueerde systemen gecreëerd: coördinatiediensten. Nu zullen we geïnteresseerd zijn in drie ervan: ZooKeeper, etcd en Consul. Van alle rijke functionaliteit van Consul zullen we ons concentreren op Consul KV.

Vijf studenten en drie gedistribueerde key-value-winkels

In wezen zijn al deze systemen fouttolerante, lineariseerbare sleutelwaardeopslagplaatsen. Hoewel hun datamodellen aanzienlijke verschillen vertonen, die we later zullen bespreken, lossen ze dezelfde praktische problemen op. Het is duidelijk dat elke applicatie die gebruikmaakt van de coördinatiedienst aan één ervan is gekoppeld, wat kan leiden tot de noodzaak om meerdere systemen in één datacenter te ondersteunen die dezelfde problemen voor verschillende applicaties oplossen.

Het idee om dit probleem op te lossen ontstond bij een Australisch adviesbureau, en het was aan ons, een klein team studenten, om het te implementeren, en daar ga ik het over hebben.

We zijn erin geslaagd een bibliotheek te creëren die een gemeenschappelijke interface biedt voor het werken met ZooKeeper, etcd en Consul KV. De bibliotheek is geschreven in C++, maar er zijn plannen om deze naar andere talen over te zetten.

Gegevensmodellen

Om een ​​gemeenschappelijke interface voor drie verschillende systemen te ontwikkelen, moet je begrijpen wat ze gemeen hebben en waarin ze verschillen. Laten we het uitzoeken.

Dierentuinmedewerker

Vijf studenten en drie gedistribueerde key-value-winkels

De sleutels zijn georganiseerd in een boom en worden knooppunten genoemd. Dienovereenkomstig kunt u voor een knooppunt een lijst met de onderliggende knooppunten krijgen. De bewerkingen voor het maken van een znode (create) en het wijzigen van een waarde (setData) zijn gescheiden: alleen bestaande sleutels kunnen worden gelezen en gewijzigd. Er kunnen horloges worden gekoppeld aan de bewerkingen zoals het controleren van het bestaan ​​van een knooppunt, het lezen van een waarde en het verkrijgen van kinderen. Watch is een eenmalige trigger die wordt geactiveerd wanneer de versie van de overeenkomstige gegevens op de server verandert. Kortstondige knooppunten worden gebruikt om fouten te detecteren. Ze zijn gekoppeld aan de sessie van de client die ze heeft gemaakt. Wanneer een client een sessie sluit of ZooKeeper niet langer op de hoogte stelt van het bestaan ​​ervan, worden deze knooppunten automatisch verwijderd. Eenvoudige transacties worden ondersteund: een reeks bewerkingen die allemaal slagen of mislukken als dit voor ten minste één van hen niet mogelijk is.

enz

Vijf studenten en drie gedistribueerde key-value-winkels

De ontwikkelaars van dit systeem waren duidelijk geïnspireerd door ZooKeeper en deden daarom alles anders. Er is geen hiërarchie van sleutels, maar ze vormen een lexicografisch geordende set. U kunt alle sleutels die tot een bepaald bereik behoren, verkrijgen of verwijderen. Deze structuur lijkt misschien vreemd, maar is in werkelijkheid zeer expressief en er kan gemakkelijk een hiërarchische visie doorheen worden nagebootst.

etcd heeft geen standaard vergelijk-en-stel-operatie, maar het heeft wel iets beters: transacties. Natuurlijk bestaan ​​ze in alle drie de systemen, maar etcd-transacties zijn bijzonder goed. Ze bestaan ​​uit drie blokken: controle, succes, mislukking. Het eerste blok bevat een reeks voorwaarden, het tweede en derde - bewerkingen. De transactie wordt atomair uitgevoerd. Als alle voorwaarden waar zijn, wordt het succesblok uitgevoerd, anders wordt het mislukkingsblok uitgevoerd. In API 3.3 kunnen succes- en mislukkingsblokken geneste transacties bevatten. Dat wil zeggen, het is mogelijk om conditionele constructies van een vrijwel willekeurig nestniveau atomair uit te voeren. U kunt meer leren over waaruit controles en bewerkingen bestaan documentatie.

Horloges bestaan ​​hier ook, hoewel ze iets ingewikkelder zijn en herbruikbaar zijn. Dat wil zeggen dat u na het installeren van een horloge op een belangrijke reeks alle updates in deze reeks ontvangt totdat u het horloge annuleert, en niet alleen de eerste. In etcd zijn leases het analogon van ZooKeeper-clientsessies.

Consul K.V.

Er is hier ook geen strikte hiërarchische structuur, maar Consul kan de schijn wekken dat deze bestaat: u kunt alle sleutels met het opgegeven voorvoegsel verkrijgen en verwijderen, dat wil zeggen, werken met de "subboom" van de sleutel. Dergelijke zoekopdrachten worden recursief genoemd. Bovendien kan Consul alleen sleutels selecteren die niet het opgegeven teken na het voorvoegsel bevatten, wat overeenkomt met het verkrijgen van onmiddellijke "kinderen". Maar het is de moeite waard om te onthouden dat dit precies de schijn is van een hiërarchische structuur: het is heel goed mogelijk om een ​​sleutel te maken als de ouder ervan niet bestaat, of om een ​​sleutel te verwijderen die kinderen heeft, terwijl de kinderen in het systeem blijven opgeslagen.

Vijf studenten en drie gedistribueerde key-value-winkels
In plaats van horloges heeft Consul HTTP-verzoeken geblokkeerd. In essentie zijn dit gewone oproepen naar de dataleesmethode, waarvoor, samen met andere parameters, de laatst bekende versie van de data wordt aangegeven. Als de huidige versie van de overeenkomstige gegevens op de server groter is dan de opgegeven versie, wordt het antwoord onmiddellijk geretourneerd, anders - wanneer de waarde verandert. Er zijn ook sessies die op elk moment aan sleutels kunnen worden gekoppeld. Het is vermeldenswaard dat, in tegenstelling tot etcd en ZooKeeper, waar het verwijderen van sessies leidt tot het verwijderen van bijbehorende sleutels, er een modus is waarin de sessie eenvoudigweg van de sessie wordt losgekoppeld. Beschikbaar transacties, zonder filialen, maar met allerlei controles.

Alles op een rij zetten

ZooKeeper heeft het meest rigoureuze datamodel. De expressieve bereikquery's die beschikbaar zijn in etcd kunnen niet effectief worden geëmuleerd in ZooKeeper of Consul. In een poging het beste van alle services te integreren, kwamen we uit op een interface die bijna gelijkwaardig was aan de ZooKeeper-interface, met de volgende belangrijke uitzonderingen:

  • reeks-, container- en TTL-knooppunten niet ondersteund
  • ACL's worden niet ondersteund
  • de set-methode maakt een sleutel aan als deze niet bestaat (in ZK retourneert setData in dit geval een fout)
  • set- en cas-methoden zijn gescheiden (in ZK zijn ze in wezen hetzelfde)
  • de wismethode verwijdert een knooppunt samen met zijn subboom (in ZK retourneert verwijderen een fout als het knooppunt kinderen heeft)
  • Voor elke sleutel is er slechts één versie: de waardeversie (in ZK het zijn er drie)

De afwijzing van sequentiële knooppunten is te wijten aan het feit dat etcd en Consul er geen ingebouwde ondersteuning voor hebben, en dat ze eenvoudig door de gebruiker kunnen worden geïmplementeerd bovenop de resulterende bibliotheekinterface.

Het implementeren van gedrag vergelijkbaar met ZooKeeper bij het verwijderen van een hoekpunt zou vereisen dat voor elke sleutel in etcd en Consul een aparte onderliggende teller wordt onderhouden. Omdat we probeerden te voorkomen dat meta-informatie werd opgeslagen, werd besloten de hele subboom te verwijderen.

Subtiliteiten van implementatie

Laten we enkele aspecten van de implementatie van de bibliotheekinterface in verschillende systemen eens nader bekijken.

Hiërarchie in etcd

Het handhaven van een hiërarchische weergave in etcd bleek een van de meest interessante taken. Met bereikquery's kunt u eenvoudig een lijst met sleutels met een opgegeven voorvoegsel ophalen. Als u bijvoorbeeld alles nodig heeft dat begint met "/foo", u vraagt ​​om een ​​bereik ["/foo", "/fop"). Maar dit zou de volledige subboom van de sleutel retourneren, wat mogelijk niet acceptabel is als de subboom groot is. In eerste instantie waren we van plan een belangrijk vertaalmechanisme te gebruiken, geïmplementeerd in zetcd. Het gaat om het toevoegen van één byte aan het begin van de sleutel, gelijk aan de diepte van het knooppunt in de boom. Laat me je een voorbeeld geven.

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

Haal dan alle directe kinderen van de sleutel "/foo" mogelijk door een bereik aan te vragen ["u02/foo/", "u02/foo0"). Ja, in ASCII-formaat "0" staat er vlak achter "/".

Maar hoe implementeer je in dit geval de verwijdering van een hoekpunt? Het blijkt dat je alle bereiken van het type moet verwijderen ["uXX/foo/", "uXX/foo0") voor XX van 01 tot FF. En toen kwamen we tegen limiet voor het aantal bewerkingen binnen één transactie.

Als resultaat werd een eenvoudig sleutelconversiesysteem uitgevonden, dat het mogelijk maakte om zowel het verwijderen van een sleutel als het verkrijgen van een lijst met kinderen effectief te implementeren. Het is voldoende om een ​​speciaal teken toe te voegen vóór het laatste token. Bijvoorbeeld:

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

Verwijder vervolgens de sleutel "/very" verandert in verwijderen "/u00very" en bereik ["/very/", "/very0"), en het krijgen van alle kinderen - in een verzoek om sleutels uit het assortiment ["/very/u00", "/very/u01").

Een sleutel verwijderen in ZooKeeper

Zoals ik al zei, kun je in ZooKeeper een knooppunt niet verwijderen als het kinderen heeft. We willen de sleutel samen met de subboom verwijderen. Wat moet ik doen? Wij doen dit met optimisme. Eerst doorkruisen we recursief de subboom, waarbij we de kinderen van elk hoekpunt verkrijgen met een afzonderlijke vraag. Vervolgens bouwen we een transactie die probeert alle knooppunten van de subboom in de juiste volgorde te verwijderen. Natuurlijk kunnen er veranderingen optreden tussen het lezen van een subboom en het verwijderen ervan. In dit geval mislukt de transactie. Bovendien kan de subboom tijdens het leesproces veranderen. Een verzoek voor de kinderen van het volgende knooppunt kan een foutmelding opleveren als dit knooppunt bijvoorbeeld al is verwijderd. In beide gevallen herhalen we het hele proces opnieuw.

Deze aanpak maakt het verwijderen van een sleutel zeer ineffectief als deze kinderen heeft, en nog meer als de toepassing blijft werken met de subboom, waarbij sleutels worden verwijderd en gemaakt. Hierdoor konden we echter voorkomen dat de implementatie van andere methoden in etcd en Consul ingewikkeld werd.

ingesteld in ZooKeeper

In ZooKeeper zijn er aparte methoden die werken met de boomstructuur (create, delete, getChildren) en die werken met data in knooppunten (setData, getData). Bovendien hebben alle methoden strikte randvoorwaarden: create retourneert een foutmelding als het knooppunt al aangemaakt, verwijder of setData – als deze nog niet bestaat. We hadden een vaste methode nodig die kan worden aangeroepen zonder na te denken over de aanwezigheid van een sleutel.

Eén optie is om een ​​optimistische benadering te kiezen, zoals bij verwijdering. Controleer of er een knooppunt bestaat. Indien aanwezig, roep setData aan, anders create. Als de laatste methode een fout retourneert, herhaalt u deze helemaal opnieuw. Het eerste dat moet worden opgemerkt, is dat de bestaanstest zinloos is. U kunt onmiddellijk Create bellen. Succesvolle voltooiing betekent dat het knooppunt niet bestond en is gemaakt. Anders retourneert create de juiste fout, waarna u setData moet aanroepen. Tussen aanroepen door kan een hoekpunt natuurlijk worden verwijderd door een concurrerende aanroep, en setData zou ook een fout retourneren. In dit geval kunt u het helemaal opnieuw doen, maar is het het waard?

Als beide methoden een fout retourneren, weten we zeker dat er een concurrerende verwijdering heeft plaatsgevonden. Laten we ons voorstellen dat deze verwijdering plaatsvond na het aanroepen van set. Dan is de betekenis die we proberen vast te stellen al gewist. Dit betekent dat we kunnen aannemen dat de set met succes is uitgevoerd, zelfs als er in feite niets is geschreven.

Meer technische details

In deze sectie nemen we een pauze van gedistribueerde systemen en praten we over coderen.
Eén van de belangrijkste eisen van de klant was cross-platform: minimaal één van de diensten moet ondersteund worden op Linux, MacOS en Windows. Aanvankelijk ontwikkelden we alleen voor Linux, en later begonnen we met testen op andere systemen. Dit veroorzaakte veel problemen, waarvan het enige tijd volkomen onduidelijk was hoe deze moesten worden aangepakt. Als gevolg hiervan worden alle drie de coördinatiediensten nu ondersteund op Linux en MacOS, terwijl alleen Consul KV wordt ondersteund op Windows.

Vanaf het allereerste begin hebben we geprobeerd kant-en-klare bibliotheken te gebruiken om toegang te krijgen tot services. In het geval van ZooKeeper viel de keuze op ZooKeeper C++, die uiteindelijk niet kon worden gecompileerd op Windows. Dit is echter niet verrassend: de bibliotheek is gepositioneerd als alleen Linux. Voor Consul was dat de enige optie ppconsul. Er moest steun aan worden toegevoegd sessies и transacties. Voor etcd werd geen volwaardige bibliotheek gevonden die de nieuwste versie van het protocol ondersteunt, dus we hebben het eenvoudigweg gedaan gegenereerde grpc-client.

Geïnspireerd door de asynchrone interface van de ZooKeeper C++-bibliotheek, hebben we besloten om ook een asynchrone interface te implementeren. ZooKeeper C++ gebruikt hiervoor future/promise-primitieven. In STL worden ze helaas zeer bescheiden geïmplementeerd. Nee, bijvoorbeeld dan methode, die de doorgegeven functie toepast op het resultaat van de toekomst wanneer deze beschikbaar komt. In ons geval is een dergelijke methode nodig om het resultaat naar het formaat van onze bibliotheek te converteren. Om dit probleem te omzeilen, moesten we onze eigen eenvoudige threadpool implementeren, omdat we op verzoek van de klant geen gebruik konden maken van zware bibliotheken van derden, zoals Boost.

Onze toenmalige implementatie werkt als volgt. Wanneer u wordt gebeld, wordt er een extra belofte/toekomstpaar gecreëerd. De nieuwe toekomst wordt teruggegeven en de doorgegeven toekomst wordt samen met de bijbehorende functie en een extra belofte in de wachtrij geplaatst. Een thread uit de pool selecteert verschillende futures uit de wachtrij en peilt deze met behulp van wait_for. Wanneer een resultaat beschikbaar komt, wordt de corresponderende functie aangeroepen en wordt de retourwaarde ervan doorgegeven aan de belofte.

We gebruikten dezelfde threadpool om queries uit te voeren naar etcd en Consul. Dit betekent dat de onderliggende bibliotheken toegankelijk zijn via meerdere verschillende threads. ppconsul is niet thread-safe, dus oproepen ernaar worden beschermd door sloten.
Je kunt met grpc vanuit meerdere threads werken, maar er zijn subtiliteiten. In etcd worden horloges geïmplementeerd via grpc-streams. Dit zijn bidirectionele kanalen voor berichten van een bepaald type. De bibliotheek creëert één thread voor alle horloges en één thread die inkomende berichten verwerkt. Grpc verbiedt dus parallelle schrijfbewerkingen om te streamen. Dit betekent dat u bij het initialiseren of verwijderen van een horloge moet wachten tot het vorige verzoek is verzonden voordat u het volgende verzendt. Wij gebruiken voor synchronisatie voorwaardelijke variabelen.

Totaal

Kijk zelf maar: liboffkv.

Ons team: Raed Romanov, Ivan Glushenkov, Dmitri Kamaldinov, Victor Krapivenski, Vitaly Ivanin.

Bron: www.habr.com

Voeg een reactie