
Отказоустойчивост и висока достъпност — големи теми, така че ще посветим на RabbitMQ и Kafka отделни статии. Тази статия е за RabbitMQ, а следващата — за Kafka, в сравнение с RabbitMQ. Статията е дълга, така че се настанете удобно.
Нека разгледаме стратегиите за отказоустойчивост, съгласуваност и висока достъпност (HA), както и компромисите, на които трябва да се прибегне с всяка стратегия. RabbitMQ може да работи в клъстър от възли — и тогава се класифицира като разпределена система. Когато става дума за разпределени системи, често говорим за съгласуваност и достъпност.
Тези понятия описват как системата се държи при повреда. Повреда на мрежовото свързване, повреда на сървъра, повреда на твърдия диск, временно недостъпност на сървъра поради събиране на боклук, загуба на пакети или забавяне на мрежовото свързване. Всичко това може да доведе до загуба на данни или конфликти. Оказва се, че практически е невъзможно да се изградят системи, които да бъдат едновременно и напълно непротиворечиви (без загуба на данни, без разминавания на данни), и достъпни (да приемат операции за четене и запис) при всички варианти на повреда.
Ще видим, че съгласуваността и достъпността се намират на различни краища на спектъра и вие трябва да изберете в коя посока да оптимизирате. Добрата новина е, че с RabbitMQ такава възможност съществува. Имате едни „гиковски“ лостове, с които да премествате баланса в полза на по-голяма съгласуваност или по-голяма достъпност.
Особено внимание ще обърнем на конфигурациите, които водят до загуба на данни поради потвърдени записи. Има верига на отговорност между издателите, брокерите и потребителите. След като съобщението бъде предадено на брокера, това е неговата работа — да не загуби съобщението. Когато брокерът потвърди получението на съобщението на издателя, не очакваме то да бъде загубено. Но ще видим, че това наистина може да се случи в зависимост от конфигурацията на вашия брокер и издател.
Примитиви на устойчивостта на един възел
Устойчиви опашки/маршрутизация
В RabbitMQ има два типа опашки: дълготрайни (durable) и недълготрайни (non-durable). Всички опашки се запазват в базата данни Mnesia. Дълготрайните опашки се обявяват повторно при стартиране на възела и, по този начин, оцеляват след рестарт, системен срив или срив на сървъра (докато данните са запазени). Това означава, че ако обявите маршрутизацията (exchange) и опашката за дълготрайни, инфраструктурата на опашките/маршрутизацията ще се върне в работен режим.
Недълготрайните опашки и маршрутизация се изтриват при рестарт на възела.
Дълготрайни съобщения
Самият факт, че опашката е дълготрайна, не означава, че всички нейни съобщения ще оцелеят след рестарт на възела. Ще бъдат възстановени само съобщенията, определени от издателя като дълготрайни (persistent). Дълготрайните съобщения наистина създават допълнително натоварване на брокера, но ако загубата на съобщение е неприемлива, няма друг изход.

Рис. 1. Матрица на дълготрайност
Клъстериране с зеркалини опашки
За да оцелее загубата на брокера, нуждаем се от излишък. Можем да обединим няколко възела RabbitMQ в клъстер и след това да добавим допълнителен излишък чрез репликация на опашките между няколко възела. Така, ако един възел се срине, не губим данни и оставаме достъпни.
Зеркалиране на опашки:
- една основна опашка (мастър), която получава всички команди за запис и четене
- едно или повече зеркала, които получават всички съобщения и метаданни от основната опашка. Тези зеркала съществуват не за мащабиране, а изключително за излишък.

