Пет ученика и три разпределени магазина за ключ-стойност

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

В света на разпределените системи има редица типични задачи: съхраняване на информация за състава на клъстера, управление на конфигурацията на възли, откриване на дефектни възли, избор на лидер друг. За решаването на тези проблеми са създадени специални разпределени системи - координационни услуги. Сега ще се интересуваме от три от тях: ZooKeeper, etcd и Consul. От цялата богата функционалност на Consul ще се спрем на Consul KV.

Пет ученика и три разпределени магазина за ключ-стойност

По същество всички тези системи са устойчиви на грешки, линеаризиращи се хранилища за ключ-стойност. Въпреки че техните модели на данни имат значителни разлики, които ще обсъдим по-късно, те решават същите практически проблеми. Очевидно всяко приложение, което използва услугата за координиране, е обвързано с едно от тях, което може да доведе до необходимостта от поддръжка на няколко системи в един център за данни, които решават едни и същи проблеми за различни приложения.

Идеята за решаването на този проблем се зароди в австралийска консултантска агенция и на нас, малък екип от студенти, се падна да я реализираме, за което ще говоря.

Успяхме да създадем библиотека, която предоставя общ интерфейс за работа със ZooKeeper, etcd и Consul KV. Библиотеката е написана на C++, но има планове да бъде пренесена на други езици.

Модели на данни

За да разработите общ интерфейс за три различни системи, трябва да разберете какво е общото между тях и как се различават. Нека да го разберем.

ZooKeeper

Пет ученика и три разпределени магазина за ключ-стойност

Ключовете са организирани в дърво и се наричат ​​възли. Съответно за възел можете да получите списък с неговите деца. Операциите за създаване на znode (create) и промяна на стойност (setData) са разделени: могат да се четат и променят само съществуващи ключове. Часовниците могат да бъдат прикрепени към операциите за проверка на съществуването на възел, четене на стойност и получаване на деца. Watch е еднократен тригер, който се задейства, когато версията на съответните данни на сървъра се промени. Ефемерните възли се използват за откриване на повреди. Те са обвързани със сесията на клиента, който ги е създал. Когато клиент затвори сесия или спре да уведомява ZooKeeper за нейното съществуване, тези възли се изтриват автоматично. Поддържат се прости транзакции - набор от операции, които или всички са успешни, или се провалят, ако това не е възможно за поне една от тях.

и т.н.

Пет ученика и три разпределени магазина за ключ-стойност

Разработчиците на тази система очевидно бяха вдъхновени от ZooKeeper и затова направиха всичко по различен начин. Няма йерархия на ключовете, но те образуват лексикографски подреден набор. Можете да получите или изтриете всички ключове, принадлежащи към определен диапазон. Тази структура може да изглежда странна, но всъщност е много изразителна и чрез нея лесно може да се емулира йерархичен изглед.

etcd няма стандартна операция за сравнение и задаване, но има нещо по-добро: транзакции. Разбира се, те съществуват и в трите системи, но etcd транзакциите са особено добри. Те се състоят от три блока: проверка, успех, неуспех. Първият блок съдържа набор от условия, вторият и третият - операции. Транзакцията се изпълнява атомарно. Ако всички условия са верни, тогава се изпълнява блокът за успех, в противен случай се изпълнява блокът за неуспех. В API 3.3 блоковете за успех и неуспех могат да съдържат вложени транзакции. Това означава, че е възможно да се изпълнят атомарно условни конструкции с почти произволно ниво на влагане. Можете да научите повече за това какви проверки и операции съществуват документация.

Тук също има часовници, но те са малко по-сложни и могат да се използват многократно. Тоест, след като инсталирате часовник на ключов диапазон, ще получавате всички актуализации в този диапазон, докато не отмените часовника, а не само първия. В etcd аналогът на клиентските сесии на ZooKeeper са лизингите.

консул К.В.

