Изграждането на надеждна, високо достъпна, мащабируема разпределена система изисква придържане към специфични техники, принципи и модели. Проектирането на такива системи включва справяне с безброй предизвикателства. Сред най-разпространените и основни проблеми е проблемът с двойното записване .
„Проблемът с двойно записване“ е предизвикателство, което възниква в разпределените системи, главно когато се работи с множество източници на данни или бази данни, които трябва да се поддържат синхронизирани. Отнася се до трудността да се гарантира, че промените в данните се записват последователно в различни хранилища на данни, като бази данни или кешове, без да се въвеждат проблеми като несъответствия в данните, конфликти или затруднения в производителността.
Архитектурата на микроуслугите и базата данни с шаблони за всяка услуга ви носи много предимства, като независимо внедряване и мащабиране, изолирани откази и потенциално повишаване на скоростта на разработка. Операциите обаче изискват промени между множество микроуслуги, което ви принуждава да мислите за надеждно решение за справяне с този проблем.
Нека разгледаме сценарий, при който нашият домейн включва приемане на заявления за заем, оценката им и след това изпращане на уведомителни сигнали до клиентите.
В духа на принципа на единната отговорност, закона на Конуей и подхода за проектиране, управляван от домейни, след няколко сесии на щурмуване на събития, целият домейн беше разделен на три поддомейна с дефинирани ограничени контексти с ясни граници, модели на домейни и повсеместен език.
Първият е натоварен с въвеждането и съставянето на нови заявления за заем. Втората система оценява тези приложения и взема решения въз основа на предоставените данни. Този процес на оценка, включително KYC/KYB, проверки за борба с измами и кредитен риск, може да отнеме много време, което налага възможността да се обработват хиляди приложения едновременно. Следователно тази функционалност е делегирана на специална микроуслуга със собствена база данни, което позволява независимо мащабиране.
Освен това тези подсистеми се управляват от два различни екипа, всеки със собствени цикли на издаване, споразумения за ниво на обслужване (SLA) и изисквания за мащабируемост.
И накрая , съществува специализирана услуга за уведомяване за изпращане на сигнали до клиентите.
Ето прецизно описание на основния случай на използване на системата:
Това е доста проста и примитивна система на пръв поглед, но нека се потопим в това как услугата за кандидатстване за заем обработва командата за подаване на заявление за заем.
Можем да разгледаме два подхода за взаимодействие на услугата:
First-Local-Commit-Ten-Publish: При този подход услугата актуализира своята локална база данни (комити) и след това публикува събитие или съобщение до други услуги.
First-Publish-Then-Local-Commit: Обратно, този метод включва публикуване на събитие или съобщение преди извършване на промените в локалната база данни.
И двата метода имат своите недостатъци и са само частично безопасни за комуникация в разпределени системи.
Това е диаграма на последователността на прилагане на първия подход.
В този сценарий услугата за кандидатстване за заем използва подхода First-Local-Commit-Ten-Publish , при който първо извършва транзакция и след това се опитва да изпрати известие до друга система. Въпреки това, този процес е податлив на неуспех, ако например има проблеми с мрежата, услугата за оценка е недостъпна или услугата за кандидатстване за заем срещне грешка с липса на памет (OOM) и се срива. В такива случаи съобщението ще бъде загубено, оставяйки оценката без известие за новото заявление за заем, освен ако не бъдат приложени допълнителни мерки.
И второто.
В сценария „Първо публикуване, след това локално ангажиране“ , услугата за кандидатстване за заем е изправена пред по-значителни рискове. Може да информира Услугата за оценка за ново приложение, но да не успее да запази тази актуализация локално поради проблеми като проблеми с базата данни, грешки в паметта или грешки в кода. Този подход може да доведе до значителни несъответствия в данните, което може да причини сериозни проблеми, в зависимост от това как услугата за преглед на заеми обработва входящите заявления.
Следователно трябва да намерим решение, което предлага стабилен механизъм за публикуване на събития на външни потребители. Но преди да се задълбочим в потенциалните решения, първо трябва да изясним типовете гаранции за доставка на съобщения, постижими в разпределени системи.
Има четири вида гаранции, които можем да постигнем.
Без гаранции
Няма гаранция, че съобщението ще бъде доставено до местоназначението. Подходът First-Local-Commit-Then-Publish е точно за това. Потребителите могат да получат съобщения веднъж, няколко пъти или изобщо никога.
Най-много веднъж доставка
Доставка най-много веднъж означава, че съобщението ще бъде доставено до дестинацията най-много 1 път. Подходът First-Local-Commit-Ten-Publish може да се приложи и по този начин с политиката за повторен опит на опити със стойност едно.
Поне веднъж доставка\Потребителите ще получат и обработят всяко съобщение, но могат да получат едно и също съобщение повече от веднъж.
Точно веднъж доставка \ Точно веднъж доставка означава, че потребителят ще получи съобщението ефективно веднъж.
Технически е възможно да се постигне с Kafka транзакции и специфично идемпотентно внедряване на производител и потребител.
В повечето случаи доставката „поне веднъж“ гарантира справяне с много проблеми, като гарантира, че съобщенията се доставят поне веднъж, но потребителите трябва да са безпомощни. Въпреки това, предвид неизбежните мрежови повреди, цялата потребителска логика трябва да бъде безсилна, за да се избегне обработката на дублирани съобщения, независимо от гаранциите на производителя. Следователно това изискване не е толкова недостатък, колкото отразява реалността.
Има много решения на този проблем, които имат своите предимства и недостатъци.
Според Wikipedia двуфазовият ангажимент (2PC) е протокол за разпределени транзакции, използван в компютърните науки и системите за управление на бази данни, за да се гарантира последователността и надеждността на разпределените транзакции. Той е проектиран за ситуации, в които множество ресурси (напр. бази данни) трябва да участват в една транзакция и гарантира, че или всички те извършват транзакцията, или всички те я прекъсват, като по този начин поддържат последователност на данните. Звучи точно това, от което се нуждаем, но Two-Phase Commit има няколко недостатъка:
Най-очевидното решение за архитектурата на микроуслугите е да се приложи шаблон (или дори понякога анти-модел) — споделена база данни. Този подход е много интуитивен, ако имате нужда от последователност на транзакциите в множество таблици в различни бази данни, просто използвайте една споделена база данни за тези микроуслуги.
Недостатъците на този подход включват въвеждане на единична точка на повреда, възпрепятстване на независимо мащабиране на база данни и ограничаване на възможността за използване на различни решения за база данни, които са най-подходящи за конкретни изисквания и случаи на употреба. Освен това ще са необходими модификации на кодовите бази на микроуслугите, за да се поддържа такава форма на разпределена транзакция.
„ Транзакционната изходяща кутия “ е модел на проектиране, използван в разпределени системи за осигуряване на надеждно разпространение на съобщения, дори в лицето на ненадеждни системи за съобщения. Това включва съхраняване на събития в определена таблица „OutboxEvents“ в рамките на същата транзакция като самата операция. Този подход е в съответствие с ACID свойствата на релационните бази данни. За разлика от това, много бази данни без SQL не поддържат напълно свойствата на ACID, като вместо това избират принципите на теоремата на CAP и философията на BASE, които дават приоритет на наличността и евентуалната последователност пред стриктната последователност.
Транзакционната изходяща кутия осигурява поне веднъж гаранция и може да се приложи с няколко подхода:
Записване на регистъра на транзакциите
Издател на анкети
Подходът за проследяване на регистъра на транзакциите предполага използване на специфични за базата данни решения като CDC (Change Data Capture). Основните недостатъци на този подход са:
Решения, специфични за бази данни
Повишена латентност поради спецификата на внедряването на CDC
Друг метод е Polling Publisher , който улеснява разтоварването на изходящата кутия чрез запитване на таблицата с изходяща кутия. Основният недостатък на този подход е потенциалът за повишено натоварване на базата данни, което може да доведе до по-високи разходи. Освен това, не всички No-SQL бази данни поддържат ефективно запитване за конкретни сегменти от документи. Следователно извличането на цели документи може да доведе до влошаване на производителността.
Ето малка диаграма на последователността, обясняваща как работи.
Основното предизвикателство с модела Transactional Outbox се крие в неговата зависимост от ACID свойствата на базата данни. Може да е лесно в типичните OLTP бази данни, но поставя предизвикателства в сферата на NoSQL. За да се справи с това, потенциално решение е да се използва регистрационният файл за добавяне (например Kafka) направо от инициирането на обработката на заявката.
Вместо директно да обработваме командата „подаване на заявление за заем“, ние незабавно я изпращаме до вътрешна тема на Kafka и след това връщаме „приет“ резултат на клиента. Въпреки това, тъй като е много вероятно командата все още да трябва да бъде обработена, не можем незабавно да информираме клиента за резултата. За да управляваме тази евентуална последователност, можем да използваме техники като дълго анкетиране, анкетиране, инициирано от клиента, оптимистични актуализации на потребителския интерфейс или използване на WebSockets или изпратени от сървъра събития за известия. Това обаче е съвсем отделна тема, така че нека се върнем към първоначалната ни тема.
Изпратихме съобщението по вътрешна тема на Кафка. След това услугата за кандидатстване за заем използва това съобщение - същата команда, която е получила от клиента - и започва обработката. Първо, той изпълнява някаква бизнес логика; само след като тази логика се изпълни успешно и резултатите се запазят, той публикува нови съобщения в публична тема на Kafka.
Нека да разгледаме малко псевдокод.
public async Task HandleAsync(SubmitLoanApplicationCommand command, ...) { //First, process business logic var loanApplication = await _loanApplicationService.HandleCommandAsync(command, ...); //Then, send new events to public Kafka topic producer.Send(new LoanApplicationSubmittedEvent(loanApplication.Id)); //Then, commit offset consumer.Commit(); }
Какво става, ако обработката на бизнес логиката се провали? Без притеснения, тъй като отместването все още не е извършено, съобщението ще бъде опитано отново.
Какво става, ако изпращането на нови събития до Kafka се провали? Без притеснения, тъй като бизнес логиката е идемпотентна, тя няма да създаде дублирано заявление за заем. Вместо това ще се опита да изпрати отново съобщения до публичната тема за Кафка.
Какво става, ако съобщенията се изпращат до Kafka, но офсетният ангажимент е неуспешен? Без притеснения, тъй като бизнес логиката е идемпотентна, тя няма да създаде дублиращо се заявление за заем. Вместо това, той ще изпрати отново съобщения до публичната тема на Kafka и ще се надява, че офсетният ангажимент ще успее този път.
Основните недостатъци на този подход включват добавената сложност, свързана с нов стил на програмиране, евентуална последователност (тъй като клиентът няма да разбере веднага резултата) и изискването цялата бизнес логика да бъде идемпотентна.
Какво е източник на събития и как може да се приложи тук? Източникът на събития е софтуерен архитектурен модел, използван за моделиране на състоянието на система чрез улавяне на всички промени в нейните данни като поредица от неизменни събития. Тези събития представляват факти или преходи на състояния и служат като единствен източник на истина за текущото състояние на системата. Така че, технически, чрез прилагане на система за източник на събития, ние вече имаме всички събития в EventStore и това EventStore може да се използва от потребителите като единствен източник на истина за случилото се. Няма нужда от конкретно решение за база данни за проследяване на всички промени или притеснения относно подреждането, единственият проблем е да седи от страна на четене, тъй като за да можете да получите действителното състояние на обекта, е необходимо да възпроизвеждате всички събития.
В тази статия прегледахме няколко подхода за изграждане на надеждни съобщения в разпределени системи. Има няколко препоръки, които можем да вземем предвид, докато изграждаме системи с тези характеристики
Следващият път ще разгледаме по-практичен пример за внедряване на транзакционна изходяща кутия. Вижте
ти!