Рис. 2. Зеркалиране на опашки
Зеркалирането се настройва с подходяща политика. В нея можете да изберете коефициент на репликация и дори възли, на които да се разполага опашката. Примери:
ha-mode: allha-mode: exactly, ha-params: 2(един мастър и едно зеркало)ha-mode: nodes, ha-params: rabbit@node1, rabbit@node2
Потвърждение на издателя
За да се постигне последователно записване, са необходими потвърждения от издателя (Publisher Confirms). Без тях има вероятност за загуба на съобщения. Потвърждение се изпраща на издателя след записа на съобщението на диска. RabbitMQ записва съобщенията на диска не при получаване, а на периодична база, в диапазона на няколко стотици милисекунди. Когато опашката е огледална, потвърждението се изпраща само след като всички огледала също запишат своята копия на съобщението на диска. Това означава, че използването на потвърждения добавя закъснение, но ако безопасността на данните е важна, те са задължителни.
Отказоустойчива опашка
Когато брокерът приключи работата си или се срине, всички водещи опашки (мастери) на този възел отпадат заедно с него. След това клъстерът избира най-старото огледало на всеки мастер и го повишава в нов мастер.

Рис. 3. Няколко огледални опашки и техните политики
Брокер 3 се срива. Обърнете внимание, че огледалото на Опашка С на Брокер 2 се повишава до мастер. Също така обърнете внимание, че за Опашка C е създадено ново огледало на Брокер 1. RabbitMQ винаги се опитва да поддържа коефициента на репликация, указан във вашите политики.

Рис. 4. Брокер 3 отпада, което води до отказ на опашка C
Отпада следващият Брокер 1! Остава ни само един брокер. Огледалото на Опашка B се повишава до мастер.

Рис. 5
Върнахме Брокер 1. Независимо от това, колко успешно данните са преживели загубата и възстановяването на брокера, всички огледални съобщения на опашката се отхвърлят при рестартиране. Важно е да се отбележи, тъй като ще има последствия. Скоро ще разгледаме тези последствия. По този начин, Брокер 1 вече отново е член на клъстера, а клъстерът се опитва да спазва политиките и следователно създава огледала на Брокер 1.
В този случай загубата на Брокер 1 беше пълна, както и на данните, така че незеркалната Опашка В е напълно загубена.

Рис. 6. Брокер 1 се връща в строя
Брокер 3 се завръща в строя, така че опашките A и B получават обратно създадените на него огледала, за да удовлетворят своите политики HA. Но сега всички основни опашки са на един възел! Това не е идеално, по-добре е равномерно разпределение между възлите. За съжаление, тук няма много опции за повторно балансиране на майсторите. Ще се върнем на този проблем по-късно, тъй като първо трябва да разгледаме синхронизацията на опашката.

Рис. 7. Брокер 3 се завръща в строя. Всички основни опашки на един възел!
Така че сега трябва да имате представа как огледалата осигуряват излишък и устойчивост на отказ. Това гарантира наличност в случай на отказ на един възел и защитава от загуба на данни. Но все още не сме приключили, защото всъщност всичко е много по-сложно.
Синхронизиране
При създаването на ново огледало всички нови съобщения винаги ще се репликират на това огледало и на всяко друго. Що се отнася до съществуващите данни в основната опашка, можем да ги репликираме в новото огледало, което става пълна копия на майстора. Също така можем да не репликираме съществуващите съобщения и да позволим на основната опашка и новото огледало да се събират с времето, когато новите съобщения влизат в опашката, а съществуващите съобщения напускат главата на основната опашка.
Такава синхронизация се извършва автоматично или ръчно и се управлява с помощта на политиките на опашките. Нека разгледаме примера.
Имахме две огледални опашки. Опашка A се синхронизира автоматично, а Опашка B — ръчно. В двете опашки има по десет съобщения.

Рис. 8. Две опашки с различни режими на синхронизация
Сега губим Брокер 3.

Рис. 9. Брокер 3 е паднал
Брокер 3 се завръща в строя. Кластерът създава огледало за всяка опашка на нов възел и автоматично синхронизира новата Опашка А с майстора. Въпреки това, огледалото на новата Опашка В остава празно. Така че имаме пълна излишност на Опашка A и само едно огледало за съществуващите съобщения на Опашка B.

Рис. 10. Новото огледало на Опашка А получава всички съществуващи съобщения, а новото огледало на Опашка B — не
В двете опашки постъпват по още десет съобщения. След това Брокер 2 пада, а Опашка А се връща към най-старото огледало, което се намира на Брокер 1. При срив не настъпва загуба на данни. В Опашка B има двадесет съобщения в майстора и само десет в огледалото, тъй като тази опашка никога не е репликирала първоначалните десет съобщения.