Тук също няма строга йерархична структура, но Consul може да създаде вид, че съществува: можете да получите и изтриете всички ключове с посочения префикс, тоест да работите с „поддървото“ на ключа. Такива заявки се наричат ​​рекурсивни. Освен това Consul може да избира само ключове, които не съдържат посочения знак след префикса, което съответства на получаването на незабавни „деца“. Но си струва да запомните, че това е именно появата на йерархична структура: напълно е възможно да създадете ключ, ако неговият родител не съществува или да изтриете ключ, който има деца, докато децата ще продължат да се съхраняват в системата.

Пет ученика и три разпределени магазина за ключ-стойност
Вместо часовници, Consul има блокиране на HTTP заявки. По същество това са обикновени извиквания на метода за четене на данни, за които наред с други параметри се посочва последната известна версия на данните. Ако текущата версия на съответните данни на сървъра е по-голяма от зададената, отговорът се връща незабавно, в противен случай - при промяна на стойността. Има и сесии, които могат да бъдат прикачени към ключове по всяко време. Струва си да се отбележи, че за разлика от etcd и ZooKeeper, където изтриването на сесии води до изтриване на свързаните ключове, има режим, в който сесията просто е прекъсната връзката с тях. На разположение сделки, без клонове, но с всякакви проверки.

Сглобяване на всичко

ZooKeeper има най-строгия модел на данни. Експресивните заявки за обхват, налични в etcd, не могат да бъдат ефективно емулирани нито в ZooKeeper, нито в Consul. Опитвайки се да включим най-доброто от всички услуги, ние завършихме с интерфейс, почти еквивалентен на интерфейса на ZooKeeper със следните значителни изключения:

  • последователност, контейнер и TTL възли Не се поддържа
  • ACL не се поддържат
  • методът set създава ключ, ако не съществува (в ZK setData връща грешка в този случай)
  • set и cas методите са разделени (в ZK те са по същество едно и също нещо)
  • методът за изтриване изтрива възел заедно с неговото поддърво (в ZK delete връща грешка, ако възелът има деца)
  • За всеки ключ има само една версия - стойностната версия (в ZK има три от тях)

Отхвърлянето на последователни възли се дължи на факта, че etcd и Consul нямат вградена поддръжка за тях и те могат лесно да бъдат внедрени от потребителя върху получения интерфейс на библиотеката.

Прилагането на поведение, подобно на ZooKeeper при изтриване на връх, би изисквало поддържане на отделен дъщерен брояч за всеки ключ в etcd и Consul. Тъй като се опитахме да избегнем съхраняването на мета информация, беше решено да изтрием цялото поддърво.

Тънкости на изпълнение

Нека разгледаме по-отблизо някои аспекти на внедряването на библиотечния интерфейс в различни системи.

Йерархия в 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 има отделни методи, които работят с дървовидната структура (create, delete, getChildren) и които работят с данни във възли (setData, getData). Освен това всички методи имат строги предварителни условия: create ще върне грешка, ако възелът вече е е създаден, изтрийте или setData – ако все още не съществува. Имахме нужда от набор метод, който може да бъде извикан, без да мислим за наличието на ключ.

Една от възможностите е да възприемете оптимистичен подход, както при изтриването. Проверете дали съществува възел. Ако съществува, извикайте setData, в противен случай създайте. Ако последният метод е върнал грешка, повторете го отново. Първото нещо, което трябва да се отбележи е, че тестът за съществуване е безсмислен. Можете веднага да извикате create. Успешното завършване ще означава, че възелът не е съществувал и е създаден. В противен случай 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 и Consul. Това означава, че основните библиотеки могат да бъдат достъпни от множество различни нишки. ppconsul не е безопасен за нишки, така че повикванията към него са защитени от ключалки.
Можете да работите с grpc от множество нишки, но има тънкости. В etcd часовниците се изпълняват чрез grpc потоци. Това са двупосочни канали за съобщения от определен тип. Библиотеката създава една нишка за всички часовници и една нишка, която обработва входящите съобщения. Така grpc забранява паралелните записи в поток. Това означава, че когато инициализирате или изтривате часовник, трябва да изчакате, докато предишната заявка завърши изпращането, преди да изпратите следващата. Използваме за синхронизация условни променливи.

Общо

Вижте сами: liboffkv.

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

Източник: www.habr.com

Добавяне на нов коментар