Cinc estudiants i tres botigues de valor-clau distribuïdes

O com vam escriure una biblioteca C++ client per a ZooKeeper, etcd i Consul KV

En el món dels sistemes distribuïts, hi ha una sèrie de tasques típiques: emmagatzemar informació sobre la composició del clúster, gestionar la configuració dels nodes, detectar nodes defectuosos, triar un líder. un altre. Per solucionar aquests problemes, s'han creat sistemes especials distribuïts: serveis de coordinació. Ara ens interessarà tres d'ells: ZooKeeper, etcd i Consul. De tota la rica funcionalitat de Consul, ens centrarem en Consul KV.

Cinc estudiants i tres botigues de valor-clau distribuïdes

En essència, tots aquests sistemes són magatzems de valor-clau linearitzables i tolerants a errors. Encara que els seus models de dades presenten diferències significatives, que comentarem més endavant, resolen els mateixos problemes pràctics. Òbviament, cada aplicació que utilitza el servei de coordinació està lligada a una d'elles, fet que pot comportar la necessitat de donar suport a diversos sistemes en un mateix centre de dades que resolguin els mateixos problemes per a diferents aplicacions.

La idea de resoldre aquest problema va sorgir en una agència de consultoria australiana, i ens va tocar a nosaltres, un petit equip d'estudiants, posar-lo en pràctica, que és del que parlaré.

Hem aconseguit crear una biblioteca que proporciona una interfície comuna per treballar amb ZooKeeper, etcd i Consul KV. La biblioteca està escrita en C++, però hi ha plans per portar-la a altres idiomes.

Models de dades

Per desenvolupar una interfície comuna per a tres sistemes diferents, cal entendre què tenen en comú i en què es diferencien. Anem a esbrinar-ho.

ZooKeeper

Cinc estudiants i tres botigues de valor-clau distribuïdes

Les claus estan organitzades en un arbre i s'anomenen nodes. En conseqüència, per a un node podeu obtenir una llista dels seus fills. Les operacions de crear un znode (crear) i canviar un valor (setData) estan separades: només es poden llegir i canviar les claus existents. Els rellotges es poden adjuntar a les operacions de comprovar l'existència d'un node, llegir un valor i obtenir fills. Watch és un activador únic que s'activa quan canvia la versió de les dades corresponents al servidor. Els nodes efímers s'utilitzen per detectar fallades. Estan lligats a la sessió del client que els ha creat. Quan un client tanca una sessió o deixa de notificar a ZooKeeper de la seva existència, aquests nodes s'eliminen automàticament. S'admeten transaccions simples: un conjunt d'operacions que totes tenen èxit o fracassen si això no és possible per a almenys una d'elles.

etc.

Cinc estudiants i tres botigues de valor-clau distribuïdes

Els desenvolupadors d'aquest sistema es van inspirar clarament en ZooKeeper i, per tant, ho van fer tot de manera diferent. No hi ha jerarquia de claus, però formen un conjunt ordenat lexicogràficament. Podeu obtenir o eliminar totes les claus que pertanyen a un interval determinat. Aquesta estructura pot semblar estranya, però en realitat és molt expressiva i a través d'ella es pot emular fàcilment una visió jeràrquica.

etcd no té una operació estàndard de comparació i configuració, però té alguna cosa millor: transaccions. Per descomptat, existeixen en els tres sistemes, però les transaccions etcd són especialment bones. Consten de tres blocs: control, èxit, fracàs. El primer bloc conté un conjunt de condicions, el segon i el tercer - operacions. La transacció s'executa atòmicament. Si totes les condicions són certes, s'executa el bloc d'èxit, en cas contrari s'executa el bloc d'error. A l'API 3.3, els blocs d'èxit i error poden contenir transaccions imbricades. És a dir, és possible executar atòmicament construccions condicionals de nivell d'imbricació gairebé arbitrari. Podeu obtenir més informació sobre quins controls i operacions existeixen documentació.

Els rellotges també existeixen aquí, tot i que són una mica més complicats i són reutilitzables. És a dir, després d'instal·lar un rellotge en un rang de claus, rebreu totes les actualitzacions d'aquest rang fins que cancel·leu el rellotge, i no només la primera. A etcd, l'anàleg de les sessions de client de ZooKeeper són arrendaments.

Cònsol K.V.

Aquí tampoc no hi ha una estructura jeràrquica estricta, però Consul pot crear l'aspecte que existeix: podeu obtenir i eliminar totes les claus amb el prefix especificat, és a dir, treballar amb el "subarbre" de la clau. Aquestes consultes s'anomenen recursives. A més, Cònsol només pot seleccionar claus que no continguin el caràcter especificat després del prefix, que correspon a l'obtenció immediata de "fills". Però val la pena recordar que aquesta és precisament l'aparença d'una estructura jeràrquica: és molt possible crear una clau si el seu pare no existeix o esborrar una clau que tingui fills, mentre que els fills continuaran emmagatzemats al sistema.