Рис. 11. Опашка А се връща на Брокер 1 без загуба на съобщения
В двете опашки постъпват още по десет съобщения. Сега пада Брокер 1. Опашка A без проблеми превключва на огледалото без загуба на съобщения. Въпреки това, Опашка B среща проблеми. На този етап можем да оптимизираме или достъпността, или последователността.
Ако искаме да оптимизираме достъпността, то политиката ha-promote-on-failure трябва да бъде зададена на always. Това е стойността по подразбиране, така че може просто да не задаваме политиката въобще. В такъв случай, по същество, допускаме сривове в несинхронизирани огледала. Това ще доведе до загуба на съобщения, но опашката остава достъпна за четене и запис.

Рис. 12. Опашка А се връща на Брокер 3 без загуба на съобщения. Опашка B се връща на Брокер 3 с загуба на десет съобщения
Можем също така да зададем ha-promote-on-failure на стойност when-synced. В този случай вместо да се върне на огледалото, опашката ще изчака, докато Брокер 1 с данните си се върне в оперативен режим. След неговото завръщане главната опашка отново попада на Брокер 1 без загуба на данни. Достъпността се жертва за сигурността на данните. Но това е рисков режим, който може да доведе дори до пълна загуба на данни, което ще разгледаме в близко бъдеще.

Рис. 13. Опашка B остава недостъпна след загубата на Брокер 1
Можете да зададете въпроса: «Може би е по-добре никога да не използваме автоматична синхронизация?». Отговорът е, че синхронизацията е блокираща операция. По време на синхронизацията главната опашка не може да извършва никакви операции по четене или запис!
Нека разгледаме пример. Сега имаме много големи опашки. Как могат да нараснат до такава степен? По няколко причини:
- Опашките не се използват активно
- Това са високоскоростни опашки, а в момента потребителите работят бавно
- Това са високоскоростни опашки, настъпи срив, и потребителите догонват

Рис. 14. Две големи опашки с различни режимове на синхронизация
Сега пада Брокер 3.

Рис. 15. Брокер 3 пада, оставяйки по един майстор и огледало в всяка опашка
Брокер 3 се връща обратно в строя и се създават нови огледала. Главната опашка А започва да репликира съществуващите съобщения на новото огледало, и през това време опашката е недостъпна. За репликацията на данните са необходими два часа, което води до два часа престой за тази опашка!
Въпреки това, опашка B остава достъпна през целия период. Тя пожертва известна излишност в името на достъпността.

