Пет студенти и тројца дистрибуираа продавници со клучна вредност

Или како напишавме клиентска библиотека C++ за ZooKeeper, etcd и Consul KV

Во светот на дистрибуираните системи, постојат голем број типични задачи: складирање информации за составот на кластерот, управување со конфигурацијата на јазлите, откривање на неисправни јазли, избор на лидер и други. За решавање на овие проблеми, создадени се специјални дистрибуирани системи - координативни служби. Сега ќе нè интересираат три од нив: ZooKeeper, etcd и конзул. Од сета богата функционалност на Конзул, ќе се фокусираме на Конзул К.В.

Пет студенти и тројца дистрибуираа продавници со клучна вредност

Во суштина, сите овие системи се толерантни на грешки, линеаризирани складишта со клучни вредности. Иако нивните модели на податоци имаат значителни разлики, за кои ќе разговараме подоцна, тие ги решаваат истите практични проблеми. Очигледно, секоја апликација што ја користи услугата за координација е врзана за една од нив, што може да доведе до потреба од поддршка на неколку системи во еден центар за податоци што ги решаваат истите проблеми за различни апликации.

Идејата да се реши овој проблем потекнува од една австралиска консултантска агенција и ни падна на нас, мал тим од студенти, да ја спроведеме, за што ќе зборувам.

Успеавме да создадеме библиотека која обезбедува заеднички интерфејс за работа со ZooKeeper, etcd и Consul KV. Библиотеката е напишана на C++, но се планира да се пренесе на други јазици.

Модели на податоци

За да развиете заеднички интерфејс за три различни системи, треба да разберете што имаат заедничко и како се разликуваат. Ајде да го сфатиме.

Зоочувар

Пет студенти и тројца дистрибуираа продавници со клучна вредност

Клучевите се организирани во дрво и се нарекуваат јазли. Соодветно на тоа, за јазол можете да добиете список на неговите деца. Операциите за создавање znode (креирање) и менување вредност (setData) се одвоени: само постоечките клучеви може да се читаат и менуваат. Часовниците може да се прикачат на операциите за проверка на постоењето на јазол, читање вредност и добивање деца. Watch е еднократен активирач што се активира кога се менува верзијата на соодветните податоци на серверот. Ефемерните јазли се користат за откривање на неуспеси. Тие се врзани за сесијата на клиентот што ги создал. Кога клиентот затвора сесија или ќе престане да го известува ZooKeeper за неговото постоење, овие јазли автоматски се бришат. Поддржани се едноставни трансакции - збир на операции кои или сите успеваат или не успеваат ако тоа не е можно барем за една од нив.

итн

Пет студенти и тројца дистрибуираа продавници со клучна вредност

Програмерите на овој систем беа јасно инспирирани од ZooKeeper и затоа правеа сè поинаку. Не постои хиерархија на клучеви, но тие формираат лексикографски подреден сет. Можете да ги добиете или избришете сите клучеви кои припаѓаат на одреден опсег. Оваа структура може да изгледа чудна, но всушност е многу експресивна и преку неа лесно може да се имитира хиерархиски поглед.

etcd нема стандардна операција за споредување и поставување, но има нешто подобро: трансакции. Се разбира, тие постојат во сите три системи, но трансакциите со etcd се особено добри. Тие се состојат од три блока: проверка, успех, неуспех. Првиот блок содржи збир на услови, вториот и третиот - операции. Трансакцијата се извршува атомски. Ако сите услови се вистинити, тогаш блокот за успех се извршува, во спротивно блокот за неуспех се извршува. Во API 3.3, блоковите за успех и неуспех може да содржат вгнездени трансакции. Односно, можно е атомски да се извршат условни конструкции на речиси произволно ниво на гнездење. Можете да дознаете повеќе за тоа од кои проверки и операции постојат документација.

И овде постојат часовници, иако се малку посложени и се повторливи. Односно, откако ќе инсталирате часовник на опсег на копчиња, ќе ги добивате сите ажурирања во овој опсег додека не го откажете часовникот, а не само првото. Во итн., аналог на сесиите на клиентите на ZooKeeper се закупот.

Конзулот К.В.

Тука, исто така, нема строга хиерархиска структура, но конзулот може да создаде изглед дека постои: можете да ги добиете и избришете сите клучеви со наведениот префикс, односно да работите со „подстеблото“ на клучот. Таквите прашања се нарекуваат рекурзивни. Покрај тоа, конзулот може да избере само клучеви што не го содржат наведениот знак по префиксот, што одговара на добивање на непосредни „деца“. Но, вреди да се запамети дека ова е токму изгледот на хиерархиска структура: сосема е можно да се создаде клуч ако неговиот родител не постои или да избрише клуч што има деца, додека децата ќе продолжат да се складираат во системот.