Cinc estudiants i tres botigues de valor-clau distribuïdes
En lloc de rellotges, Consul ha bloquejat les sol·licituds HTTP. En essència, es tracta de trucades ordinàries al mètode de lectura de dades, per a les quals, juntament amb altres paràmetres, s'indica la darrera versió coneguda de les dades. Si la versió actual de les dades corresponents al servidor és superior a l'especificada, la resposta es retorna immediatament, en cas contrari, quan canvia el valor. També hi ha sessions que es poden adjuntar a les claus en qualsevol moment. Val la pena assenyalar que, a diferència d'etcd i ZooKeeper, on la supressió de sessions comporta la supressió de claus associades, hi ha un mode en què la sessió simplement es desenllaça d'elles. Disponible transaccions, sense branques, però amb tot tipus de xecs.

Ajuntant-ho tot

ZooKeeper té el model de dades més rigorós. Les consultes d'interval expressiu disponibles a etcd no es poden emular de manera efectiva ni a ZooKeeper ni a Cònsol. Intentant incorporar el millor de tots els serveis, vam acabar amb una interfície gairebé equivalent a la interfície ZooKeeper amb les següents excepcions significatives:

  • seqüència, contenidor i nodes TTL no compatible
  • Les ACL no són compatibles
  • el mètode set crea una clau si no existeix (en ZK setData retorna un error en aquest cas)
  • Els mètodes set i cas estan separats (en ZK són essencialment el mateix)
  • el mètode d'esborrar suprimeix un node juntament amb el seu subarbre (en ZK, elete retorna un error si el node té fills)
  • Per a cada clau només hi ha una versió: la versió del valor (en ZK n’hi ha tres)

El rebuig dels nodes seqüencials es deu al fet que etcd i Consul no tenen suport integrat per a ells, i l'usuari els pot implementar fàcilment a la part superior de la interfície de la biblioteca resultant.

La implementació d'un comportament similar a ZooKeeper quan s'elimina un vèrtex requeriria mantenir un comptador fill separat per a cada clau a etcd i Consul. Com que vam intentar evitar emmagatzemar la metainformació, es va decidir eliminar tot el subarbre.

Subtileses de la implementació

Fem una ullada més de prop a alguns aspectes de la implementació de la interfície de biblioteca en diferents sistemes.

Jerarquia en etcd

Mantenir una visió jeràrquica a etcd va resultar ser una de les tasques més interessants. Les consultes d'interval faciliten la recuperació d'una llista de claus amb un prefix especificat. Per exemple, si necessiteu tot el que comença "/foo", estàs demanant un rang ["/foo", "/fop"). Però això retornaria tot el subarbre de la clau, cosa que pot no ser acceptable si el subarbre és gran. Al principi teníem previst utilitzar un mecanisme de traducció clau, implementat a zetcd. Implica afegir un byte al principi de la clau, igual a la profunditat del node de l'arbre. Permeteu-me que us posi un exemple.

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

A continuació, obteniu tots els fills immediats de la clau "/foo" possible sol·licitant un rang ["u02/foo/", "u02/foo0"). Sí, en ASCII "0" es troba just després "/".

Però, com implementar l'eliminació d'un vèrtex en aquest cas? Resulta que cal esborrar tots els intervals del tipus ["uXX/foo/", "uXX/foo0") per XX de 01 a FF. I després ens vam topar límit de nombre d'operacions dins d'una transacció.

Com a resultat, es va inventar un senzill sistema de conversió de claus, que va permetre implementar eficaçment tant l'eliminació d'una clau com l'obtenció d'una llista de nens. N'hi ha prou amb afegir un caràcter especial abans de l'últim testimoni. Per exemple:

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

A continuació, esborra la clau "/very" es converteix en supressió "/u00very" i rang ["/very/", "/very0"), i aconseguir tots els nens - en una sol·licitud de claus de la gamma ["/very/u00", "/very/u01").

Eliminació d'una clau a ZooKeeper

Com ja he comentat, a ZooKeeper no es pot eliminar un node si té fills. Volem eliminar la clau juntament amb el subarbre. Que hauria de fer? Ho fem amb optimisme. Primer, recorrem recursivament el subarbre, obtenint els fills de cada vèrtex amb una consulta separada. A continuació, construïm una transacció que intenta suprimir tots els nodes del subarbre en l'ordre correcte. Per descomptat, es poden produir canvis entre llegir un subarbre i suprimir-lo. En aquest cas, la transacció fallarà. A més, el subarbre pot canviar durant el procés de lectura. Una sol·licitud per als fills del següent node pot retornar un error si, per exemple, aquest node ja s'ha suprimit. En tots dos casos, tornem a repetir tot el procés.