Рис. 16. Опашката остава недостъпна по време на синхронизация
След два часа опашка A също става достъпна и може отново да започне да приема операции за четене и запис.
Актуализации
Такова блокиращо поведение по време на синхронизация затруднява обновлението на клъстери с много големи опашки. В определен момент узел с майстор трябва да се рестартира, което означава или преминаване на огледалото, или спиране на опашката по време на обновлението на сървъра. Ако изберем преминаването, ще загубим съобщения, ако огледалата не са синхронизирани. По подразбиране, по време на спирането на брокера не се извършва преминаване на несинхронизирано огледало. Това означава, че след като брокерът се върне, няма да загубим никакви съобщения, единственият щет се състои само в престоя на опашката. Правилата за поведение при спиране на брокера се задават от политиката ha-promote-on-shutdown. Може да се зададе една от двете стойности:
always= активирането на прехода към несинхронизирани огледалаwhen-synced= преход само към синхронизирано огледало, в противен случай опашката става недостъпна за четене и запис. Опашката се възстановява, веднага щом брокерът се върне
Както и да е, с големи опашки трябва да се избира между загуба на данни и недостъпност.
Когато достъпността повишава сигурността на данните
Преди да вземем решение, трябва да вземем предвид още едно затруднение. Въпреки че автоматичната синхронизация е по-добра за излишност, как тя влияе на сигурността на данните? Разбира се, благодарение на по-добрата излишност RabbitMQ с по-малка вероятност ще загуби съществуващи съобщения, но какво ще стане с новите съобщения от публикаторите?
Тук трябва да се вземат предвид следните точки:
- Може ли издателят просто да върне грешка, а по-високата служба или потребителят по-късно да опитат отново?
- Може ли публикуващият да запази съобщение локално или в база данни, за да опита отново по-късно?
Ако издателят може само да отхвърли съобщението, то всъщност подобряването на достъпността също повишава безопасността на данните.
Следователно, трябва да търсим баланс, а решението зависи от конкретната ситуация.
Проблеми с ha-promote-on-failure=when-synced
Идея ha-promote-on-failure= when-synced се състои в това, че предотвратяваме превключването на несинхронизирано огледало и по този начин избягваме загубата на данни. Опашката остава недостъпна за четене или запис. Вместо това се опитваме да възстановим падналия брокер с непокътнати данни, за да продължи да работи като майстор без загуба на данни.
Но (и това е голямо но) ако брокерът е загубил данните си, то имаме голям проблем: опашката е загубена! Всички данни са изчезнали! Дори ако имате огледала, които основно наваксват основната опашка, тези огледала също се отхвърлят.
За да добавим отново възел с същото име, ние казваме на клъстера да забрави изгубения възел (с командата rabbitmqctl forget_cluster_node) и да стартира нов брокер с същото име на хоста. Докато клъстерът помни изгубения възел, той помни старата опашка и несинхронизирани огледала. Когато на клъстера се каже да забрави изгубения възел, тази опашка също се забравя. Сега е необходимо да я обявим отново. Загубихме всички данни, въпреки че имахме огледала с частичен набор от данни. По-добре беше да преминем на несинхронизирано огледало!
Затова ръчната синхронизация (и неверността на синхронизацията) в комбинация с ha-promote-on-failure=when-synced, на мой поглед, е доста рискова. Документите твърдят, че този вариант съществува за безопасността на данните, но това е двуостър нож.
Преразпределяне на майсторите
Както беше обещано, връщаме се към проблема с натрупването на всички майстори на един или няколко възела. Това може да се случи дори в резултат на „плъзгащ“ (rolling) ъпдейт на клъстера. В клъстер с три възела всички основни опашки ще се натрупат на един или два възела.
Преразпределянето на майсторите може да се окаже проблематично по две причини:
- Няма добри инструменти за извършване на преразпределение
- Синхронизиране на опашките
Има трета страна за преразпределение, , която не се поддържа официално. По отношение на страничните плъгини в ръководството на RabbitMQ. : «Плагин предоставя някои допълнителни инструменти за конфигуриране и отчетност, но не се поддържа и не е проверен от екипа на RabbitMQ. Използвайте на собствен риск».
Има още един трик за преместване на основната опашка чрез политиките за HA. В ръководството се споменава за това. Работи по следния начин:
- Премахва всички огледала с временна политика с по-висок приоритет от съществуващата политика за HA.
- Променя временната политика за HA, за да използва режим «възли» с указание за възела, на който трябва да се премести основната опашка.
- Синхронизира опашката за принудителна миграция.
- След завършване на миграцията премахва временната политика. Влиза в сила оригиналната политика за HA и се създава нужното количество огледала.
Недостатъкът е, че този подход може да не сработи, ако имате големи опашки или строг към излишъка изисквания.
Сега да видим как работят клъстерите RabbitMQ с мрежови раздели.
Нарушение на свързаността
Възлите на разпределената система се свързват чрез мрежови връзки, а мрежовите връзки могат и ще бъдат прекъсвани. Честотата на прекъсванията зависи от локалната инфраструктура или надеждността на избраното облако. Във всеки случай, разпределените системи трябва да могат да се справят с тях. Отново стои изборът между наличност и последователност, и отново добрата новина е, че RabbitMQ осигурява и двете опции (просто не едновременно).
С RabbitMQ имаме две основни опции:
- Позволете логично разделение (split-brain). Това осигурява наличност, но може да предизвика загуба на данни.
- Забранете логичното разделение. Може да доведе до краткосрочна загуба на наличност в зависимост от начина, по който клиентите се свързват с клъстера. Също така, може да доведе до пълна недостъпност в клъстера от два възела.
Но какво е логично разделение? Това е, когато клъстерът се разделя на две поради загуба на мрежови връзки. На всяка страна огледалата повдигат до мастър, така че в крайна сметка на всяка опашка се падат няколко мастъра.