Пет студенти и тројца дистрибуираа продавници со клучна вредност
Наместо часовници, конзулот има блокирачки барања за HTTP. Во суштина, ова се обични повици до методот на читање податоци, за кои, заедно со другите параметри, е означена и последната позната верзија на податоците. Ако тековната верзија на соодветните податоци на серверот е поголема од наведената, одговорот се враќа веднаш, во спротивно - кога вредноста ќе се промени. Исто така, постојат сесии кои можат да се прикачат на копчињата во секое време. Вреди да се напомене дека за разлика од etcd и ZooKeeper, каде што бришењето на сесиите доведува до бришење на поврзаните клучеви, постои режим во кој сесијата едноставно се прекинува со нив. Достапно трансакции, без гранки, но со секакви проверки.

Составување на сето тоа заедно

ZooKeeper го има најригорозниот модел на податоци. Прашањата за експресивен опсег достапни во etcd не можат ефективно да се емулираат ниту во ZooKeeper ниту во Consul. Обидувајќи се да го вклучиме најдоброто од сите услуги, завршивме со интерфејс речиси еквивалентен на интерфејсот ZooKeeper со следните значајни исклучоци:

  • низа, контејнер и TTL јазли не се поддржани
  • ACL не се поддржани
  • методот set создава клуч ако не постои (во ZK setData враќа грешка во овој случај)
  • методите на set и cas се одделени (во ЗК тие се во суштина иста работа)
  • методот на бришење брише јазол заедно со неговото поддрво (во ZK delete враќа грешка ако јазолот има деца)
  • За секој клуч има само една верзија - вредносната верзија (во ZK има три од нив)

Отфрлањето на секвенцијалните јазли се должи на фактот што etcd и Consul немаат вградена поддршка за нив, и тие можат лесно да се имплементираат од корисникот на врвот на добиениот интерфејс на библиотеката.

Спроведувањето на однесување слично на ZooKeeper при бришење теме ќе бара одржување на посебен бројач за деца за секој клуч во etcd и конзул. Бидејќи се обидовме да избегнеме складирање на мета информации, беше одлучено да се избрише целото поддрво.

Суптилностите на имплементацијата

Да разгледаме подетално некои аспекти од имплементацијата на интерфејсот на библиотеката во различни системи.

Хиерархија во итн

Одржувањето на хиерархиски поглед во etcd се покажа како една од најинтересните задачи. Пребарувањата за опсег го олеснуваат преземањето листа на клучеви со одреден префикс. На пример, ако ви треба сè што започнува со "/foo", барате опсег ["/foo", "/fop"). Но, ова би го вратило целото подстебло на клучот, што можеби не е прифатливо ако подстеблото е големо. На почетокот планиравме да користиме клучен механизам за превод, имплементиран во zetcd. Тоа вклучува додавање на еден бајт на почетокот на клучот, еднаков на длабочината на јазолот во дрвото. Дозволете ми да ви дадам пример.

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

Потоа земете ги сите непосредни деца на клучот "/foo" можно со барање опсег ["u02/foo/", "u02/foo0"). Да, во ASCII "0" стои веднаш потоа "/".

Но, како да се спроведе отстранување на теме во овој случај? Излегува дека треба да ги избришете сите опсези од типот ["uXX/foo/", "uXX/foo0") за XX од 01 до FF. И тогаш налетавме ограничување на бројот на операции во рамките на една трансакција.

Како резултат на тоа, беше измислен едноставен систем за конверзија на клучеви, што овозможи ефикасно да се имплементира и бришењето клуч и добивањето список на деца. Доволно е да додадете посебен знак пред последниот токен. На пример:

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

Потоа бришење на клучот "/very" се претвора во бришење "/u00very" и опсег ["/very/", "/very0"), и добивање на сите деца - во барање за клучеви од опсегот ["/very/u00", "/very/u01").

Отстранување клуч во ZooKeeper

Како што веќе спомнав, во ZooKeeper не можете да избришете јазол ако има деца. Сакаме да го избришеме клучот заедно со поддрвото. Што да правам? Ова го правиме со оптимизам. Прво, рекурзивно го поминуваме подстеблото, добивајќи ги децата од секое теме со посебно барање. Потоа градиме трансакција која се обидува да ги избрише сите јазли од подстеблото по правилен редослед. Се разбира, може да настанат промени помеѓу читањето на поддрвото и неговото бришење. Во овој случај, трансакцијата ќе пропадне. Покрај тоа, подстеблото може да се промени за време на процесот на читање. Барањето за децата од следниот јазол може да врати грешка ако, на пример, овој јазол е веќе избришан. Во двата случаи, повторно го повторуваме целиот процес.

Овој пристап го прави бришењето клуч многу неефикасно ако има деца, а уште повеќе ако апликацијата продолжи да работи со поддрвото, бришејќи и создавајќи клучеви. Сепак, ова ни овозможи да избегнеме комплицирање на имплементацијата на други методи во etcd и Consul.