Aquest enfocament fa que l'eliminació d'una clau sigui molt ineficaç si té fills, i més encara si l'aplicació continua funcionant amb el subarbre, eliminant i creant claus. Tanmateix, això ens va permetre evitar complicar la implementació d'altres mètodes a etcd i Consul.

ambientat a ZooKeeper

A ZooKeeper hi ha mètodes separats que funcionen amb l'estructura d'arbre (create, delete, getChildren) i que funcionen amb dades en nodes (setData, getData) A més, tots els mètodes tenen unes condicions prèvies estrictes: create retornarà un error si el node ja ho ha fet. s'ha creat, suprimiu o setData, si encara no existeix. Necessitàvem un mètode conjunt que es pugui cridar sense pensar en la presència d'una clau.

Una opció és adoptar un enfocament optimista, com amb la supressió. Comproveu si existeix un node. Si existeix, truqueu a setData, en cas contrari, creeu. Si l'últim mètode va retornar un error, repetiu-ho tot de nou. El primer que cal destacar és que la prova d'existència no té sentit. Podeu trucar immediatament a create. La finalització correcta significarà que el node no existia i es va crear. En cas contrari, create retornarà l'error adequat, després del qual haureu de cridar a setData. Per descomptat, entre trucades, un vèrtex podria ser suprimit per una trucada competidora i setData també retornaria un error. En aquest cas, podeu tornar a fer-ho tot, però val la pena?

Si tots dos mètodes retornen un error, sabem del cert que s'ha produït una supressió competitiva. Imaginem que aquesta supressió es va produir després de trucar a set. Aleshores, qualsevol significat que estem intentant establir ja s'esborra. Això vol dir que podem suposar que el conjunt s'ha executat correctament, encara que de fet no s'ha escrit res.

Més detalls tècnics

En aquesta secció farem un descans dels sistemes distribuïts i parlarem de la codificació.
Un dels principals requisits del client era multiplataforma: almenys un dels serveis ha de ser compatible amb Linux, MacOS i Windows. Inicialment, vam desenvolupar només per a Linux, i més tard vam començar a provar en altres sistemes. Això va causar molts problemes, que durant un temps no tenien clar com abordar. Com a resultat, els tres serveis de coordinació ara són compatibles amb Linux i MacOS, mentre que només el Consul KV és compatible amb Windows.

Des del primer moment, vam intentar utilitzar biblioteques ja fetes per accedir als serveis. En el cas de ZooKeeper, l'elecció va recaure ZooKeeper C++, que finalment no es va poder compilar a Windows. Això, però, no és d'estranyar: la biblioteca es posiciona com a només per a Linux. Per al cònsol l'única opció era ppconsul. S'hi havia d'afegir suport sessions и transaccions. Per a etcd, no s'ha trobat una biblioteca completa que admeti la darrera versió del protocol, de manera que simplement client grpc generat.

Inspirats per la interfície asíncrona de la biblioteca ZooKeeper C++, vam decidir implementar també una interfície asíncrona. ZooKeeper C++ utilitza primitives futures/promeses per a això. En STL, malauradament, s'implementen molt modestament. Per exemple, no després mètode, que aplica la funció passada al resultat del futur quan estigui disponible. En el nostre cas, aquest mètode és necessari per convertir el resultat al format de la nostra biblioteca. Per evitar aquest problema, vam haver d'implementar el nostre propi grup de fils senzill, ja que a petició del client no podíem utilitzar biblioteques pesades de tercers com Boost.

La nostra implementació llavors funciona així. Quan es truca, es crea una parella promesa/futur addicional. Es retorna el nou futur i el passat es col·loca juntament amb la funció corresponent i una promesa addicional a la cua. Un fil del grup selecciona diversos futurs de la cua i els enquesta mitjançant wait_for. Quan un resultat està disponible, es crida a la funció corresponent i el seu valor de retorn es passa a la promesa.

Hem utilitzat el mateix grup de fils per executar consultes a etcd i Consul. Això vol dir que es pot accedir a les biblioteques subjacents mitjançant diversos fils diferents. ppconsul no és segur per a threads, de manera que les trucades estan protegides per bloquejos.
Podeu treballar amb grpc des de diversos fils, però hi ha subtileses. En etcd els rellotges s'implementen mitjançant fluxos grpc. Són canals bidireccionals per a missatges d'un determinat tipus. La biblioteca crea un únic fil per a tots els rellotges i un únic fil que processa els missatges entrants. Per tant, grpc prohibeix les escriptures paral·leles al flux. Això vol dir que en inicialitzar o suprimir un rellotge, heu d'esperar fins que s'hagi completat l'enviament de la sol·licitud anterior abans d'enviar la següent. Utilitzem per a la sincronització variables condicionals.

Total

Mireu: liboffkv.

El nostre equip: Raed Romanov, Ivan Gluixenkov, Dmitri Kamaldinov, Víctor Krapivensky, Vitali Ivanin.

Font: www.habr.com

Afegeix comentari