Рис. 17. Главната опашка и две огледала, всяко на отделен възел. След това настъпва мрежов проблем, и едно огледало се отделя. Отделеният възел вижда, че две други са се изключили, и предава своите огледала на мастера. Сега имаме две главни опашки, и двете позволяват запис и четене.
Ако публикациите изпращат данни до двата мастера, ще получим две разминаващи се копия на опашката.
Различните режими на RabbitMQ осигуряват или наличност, или последователност.
Режим Ignore (по подразбиране)
Този режим осигурява наличност. След загуба на свързаност настъпва логично разделяне. След възстановяване на свързаността администраторът трябва да реши на коя част да даде предимство. Загубилата страна ще бъде рестартирана и всички натрупани данни от тази страна ще бъдат загубени.

Рис. 18. Три публикации са свързани с три брокера. Вътрешно клъстерът насочва всички заявки към главната опашка на Брокер 2.
Сега губим Брокер 3. Той вижда, че другите брокери са се изключили и предава своето огледало на мастера. Така настъпва логично разделяне.

Рис. 19. Логично разделение (split-brain). Записите отиват в две главни опашки, и две копия се разминават.
Свързаността се възстановява, но логичното разделение остава. Администраторът трябва ръчно да избере загубилата страна. В следващия случай администраторът рестартира Брокер 3. Всички съобщения, които той не е успял да предаде, са загубени.

Рис. 20. Администраторът изключва Брокер 3.

Рис. 21. Администраторът стартира Брокер 3, и той се присъединява към клъстера, губейки всички съобщения, които са останали там.
По време на загуба на свързаност и след нейното възстановяване клъстерът и тази опашка бяха налични за четене и запис.
Режим Autoheal
Работи аналогично на режима Ignore, с изключение на това, че самият клъстер автоматично избира загубилата страна след разделяне и възстановяване на свързаността. Загубилата страна се връща в клъстера празна, а опашката губи всички съобщения, които са били изпратени само на тази страна.
Режим Pause Minority
Ако не искаме да допуснем логическо разделение, единственият ни вариант е да се откажем от четенето и писането от по-малката страна след разделението на клъстера. Когато брокерът види, че е на по-малката страна, той спира работа, тоест затваря всички съществуващи връзки и отказва всички нови. Веднъж в секунда проверява възстановяването на свързаността. След като свързаността бъде възстановена, той възобновява работа и се присъединява отново към клъстера.

Рис. 22. Три публишера са свързани с три брокера. Вътрешно клъстерът насочва всички запити към главната опашка на Брокер 2.
След това Брокер 1 и Брокер 2 се отделят от Брокер 3. Вместо да повиши своето огледало до майстор, Брокер 3 спира работа и става недостъпен.

Рис. 23. Брокер 3 спира работа, изключва всички клиенти и отказва запитвания за свързване.
След като свързаността бъде възстановена, той се връща в клъстера.
Нека погледнем на друг пример, където главната опашка е на Брокер 3.

Рис. 24. Главната опашка на Брокер 3.
След това се случва същата загуба на свързаност. Брокер 3 спира работа, тъй като е на по-малката страна. От другата страна възелите виждат, че Брокер 3 е отпаднал, така че по-старото огледало от Брокер 1 и 2 се повишава до майстор.

Рис. 25. Преход към Брокер 2 при недостъпност на Брокер 3.
Когато свързаността бъде възстановена, Брокер 3 ще се присъедини към клъстера.

