Öt diák és három elosztott kulcsérték-tárhely

Vagy hogyan írtunk egy kliens C++ könyvtárat a ZooKeeper, etcd és Consul KV számára

Az elosztott rendszerek világában számos tipikus feladat létezik: információ tárolása a klaszter összetételéről, csomópontok konfigurációjának kezelése, hibás csomópontok észlelése, vezető kiválasztása és mások. E problémák megoldására speciális elosztott rendszereket hoztak létre - koordinációs szolgáltatásokat. Most ezek közül háromra leszünk kíváncsiak: ZooKeeper, etcd és Consul. A Consul gazdag funkcionalitása közül a Consul KV-ra fogunk összpontosítani.

Öt diák és három elosztott kulcsérték-tárhely

Lényegében ezek a rendszerek hibatűrő, linearizálható kulcsérték-tárolók. Bár adatmodelljeik jelentős eltéréseket mutatnak, amelyekről később még szó lesz, ugyanazokat a gyakorlati problémákat oldják meg. Nyilvánvaló, hogy minden egyes koordinációs szolgáltatást használó alkalmazás valamelyikhez van kötve, ami ahhoz vezethet, hogy egy adatközpontban több olyan rendszert kell támogatni, amelyek ugyanazokat a problémákat oldják meg különböző alkalmazásokhoz.

A probléma megoldásának ötlete egy ausztrál tanácsadó ügynökségtől származik, és ránk, egy kis diákcsapatra hárult a megvalósítás, amiről most beszélek.

Sikerült létrehoznunk egy olyan könyvtárat, amely közös felületet biztosít a ZooKeeper, etcd és Consul KV használatához. A könyvtár C++ nyelven íródott, de a tervek között szerepel, hogy más nyelvekre is portolják.

Adatmodellek

Három különböző rendszer közös interfészének kialakításához meg kell értenie, mi a közös bennük, és miben különböznek egymástól. Találjuk ki.

Állatgondozó

Öt diák és három elosztott kulcsérték-tárhely

A kulcsok fába vannak rendezve, és csomópontoknak nevezzük. Ennek megfelelően egy csomóponthoz megkaphatja a gyermekeinek listáját. A znode létrehozásának (create) és az érték megváltoztatásának (setData) műveletei elkülönülnek: csak a meglévő kulcsok olvashatók és módosíthatók. Órák kapcsolhatók a csomópont meglétének ellenőrzésére, egy érték leolvasására és a gyerekek megszerzésére. A Watch egy egyszeri eseményindító, amely akkor aktiválódik, amikor a megfelelő adatok verziója megváltozik a kiszolgálón. A mulandó csomópontokat a hibák észlelésére használják. Az őket létrehozó ügyfél munkamenetéhez kötődnek. Amikor egy ügyfél bezár egy munkamenetet, vagy nem értesíti a ZooKeepert a létezéséről, ezek a csomópontok automatikusan törlődnek. Az egyszerű tranzakciók támogatottak – olyan műveletek halmaza, amelyek mindegyike sikeres vagy sikertelen, ha ez legalább az egyiknél nem lehetséges.

stb

Öt diák és három elosztott kulcsérték-tárhely

A rendszer fejlesztőit egyértelműen a ZooKeeper ihlette, ezért mindent másképp csináltak. A kulcsoknak nincs hierarchiája, hanem lexikográfiailag rendezett halmazt alkotnak. Egy adott tartományhoz tartozó összes kulcsot beszerezhet vagy törölhet. Ez a szerkezet furcsának tűnhet, de valójában nagyon kifejező, és egy hierarchikus nézet könnyen utánozható rajta.

Az etcd-nek nincs szabványos összehasonlítási és beállítási művelete, de van valami jobb is: a tranzakciók. Természetesen mindhárom rendszerben léteznek, de az etcd tranzakciók kifejezetten jók. Három blokkból állnak: ellenőrzés, siker, kudarc. Az első blokk egy sor feltételt tartalmaz, a második és a harmadik - műveleteket. A tranzakció atomszerűen kerül végrehajtásra. Ha minden feltétel igaz, akkor a sikerblokk végrehajtásra kerül, ellenkező esetben a hibablokk. Az API 3.3-ban a siker és a kudarc blokkok tartalmazhatnak beágyazott tranzakciókat. Azaz szinte tetszőleges beágyazási szintű feltételes konstrukciók atomi végrehajtása lehetséges. További információ arról, hogy milyen ellenőrzések és műveletek léteznek dokumentáció.