поставена во ZooKeeper

Во ZooKeeper постојат посебни методи кои работат со структурата на дрвото (креирање, бришење, getChildren) и кои работат со податоци во јазли (setData, getData).Покрај тоа, сите методи имаат строги предуслови: create ќе врати грешка ако јазолот веќе има се создадени, бришете или поставитеData – ако веќе не постојат. Ни требаше сет метод што може да се повика без да размислуваме за присуството на клуч.

Една опција е да се заземе оптимистички пристап, како со бришењето. Проверете дали постои јазол. Ако постои, повикајте setData, инаку креирајте. Ако последниот метод врати грешка, повторете го повторно. Првото нешто што треба да се забележи е дека тестот за постоење е бесмислен. Можете веднаш да повикате креирање. Успешното завршување ќе значи дека јазолот не постоел и дека е создаден. Во спротивно, create ќе ја врати соодветната грешка, по што треба да повикате setData. Се разбира, помеѓу повиците, темето може да се избрише со конкурентен повик, а setData исто така ќе врати грешка. Во овој случај, можете да го направите тоа одново, но дали вреди?

Ако двата методи вратат грешка, тогаш сигурно знаеме дека се случило конкурентно бришење. Да замислиме дека ова бришење се случило по повикот set. Тогаш, секое значење што се обидуваме да го воспоставиме, веќе е избришано. Ова значи дека можеме да претпоставиме дека множеството е успешно извршено, дури и ако всушност ништо не е напишано.

Повеќе технички детали

Во овој дел ќе направиме пауза од дистрибуираните системи и ќе зборуваме за кодирање.
Едно од главните барања на клиентот беше крос-платформа: барем една од услугите мора да биде поддржана на Linux, MacOS и Windows. Првично, развивме само за Linux, а подоцна почнавме да тестираме и на други системи. Ова предизвика многу проблеми, на кои некое време беше целосно нејасно како да се пристапи. Како резултат на тоа, сите три услуги за координација сега се поддржани на Linux и MacOS, додека само Consul KV е поддржан на Windows.

Од самиот почеток, се обидовме да користиме готови библиотеки за пристап до услугите. Во случајот на ZooKeeper, изборот падна ZooKeeper C++, што на крајот не успеа да се компајлира на Windows. Ова, сепак, не е изненадувачки: библиотеката е позиционирана само како Linux. За конзулот единствената опција беше ппконзул. На него мораше да се додаде поддршка сесии и трансакции. За etcd, не беше пронајдена полноправна библиотека што ја поддржува најновата верзија на протоколот, така што едноставно генериран grpc клиент.

Инспирирани од асинхрониот интерфејс на библиотеката ZooKeeper C++, решивме да имплементираме и асинхрон интерфејс. ZooKeeper C++ користи идни/ветувачки примитиви за ова. Во STL, за жал, тие се спроведуваат многу скромно. На пример, бр потоа метод, која ја применува поминатата функција на резултатот од иднината кога ќе стане достапен. Во нашиот случај, таков метод е неопходен за конвертирање на резултатот во формат на нашата библиотека. За да го надминеме овој проблем, моравме да имплементираме наш едноставен базен со нишки, бидејќи на барање на клиентот не можевме да користиме тешки библиотеки од трети страни, како што е Boost.

Нашата тогашна имплементација функционира вака. Кога се повикува, се создава дополнителен пар ветување/иден пар. Се враќа новата иднина, а поминатата се става заедно со соодветната функција и дополнително ветување во редот. Нишка од базенот избира неколку фјучерси од редот и ги анкетира користејќи wait_for. Кога резултатот ќе стане достапен, се повикува соодветната функција и нејзината повратна вредност се пренесува на ветувањето.

Го користевме истиот базен на нишки за извршување на прашања до etcd и конзул. Ова значи дека до основните библиотеки може да се пристапи со повеќе различни нишки. ppconsul не е безбеден за конец, така што повиците до него се заштитени со брави.
Можете да работите со grpc од повеќе нишки, но има суптилности. Во etcd часовниците се имплементирани преку grpc streams. Тоа се двонасочни канали за пораки од одреден тип. Библиотеката создава една нишка за сите часовници и една нишка која ги обработува дојдовните пораки. Значи, grpc забранува паралелни запишувања во стрим. Ова значи дека кога иницијализирате или бришете часовник, мора да почекате додека претходното барање не заврши со испраќањето пред да го испратите следното. Ние користиме за синхронизација условни променливи.

Вкупно

Погледнете за себе: либофкв.

Нашиот тим: Раед Романов, Иван Глушенков, Дмитриј Камалдинов, Виктор Крапивенски, Виталиј Иванин.

Извор: www.habr.com

Додадете коментар