Рис. 26. Клъстерът се е върнал към нормалната работа.
Тук е важно да разберем, че получаваме последователност, но също така можем да получим достъпност, ако успешно да прехвърлим клиентите на по-голямата част от раздела. За повечето ситуации аз лично бих избрал режима Pause Minority, но всъщност това зависи от конкретния случай.
За осигуряване на достъпност е важно да се уверим, че клиентите успешно се свързват с възела. Нека разгледаме нашите опции.
Осигуряване на свързаност за клиентите.
Имаме няколко опции за това как да насочим клиентите към основната част на клъстера или към работещите възли след загуба на свързаност (след срив на един възел). Първо, нека си припомним, че конкретната опашка е разположена на определен възел, но маршрутизацията и политиките се репликират на всички възли. Клиентите могат да се свържат с всеки възел, а вътрешната маршрутизация ще ги насочи където трябва. Но когато възелът е спрен, той отказва свързвания, затова клиентите трябва да се свържат с друг възел. Ако възелът е паднал, той не може да направи почти нищо.
Нашите опции:
- Достъпът до клъстера се осъществява чрез балансировач на натоварването, който просто циклично преминава през възлите, а клиентите правят повторни опити за свързване до успешно приключване. Ако възелът не работи или е спрян, опитите за свързване с него ще завършат неуспешно, но последващите опити ще отидат на други сървъри (в цикличен режим). Това е подходящо за краткотрайна загуба на свързаност или за сървър, който е паднал и бързо ще бъде възстановен.
- Достъп до клъстера чрез балансировач на натоварването и премахване на спрени/паднали възли от списъка, веднага щом бъдат открити. Ако това се направи бързо и ако клиентите са способни на повторни опити за свързване, ще получим постоянна наличност.
- Да предоставим на всеки клиент списък на всички възли, а клиентът при свързването случайно да избира един от тях. Ако получи грешка при опит за свързване, преминава към следващия възел в списъка, докато не се свърже.
- Да премахнем трафика от падналия/спрения възел с помощта на DNS. Това се прави с малък TTL.
Изводи
Кластеризацията на RabbitMQ има свои предимства и недостатъци. Най-съществени недостатъци са:
- при присъединяване към клъстера възлите изхвърлят данните си;
- блокиращата синхронизация води до недостъпност на опашката.
Всички трудни решения произтичат от тези две характеристики на архитектурата. Ако RabbitMQ можеше да запазва данни при повторно свързване на клъстера, синхронизацията щеше да протича по-бързо. Ако беше способен на неблокираща синхронизация, щеше да поддържа по-добре големи опашки. Отстраняването на тези два проблема сериозно би подобрило характеристиките на RabbitMQ като отказоустойчива и високодостъпна технология за обмен на съобщения. Не бих се осмелил да препоръчам RabbitMQ с клъстеризация в следните ситуации:
- Ненадеждна мрежа.
- Ненадеждно хранилище.
- Много големи опашки.
По отношение на настройките за висока наличност, помислете за следните:
ha-promote-on-failure=alwaysha-sync-mode=manualcluster_partition_handling=ignore(илиautoheal)- устойчиви съобщения
- уверете се, че клиентите се свързват към активен възел, когато някой възел излезе от строя
За последователност (сигурност на данните) разгледайте следните настройки:
- Publisher Confirms и Manual Acknowledgements от страна на потребителя
ha-promote-on-failure=when-synced, ако издателите могат да опитат отново по-късно и ако имате много надеждно хранилище! Иначе поставете=always.ha-sync-mode=automatic(но за големи неактивни опашки може да се наложи ръчен режим; освен това, помислете дали недостъпността няма да доведе до загуба на съобщения)- режим Pause Minority
- устойчиви съобщения
Още не сме разгледали всички въпроси на отказоустойчивостта и високата наличност; например, как безопасно да изпълняваме административни процедури (като например плавни обновления). Трябва да поговорим и за федерацията и плъгина Shovel.
Ако съм пропуснал нещо, моля, дайте ми знак.
Вижте също моя , където провеждам тест в клъстера на RabbitMQ с помощта на Docker и Blockade, за да тествам някои сценарии за загуба на съобщения, описани в тази статия.
Предишни статии от серията:
№1 —
№2 —
№3 —
Източник: habr.com