Órák itt is léteznek, bár kicsit bonyolultabbak és újrafelhasználhatók. Ez azt jelenti, hogy miután egy órát telepített egy kulcstartományra, az összes frissítést megkapja ebben a tartományban, amíg le nem mondja az órát, és nem csak az elsőt. Az etcd-ben a ZooKeeper kliens munkamenetek analógja a bérlet.

konzul K.V.

Itt sincs szigorú hierarchikus struktúra, de a Consul azt a látszatot keltheti, hogy létezik: az összes kulcsot megkaphatja és törölheti a megadott előtaggal, vagyis a kulcs „részfájával” dolgozhat. Az ilyen lekérdezéseket rekurzívnak nevezzük. Ezenkívül a Consul csak azokat a kulcsokat választhatja ki, amelyek nem tartalmazzák a megadott karaktert az előtag után, ami az azonnali „gyermekek” megszerzésének felel meg. De érdemes megjegyezni, hogy ez pontosan egy hierarchikus struktúra megjelenése: nagyon is lehetséges kulcsot létrehozni, ha nem létezik szülője, vagy törölni kell egy olyan kulcsot, amelynek vannak gyermekei, miközben a gyerekek továbbra is a rendszerben maradnak.

Öt diák és három elosztott kulcsérték-tárhely
Az órák helyett a Consul blokkolja a HTTP kéréseket. Lényegében az adatolvasási metódus közönséges hívásairól van szó, amelyeknél más paraméterekkel együtt az adatok utolsó ismert verziója van feltüntetve. Ha a megfelelő adatok aktuális verziója a szerveren nagyobb, mint a megadott, a válasz azonnal visszaküldésre kerül, ellenkező esetben - az érték megváltozásakor. Vannak olyan munkamenetek is, amelyek bármikor kulcsokhoz kapcsolhatók. Érdemes megjegyezni, hogy az etcd-vel és a ZooKeeperrel ellentétben, ahol a munkamenetek törlése a társított kulcsok törléséhez vezet, van egy mód, amelyben a munkamenet egyszerűen leválasztható róluk. Elérhető tranzakciók, ágak nélkül, de mindenféle csekkel.

Az egészet összerakva

A ZooKeeper rendelkezik a legszigorúbb adatmodellel. Az etcd-ben elérhető kifejező tartománylekérdezések nem emulálhatók hatékonyan sem a ZooKeeperben, sem a Consulban. Igyekeztünk minden szolgáltatásból a legjobbat beépíteni, végül a ZooKeeper felülettel szinte egyenértékű felületet kaptunk, a következő jelentős kivételekkel:

  • szekvencia, tároló és TTL csomópontok Nem támogatott
  • Az ACL-ek nem támogatottak
  • a set metódus létrehoz egy kulcsot, ha az nem létezik (ZK-ban a setData ebben az esetben hibát ad vissza)
  • A set és a cas metódusok el vannak választva (a ZK-ban ezek lényegében ugyanazok)
  • az erase metódus törli a csomópontot a részfával együtt (ZK-ban a delete hibát ad vissza, ha a csomópontnak gyermekei vannak)
  • Minden kulcshoz csak egy verzió tartozik - az értékverzió (ZK-ban hárman vannak)

A szekvenciális csomópontok elutasítása annak köszönhető, hogy az etcd és a Consul nem rendelkezik beépített támogatással, és könnyen implementálható a felhasználó által az így létrejövő könyvtári felületre.

A ZooKeeperhez hasonló viselkedés megvalósításához egy csúcs törlésekor külön gyermekszámlálót kell fenntartani minden egyes kulcshoz az etcd-ben és a Consul-ban. Mivel igyekeztünk elkerülni a metainformációk tárolását, a teljes részfa törlése mellett döntöttünk.

A megvalósítás finomságai

Nézzük meg közelebbről a könyvtári felület különböző rendszerekben történő megvalósításának néhány szempontját.

Hierarchia az etcd-ben

