Cinci studenți și trei magazine cheie-valoare distribuite

Sau cum am scris o bibliotecă C++ client pentru ZooKeeper, etcd și Consul KV

În lumea sistemelor distribuite, există o serie de sarcini tipice: stocarea informațiilor despre compoziția clusterului, gestionarea configurației nodurilor, detectarea nodurilor defecte, alegerea unui lider alte. Pentru rezolvarea acestor probleme au fost create sisteme speciale distribuite - servicii de coordonare. Acum ne vor interesa trei dintre ele: ZooKeeper, etcd și Consul. Dintre toate funcționalitățile bogate ale Consul, ne vom concentra pe Consul KV.

Cinci studenți și trei magazine cheie-valoare distribuite

În esență, toate aceste sisteme sunt depozite cheie-valoare liniizabile, tolerante la erori. Deși modelele lor de date au diferențe semnificative, despre care vom discuta mai târziu, ele rezolvă aceleași probleme practice. Evident, fiecare aplicație care folosește serviciul de coordonare este legată de una dintre ele, ceea ce poate duce la necesitatea suportării mai multor sisteme într-un singur centru de date care rezolvă aceleași probleme pentru aplicații diferite.

Ideea de a rezolva această problemă a apărut la o agenție de consultanță australiană și ne-a revenit nouă, o echipă mică de studenți, să o implementăm, despre care voi vorbi.

Am reușit să creăm o bibliotecă care oferă o interfață comună pentru lucrul cu ZooKeeper, etcd și Consul KV. Biblioteca este scrisă în C++, dar există planuri de a o porta în alte limbi.

Modele de date

Pentru a dezvolta o interfață comună pentru trei sisteme diferite, trebuie să înțelegeți ce au în comun și cum diferă. Să ne dăm seama.

Ingrijitor zoo

Cinci studenți și trei magazine cheie-valoare distribuite

Cheile sunt organizate într-un arbore și se numesc noduri. În consecință, pentru un nod puteți obține o listă a copiilor săi. Operațiile de creare a unui znode (creare) și de modificare a unei valori (setData) sunt separate: doar cheile existente pot fi citite și modificate. Ceasurile pot fi atașate la operațiunile de verificare a existenței unui nod, citirea unei valori și obținerea de copii. Watch este un declanșator unic care se declanșează atunci când versiunea datelor corespunzătoare de pe server se modifică. Nodurile efemere sunt folosite pentru a detecta defecțiunile. Sunt legate de sesiunea clientului care le-a creat. Când un client închide o sesiune sau încetează să notifice ZooKeeper despre existența acesteia, aceste noduri sunt șterse automat. Sunt acceptate tranzacții simple - un set de operațiuni care fie reușesc, fie eșuează dacă acest lucru nu este posibil pentru cel puțin una dintre ele.

etcd

Cinci studenți și trei magazine cheie-valoare distribuite

Dezvoltatorii acestui sistem au fost în mod clar inspirați de ZooKeeper și, prin urmare, au făcut totul diferit. Nu există o ierarhie a cheilor, dar ele formează un set ordonat lexicografic. Puteți obține sau șterge toate cheile aparținând unui anumit interval. Această structură poate părea ciudată, dar este de fapt foarte expresivă, iar o viziune ierarhică poate fi emulată cu ușurință prin ea.

etcd nu are o operațiune standard de comparare și setare, dar are ceva mai bun: tranzacții. Desigur, ele există în toate cele trei sisteme, dar tranzacțiile etcd sunt deosebit de bune. Ele constau din trei blocuri: verificare, succes, eșec. Primul bloc conține un set de condiții, al doilea și al treilea - operații. Tranzacția este executată atomic. Dacă toate condițiile sunt adevărate, atunci blocul de succes este executat, în caz contrar blocul de eșec este executat. În API 3.3, blocurile de succes și eșec pot conține tranzacții imbricate. Adică, este posibil să se execute atomic constructe condiționate de un nivel de imbricare aproape arbitrar. Puteți afla mai multe despre ce verificări și operațiuni există documentație.

Ceasurile există și aici, deși sunt puțin mai complicate și sunt reutilizabile. Adică, după instalarea unui ceas pe o gamă de chei, vei primi toate actualizările din această gamă până când anulezi ceasul, și nu doar prima. În etcd, analogul sesiunilor client ZooKeeper sunt leasing.

Consulul K.V.

De asemenea, nu există nicio structură ierarhică strictă aici, dar Consul poate crea aspectul că există: puteți obține și șterge toate cheile cu prefixul specificat, adică să lucrați cu „subarborele” cheii. Astfel de interogări sunt numite recursive. În plus, Consul poate selecta doar cheile care nu conțin caracterul specificat după prefix, care corespunde obținerii imediate de „copii”. Dar merită să ne amintim că acesta este tocmai aspectul unei structuri ierarhice: este foarte posibil să se creeze o cheie dacă părintele ei nu există sau să șterge o cheie care are copii, în timp ce copiii vor continua să fie stocați în sistem.