A hierarchikus nézet fenntartása az etcd-ben az egyik legérdekesebb feladatnak bizonyult. A tartománylekérdezések megkönnyítik a megadott előtaggal rendelkező kulcsok listájának lekérését. Például ha mindenre szüksége van, ami azzal kezdődik "/foo", Ön egy tartományt kér ["/foo", "/fop"). Ez azonban a kulcs teljes részfáját adja vissza, ami nem biztos, hogy elfogadható, ha az részfa nagy. Először egy kulcsfontosságú fordítási mechanizmus alkalmazását terveztük, zetcd-ben implementálva. Ez magában foglalja egy bájt hozzáadását a kulcs elejéhez, amely megegyezik a fa csomópontjának mélységével. Hadd mondjak egy példát.

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

Ezután szerezze be a kulcs összes közvetlen gyermekét "/foo" tartomány kérésével lehetséges ["u02/foo/", "u02/foo0"). Igen, ASCII-ben "0" közvetlenül utána áll "/".

De hogyan valósítsuk meg ebben az esetben egy csúcs eltávolítását? Kiderült, hogy törölnie kell a típus összes tartományát ["uXX/foo/", "uXX/foo0") XX 01-től FF-ig. És akkor összefutottunk műveleti számkorlát egy tranzakción belül.

Ennek eredményeként egy egyszerű kulcskonverziós rendszert találtak ki, amely lehetővé tette mind a kulcs törlését, mind a gyermekek listájának megszerzését. Elég egy speciális karaktert hozzáadni az utolsó tokenhez. Például:

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

Ezután a kulcs törlése "/very" törléssé válik "/u00very" és tartomány ["/very/", "/very0"), és minden gyermek megszerzése - kulcskérésre a kínálatból ["/very/u00", "/very/u01").

Kulcs eltávolítása a ZooKeeperben

Mint már említettem, a ZooKeeperben nem törölhet csomópontot, ha annak gyermekei vannak. Törölni akarjuk a kulcsot a részfával együtt. Mit kellene tennem? Ezt optimizmussal tesszük. Először rekurzívan bejárjuk a részfát, és külön lekérdezéssel megkapjuk az egyes csúcsok gyermekeit. Ezután létrehozunk egy tranzakciót, amely megpróbálja törölni a részfa összes csomópontját a megfelelő sorrendben. Természetesen változások történhetnek egy részfa beolvasása és törlése között. Ebben az esetben a tranzakció sikertelen lesz. Ezenkívül a részfa változhat az olvasási folyamat során. A következő csomópont gyermekeire vonatkozó kérés hibát jelezhet, ha például ezt a csomópontot már törölték. Mindkét esetben megismételjük az egész folyamatot.

Ez a megközelítés nagyon hatástalanná teszi a kulcsok törlését, ha gyermekei vannak, és még inkább, ha az alkalmazás továbbra is dolgozik a részfával, törli és hoz létre kulcsokat. Ez azonban lehetővé tette számunkra, hogy elkerüljük az etcd és a Consul egyéb módszereinek megvalósítását.

Állítsa be a ZooKeeperben

A ZooKeeperben külön metódusok működnek a fastruktúrával (létrehozás, törlés, getChildren) és a csomópontokban lévő adatokkal (setData, getData), ráadásul minden metódusnak szigorú előfeltételei vannak: a create hibát ad vissza, ha a csomópont már létrehozva, törölje vagy setData – ha még nem létezik. Szükségünk volt egy beállított metódusra, amelyet anélkül lehet meghívni, hogy gondolkodnánk a kulcs jelenlétéről.

Az egyik lehetőség az optimista megközelítés, mint a törlés esetében. Ellenőrizze, hogy létezik-e csomópont. Ha létezik, hívja meg a setData-t, ellenkező esetben hozza létre. Ha az utolsó módszer hibát adott vissza, ismételje meg az egészet. Az első dolog, amit meg kell jegyezni, hogy a létezési teszt értelmetlen. Azonnal hívhatja a létrehozást. A sikeres befejezés azt jelenti, hogy a csomópont nem létezett, és létrejött. Ellenkező esetben a Create a megfelelő hibát adja vissza, ami után meg kell hívnia a setData-t. Természetesen a hívások között egy csúcsot törölhet egy versengő hívás, és a setData is hibát adna vissza. Ebben az esetben újra meg lehet csinálni, de megéri?