Cinci studenți și trei magazine cheie-valoare distribuite
În loc de ceasuri, Consul are blocarea solicitărilor HTTP. În esență, acestea sunt apeluri obișnuite la metoda de citire a datelor, pentru care, împreună cu alți parametri, este indicată ultima versiune cunoscută a datelor. Dacă versiunea curentă a datelor corespunzătoare de pe server este mai mare decât cea specificată, răspunsul este returnat imediat, în caz contrar - când valoarea se schimbă. Există, de asemenea, sesiuni care pot fi atașate la chei în orice moment. Este de remarcat faptul că, spre deosebire de etcd și ZooKeeper, unde ștergerea sesiunilor duce la ștergerea cheilor asociate, există un mod în care sesiunea este pur și simplu deconectată de la acestea. Disponibil tranzacții, fara ramuri, dar cu tot felul de verificari.

Punând totul împreună

ZooKeeper are cel mai riguros model de date. Interogările expresive disponibile în etcd nu pot fi emulate în mod eficient nici în ZooKeeper, nici în Consul. Încercând să încorporăm tot ce este mai bun din toate serviciile, am ajuns să avem o interfață aproape echivalentă cu interfața ZooKeeper, cu următoarele excepții semnificative:

  • secvență, container și noduri TTL nu sunt acceptate
  • ACL-urile nu sunt acceptate
  • metoda set creează o cheie dacă nu există (în ZK setData returnează o eroare în acest caz)
  • metodele set și cas sunt separate (în ZK sunt în esență același lucru)
  • metoda de ștergere șterge un nod împreună cu subarborele său (în ZK ștergerea returnează o eroare dacă nodul are copii)
  • Pentru fiecare cheie există o singură versiune - versiunea cu valoare (în ZK sunt trei dintre ei)

Respingerea nodurilor secvențiale se datorează faptului că etcd și Consul nu au suport încorporat pentru ele și pot fi implementate cu ușurință de către utilizator pe deasupra interfeței bibliotecii rezultate.

Implementarea unui comportament similar cu ZooKeeper atunci când ștergeți un vârf ar necesita menținerea unui contor copil separat pentru fiecare cheie în etcd și Consul. Deoarece am încercat să evităm stocarea meta informațiilor, s-a decis ștergerea întregului subarbor.

Subtilități ale implementării

Să aruncăm o privire mai atentă asupra unor aspecte ale implementării interfeței bibliotecii în diferite sisteme.

Ierarhia în etcd

Menținerea unei viziuni ierarhice în etcd s-a dovedit a fi una dintre cele mai interesante sarcini. Interogările de interval facilitează preluarea unei liste de chei cu un prefix specificat. De exemplu, dacă aveți nevoie de tot ceea ce începe cu "/foo", ceri un interval ["/foo", "/fop"). Dar acest lucru ar returna întregul subarboresc al cheii, ceea ce poate să nu fie acceptabil dacă subarborele este mare. La început am plănuit să folosim un mecanism de traducere cheie, implementat în zetcd. Implică adăugarea unui octet la începutul cheii, egal cu adâncimea nodului din arbore. Să vă dau un exemplu.

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

Apoi obțineți toți copiii imediati ai cheii "/foo" posibil prin solicitarea unui interval ["u02/foo/", "u02/foo0"). Da, în ASCII "0" stă imediat după "/".

Dar cum se implementează eliminarea unui vârf în acest caz? Se pare că trebuie să ștergeți toate intervalele de tip ["uXX/foo/", "uXX/foo0") pentru XX de la 01 la FF. Și apoi am dat peste limita numărului de operațiuni în cadrul unei singure tranzacții.

Ca urmare, a fost inventat un sistem simplu de conversie a cheilor, care a făcut posibilă implementarea eficientă atât a ștergerii unei chei, cât și a obținerii unei liste de copii. Este suficient să adăugați un caracter special înainte de ultimul simbol. De exemplu:

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

Apoi ștergerea cheii "/very" se transformă în ștergere "/u00very" și gamă ["/very/", "/very0"), și obținerea tuturor copiilor - într-o cerere de chei din gamă ["/very/u00", "/very/u01").

Scoaterea unei chei din ZooKeeper

După cum am menționat deja, în ZooKeeper nu puteți șterge un nod dacă are copii. Vrem să ștergem cheia împreună cu subarborele. Ce ar trebuii să fac? Facem asta cu optimism. În primul rând, parcurgem recursiv subarborele, obținând copiii fiecărui vârf cu o interogare separată. Apoi construim o tranzacție care încearcă să ștergă toate nodurile subarborelui în ordinea corectă. Desigur, pot apărea modificări între citirea unui subarboresc și ștergerea acestuia. În acest caz, tranzacția va eșua. Mai mult, subarborele se poate schimba în timpul procesului de citire. O solicitare pentru copiii următorului nod poate returna o eroare dacă, de exemplu, acest nod a fost deja șters. În ambele cazuri, repetăm ​​din nou întregul proces.

Această abordare face ca ștergerea unei chei să fie foarte ineficientă dacă are copii și cu atât mai mult dacă aplicația continuă să funcționeze cu subarborele, ștergând și creând chei. Totuși, acest lucru ne-a permis să evităm complicarea implementării altor metode în etcd și Consul.