Ha mindkét módszer hibát ad vissza, akkor biztosan tudjuk, hogy versengő törlés történt. Képzeljük el, hogy ez a törlés a set hívása után történt. Ekkor bármilyen jelentést próbálunk megállapítani, az már törlődik. Ez azt jelenti, hogy feltételezhetjük, hogy a halmaz sikeresen végrehajtódott, még akkor is, ha valójában semmit sem írtak.

További technikai részletek

Ebben a részben egy kis szünetet tartunk az elosztott rendszerekben, és beszélünk a kódolásról.
Az ügyfél egyik fő követelménye a többplatformos volt: a szolgáltatások közül legalább egyet támogatnia kell Linuxon, MacOS-en és Windowson. Kezdetben csak Linuxra fejlesztettük, majd később más rendszereken is elkezdtünk tesztelni. Ez sok problémát okozott, amelyek egy ideig teljesen homályosak voltak, hogyan kell megközelíteni. Ennek eredményeként mostantól mindhárom koordinációs szolgáltatás támogatott Linuxon és MacOS-on, míg Windowson csak a Consul KV.

Kezdettől fogva arra törekedtünk, hogy a szolgáltatások eléréséhez kész könyvtárakat használjunk. A ZooKeeper esetében esett a választás ZooKeeper C++, amelyet végül nem sikerült lefordítani Windowson. Ez azonban nem meglepő: a könyvtár csak linuxosként van elhelyezve. A konzul számára az egyetlen lehetőség volt ppconsul. Támogatást kellett hozzá adni foglalkozások и tranzakciók. Az etcd esetében nem található a protokoll legújabb verzióját támogató teljes értékű könyvtár, így egyszerűen generált grpc kliens.

A ZooKeeper C++ könyvtár aszinkron felülete alapján úgy döntöttünk, hogy egy aszinkron felületet is megvalósítunk. A ZooKeeper C++ jövőbeli/ígéret primitíveket használ erre. STL-ben sajnos nagyon szerényen vannak megvalósítva. Például nem akkor módszer, amely az átadott függvényt alkalmazza a jövő eredményére, amikor az elérhetővé válik. Esetünkben egy ilyen módszer szükséges ahhoz, hogy az eredményt a könyvtárunk formátumára konvertáljuk. A probléma megkerüléséhez saját egyszerű szálkészletünket kellett megvalósítanunk, mivel az ügyfél kérésére nem tudtunk olyan nehéz harmadik féltől származó könyvtárakat használni, mint például a Boost.

Az akkori megvalósításunk így működik. Meghíváskor egy további ígéret/jövő pár jön létre. Az új jövő visszakerül, az átadott pedig a megfelelő függvénnyel és egy további ígérettel együtt a sorba kerül. Egy szál a készletből kiválaszt több határidős ügyletet a sorból, és lekérdezi őket a wait_for használatával. Amikor egy eredmény elérhetővé válik, a megfelelő függvény meghívásra kerül, és annak visszatérési értéke átadásra kerül az ígéretnek.

Ugyanazt a szálkészletet használtuk az etcd és a Consul lekérdezések végrehajtására. Ez azt jelenti, hogy az alapul szolgáló könyvtárak több különböző szálon keresztül érhetők el. A ppconsul nem szálbiztos, ezért a rá irányuló hívásokat zár védi.
A grpc-vel több szálból is dolgozhat, de vannak finomságok. Az etcd-ben az órák grpc-folyamokon keresztül valósulnak meg. Ezek kétirányú csatornák egy bizonyos típusú üzenetekhez. A könyvtár egyetlen szálat hoz létre az összes figyeléshez, és egyetlen szálat, amely feldolgozza a bejövő üzeneteket. Tehát a grpc tiltja a párhuzamos írást a streambe. Ez azt jelenti, hogy egy óra inicializálása vagy törlése során meg kell várnia, amíg az előző kérés befejezi az elküldést, mielőtt elküldi a következőt. Szinkronizálásra használjuk feltételes változók.

Teljes

Nézd meg magad: liboffkv.

Csapatunk: Raed Romanov, Ivan Glushenkov, Dmitrij Kamaldinov, Viktor Krapivensky, Vitalij Ivanin.

Forrás: will.com

Hozzászólás