stabilit în ZooKeeper

În ZooKeeper există metode separate care lucrează cu structura arborescentă (create, delete, getChildren) și care funcționează cu date în noduri (setData, getData).Mai mult, toate metodele au precondiții stricte: create va returna o eroare dacă nodul are deja fost creat, șterge sau setData – dacă nu există deja. Aveam nevoie de o metodă setată care să poată fi apelată fără să ne gândim la prezența unei chei.

O opțiune este să adoptați o abordare optimistă, ca și în cazul ștergerii. Verificați dacă există un nod. Dacă există, apelați setData, altfel creați. Dacă ultima metodă a returnat o eroare, repetați-o din nou. Primul lucru de remarcat este că testul existenței este inutil. Puteți apela imediat la create. Finalizarea cu succes va însemna că nodul nu a existat și a fost creat. În caz contrar, create va returna eroarea corespunzătoare, după care trebuie să apelați setData. Desigur, între apeluri, un vârf ar putea fi șters de un apel concurent, iar setData ar returna, de asemenea, o eroare. În acest caz, puteți face totul din nou, dar merită?

Dacă ambele metode returnează o eroare, atunci știm sigur că a avut loc o ștergere concurentă. Să ne imaginăm că această ștergere a avut loc după apelarea setului. Atunci orice sens pe care încercăm să-l stabilim este deja șters. Aceasta înseamnă că putem presupune că setul a fost executat cu succes, chiar dacă de fapt nu a fost scris nimic.

Mai multe detalii tehnice

În această secțiune vom face o pauză de la sistemele distribuite și vom vorbi despre codificare.
Una dintre principalele cerințe ale clientului a fost cross-platform: cel puțin unul dintre servicii trebuie să fie suportat pe Linux, MacOS și Windows. Inițial, am dezvoltat doar pentru Linux, iar mai târziu am început testarea pe alte sisteme. Acest lucru a cauzat o mulțime de probleme, care de ceva timp au fost complet neclare cum să abordeze. Drept urmare, toate cele trei servicii de coordonare sunt acum acceptate pe Linux și MacOS, în timp ce numai Consul KV este acceptat pe Windows.

Încă de la început, am încercat să folosim biblioteci gata făcute pentru a accesa servicii. În cazul ZooKeeper, alegerea a revenit ZooKeeper C++, care în cele din urmă nu a reușit să se compileze pe Windows. Acest lucru, totuși, nu este surprinzător: biblioteca este poziționată doar pentru Linux. Pentru Consul singura variantă era ppconsul. A trebuit să i se adauge suport sesiuni и tranzacții. Pentru etcd, nu a fost găsită o bibliotecă cu drepturi depline care să suporte cea mai recentă versiune a protocolului, așa că pur și simplu client grpc generat.

Inspirați de interfața asincronă a bibliotecii ZooKeeper C++, am decis să implementăm și o interfață asincronă. ZooKeeper C++ folosește primitive viitor/promisiune pentru aceasta. În STL, din păcate, acestea sunt implementate foarte modest. De exemplu, nu apoi metoda, care aplică funcția transmisă rezultatului viitorului atunci când acesta devine disponibil. În cazul nostru, o astfel de metodă este necesară pentru a converti rezultatul în formatul bibliotecii noastre. Pentru a rezolva această problemă, a trebuit să implementăm propriul nostru pool de fire simplu, deoarece la cererea clientului nu am putut folosi biblioteci grele de la terți, cum ar fi Boost.

Implementarea noastră de atunci funcționează așa. Când este apelat, este creată o pereche suplimentară promisiune/viitoare. Noul viitor este returnat, iar cel trecut este plasat împreună cu funcția corespunzătoare și o promisiune suplimentară în coadă. Un fir din grup selectează mai multe futures din coadă și le interoghează folosind wait_for. Când un rezultat devine disponibil, funcția corespunzătoare este apelată și valoarea sa returnată este transmisă promisiunii.

Am folosit același pool de fire pentru a executa interogări către etcd și Consul. Aceasta înseamnă că bibliotecile subiacente pot fi accesate de mai multe fire diferite. ppconsul nu este sigur pentru fire, așa că apelurile către acesta sunt protejate de blocări.
Puteți lucra cu grpc din mai multe fire, dar există subtilități. În etcd ceasurile sunt implementate prin fluxuri grpc. Acestea sunt canale bidirecționale pentru mesaje de un anumit tip. Biblioteca creează un singur fir pentru toate ceasurile și un singur fir care procesează mesajele primite. Prin urmare, grpc interzice scrierea paralelă în flux. Aceasta înseamnă că atunci când inițializați sau ștergeți un ceas, trebuie să așteptați până când cererea anterioară s-a terminat de trimitere înainte de a o trimite pe următoarea. Folosim pentru sincronizare variabile condiționale.

Total

Vezi pentru tine: liboffkv.

Echipa noastră: Raed Romanov, Ivan Glușenkov, Dmitri Kamaldinov, Victor Krapivensky, Vitali Ivanin.

Sursa: www.habr.com

Adauga un comentariu