Устойчивостта на софтуера се отнася до способността на приложението да продължи да функционира безпроблемно и надеждно, дори при неочаквани проблеми или повреди. Във Fintech проектите устойчивостта е от особено голямо значение поради няколко причини. Първо, компаниите са задължени да отговарят на регулаторните изисквания, а финансовите регулатори наблягат на оперативната устойчивост, за да поддържат стабилност в рамките на системата. Освен това, разпространението на дигитални инструменти и зависимостта от доставчици на услуги от трети страни излагат Fintech бизнеса на повишени заплахи за сигурността. Устойчивостта също така помага за смекчаване на рисковете от прекъсвания, причинени от различни фактори като кибернетични заплахи, пандемии или геополитически събития, защитавайки основните бизнес операции и критични активи.
Под модели на устойчивост ние разбираме набор от най-добри практики и стратегии, предназначени да гарантират, че софтуерът може да издържи на прекъсвания и да поддържа операциите си. Тези модели действат като предпазни мрежи, осигурявайки механизми за справяне с грешки, управление на натоварването и възстановяване от повреди, като по този начин гарантират, че приложенията остават здрави и надеждни при неблагоприятни условия.
Най-често срещаните стратегии за устойчивост включват преграда, кеш, резервен вариант, повторен опит и прекъсвач. В тази статия ще ги обсъдя по-подробно, с примери за проблеми, които могат да помогнат за разрешаването им.
Нека да разгледаме горната настройка. Имаме съвсем обикновено приложение с няколко задни части зад нас, от които да вземем някои данни. Има няколко HTTP клиента, свързани с тези бекендове. Оказва се, че всички те споделят един и същ пул от връзки! А също и други ресурси като CPU и RAM.
Какво ще се случи, ако един от бекендовете изпита някакви проблеми, водещи до голямо забавяне на заявката? Поради високото време за отговор, целият набор от връзки ще бъде напълно зает от заявки, чакащи отговори от backend1. В резултат на това заявките, предназначени за здравия backend2 и backend3, няма да могат да продължат, защото пулът е изчерпан. Това означава, че повреда в един от нашите бекенди може да причини повреда в цялото приложение. В идеалния случай искаме само функционалността, свързана с неуспешния бекенд, да претърпи влошаване, докато останалата част от приложението продължава да работи нормално.
Какво представлява моделът на преградата?
Терминът, модел на преградата, произлиза от корабостроенето и включва създаването на няколко изолирани отделения в кораба. Ако възникне теч в едно отделение, то се пълни с вода, но другите отделения остават незасегнати. Тази изолация предотвратява потъването на целия кораб поради еднократно нарушение.
Моделът Bulkhead може да се използва за изолиране на различни типове ресурси в рамките на приложение, предотвратявайки повреда в една част да засегне цялата система. Ето как можем да го приложим към нашия проблем:
Да предположим, че нашите backend системи имат малка вероятност да срещнат грешки поотделно. Въпреки това, когато една операция включва запитване към всички тези бекенди паралелно, всеки може независимо да върне грешка. Тъй като тези грешки се появяват независимо, общата вероятност за грешка в нашето приложение е по-висока от вероятността за грешка на който и да е отделен бекенд. Кумулативната вероятност за грешка може да се изчисли с помощта на формулата P_total=1−(1−p)^n, където n е броят на задните системи.
Например, ако имаме десет бекенда, всеки с вероятност за грешка p=0,001 (съответстващо на SLA от 99,9%), получената вероятност за грешка е:
P_total=1−(1−0,001)^10=0,009955
Това означава, че нашето комбинирано SLA пада до приблизително 99%, което илюстрира как цялостната надеждност намалява при паралелно запитване на множество бекендове. За да смекчим този проблем, можем да внедрим кеш в паметта.
Кешът в паметта служи като високоскоростен буфер за данни, съхранявайки често достъпни данни и елиминирайки необходимостта всеки път да ги извличате от потенциално бавни източници. Тъй като кешовете, съхранявани в паметта, имат 0% шанс за грешка в сравнение с извличането на данни по мрежата, те значително повишават надеждността на нашето приложение. Освен това кеширането намалява мрежовия трафик, което допълнително намалява вероятността от грешки. Следователно, като използваме кеш в паметта, можем да постигнем дори по-нисък процент грешки в нашето приложение в сравнение с нашите бекенд системи. Освен това кешовете в паметта предлагат по-бързо извличане на данни от мрежовото извличане, като по този начин намаляват забавянето на приложението – забележително предимство.
За персонализирани данни, като потребителски профили или препоръки, използването на кешове в паметта също може да бъде много ефективно. Но ние трябва да гарантираме, че всички заявки от потребител последователно отиват към едно и също приложение, за да използват кеширани данни за тях, което изисква лепкави сесии. Внедряването на лепкави сесии може да бъде предизвикателство, но за този сценарий не се нуждаем от сложни механизми. Приемливо е незначително ребалансиране на трафика, така че стабилен алгоритъм за балансиране на натоварването като последователно хеширане ще бъде достатъчен.
Нещо повече, в случай на повреда на възел, последователното хеширане гарантира, че само потребителите, свързани с неуспешния възел, преминават през повторно балансиране, минимизирайки смущенията в системата. Този подход опростява управлението на персонализирани кешове и подобрява цялостната стабилност и производителност на нашето приложение.
Ако данните, които възнамеряваме да кешираме, са критични и се използват във всяка заявка, която обработва нашата система, като например политики за достъп, абонаментни планове или други жизненоважни обекти в нашия домейн - източникът на тези данни може да представлява значителна точка на повреда в нашата система. За да се справим с това предизвикателство, един подход е да копираме напълно тези данни директно в паметта на нашето приложение.
В този сценарий, ако обемът на данните в източника е управляем, можем да инициираме процеса, като изтеглим моментна снимка на тези данни в началото на нашето приложение. Впоследствие можем да получаваме събития за актуализации, за да гарантираме, че кешираните данни остават синхронизирани с източника. Чрез приемането на този метод ние повишаваме надеждността на достъпа до тези важни данни, тъй като всяко извличане се извършва директно от паметта с 0% вероятност за грешка. Освен това извличането на данни от паметта е изключително бързо, като по този начин се оптимизира производителността на нашето приложение. Тази стратегия ефективно намалява риска, свързан с разчитането на външен източник на данни, като гарантира последователен и надежден достъп до критична информация за работата на нашето приложение.
Въпреки това, необходимостта от изтегляне на данни при стартиране на приложението, като по този начин забавя процеса на стартиране, нарушава един от принципите на „12-факторното приложение“, застъпващо се за бързо стартиране на приложението. Но ние не искаме да се лишим от предимствата на използването на кеширане. За да разрешим тази дилема, нека проучим потенциалните решения.
Бързото стартиране е от решаващо значение, особено за платформи като Kubernetes, които разчитат на бърза миграция на приложения към различни физически възли. За щастие, Kubernetes може да управлява бавно стартиращи приложения, като използва функции като стартиращи сонди.
Друго предизвикателство, с което може да се сблъскаме, е актуализирането на конфигурациите, докато приложението работи. Често коригирането на времената на кеша или времето на изчакване на заявката е необходимо за разрешаване на производствени проблеми. Дори ако можем бързо да внедрим актуализирани конфигурационни файлове в нашето приложение, прилагането на тези промени обикновено изисква рестартиране. С удълженото време за стартиране на всяко приложение, непрекъснатото рестартиране може значително да забави внедряването на корекции за нашите потребители.
За да се справите с това, едно решение е да съхранявате конфигурации в паралелна променлива и периодично да я актуализирате във фонова нишка. Някои параметри обаче, като изчакване на HTTP заявка, може да изискват повторно инициализиране на HTTP или клиенти на база данни, когато съответната конфигурация се промени, което представлява потенциално предизвикателство. И все пак някои клиенти, като драйвера на Cassandra за Java, поддържат автоматично презареждане на конфигурации, опростявайки този процес.
Внедряването на презареждащи се конфигурации може да смекчи отрицателното въздействие на дългите времена за стартиране на приложението и да предложи допълнителни предимства, като например улесняване на внедряването на флагове за функции. Този подход ни позволява да поддържаме надеждността и отзивчивостта на приложението, като същевременно управляваме ефективно актуализациите на конфигурацията.
Сега нека да разгледаме друг проблем: в нашата система, когато потребителска заявка бъде получена и обработена чрез изпращане на заявка към бекенд или база данни, понякога се получава отговор за грешка вместо очакваните данни. Впоследствие нашата система отговаря на потребителя с „грешка“.
Въпреки това, в много сценарии може да е по-предпочитано да се покажат леко остарели данни заедно със съобщение, показващо, че има забавяне на опресняването на данните, вместо да оставя потребителя с голямо червено съобщение за грешка.
За да се справим с този проблем и да подобрим поведението на нашата система, можем да внедрим резервния модел. Концепцията зад този модел включва наличието на вторичен източник на данни, който може да съдържа данни с по-ниско качество или свежест в сравнение с първичния източник. Ако първичният източник на данни е недостъпен или връща грешка, системата може да се върне към извличане на данни от този вторичен източник, като гарантира, че на потребителя се представя някаква форма на информация, вместо да показва съобщение за грешка.
Ако погледнете снимката по-горе, ще забележите прилика между проблема, пред който сме изправени сега, и този, който срещнахме с примера с кеша.
За да го разрешим, можем да обмислим прилагането на модел, известен като повторен опит. Вместо да разчита на кешове, системата може да бъде проектирана така, че автоматично да изпраща отново заявката в случай на грешка. Този модел за повторен опит предлага по-проста алтернатива и може ефективно да намали вероятността от грешки в нашето приложение. За разлика от кеширането, което често изисква сложни механизми за анулиране на кеша, за да се справят с промените в данните, повторният опит за неуспешни заявки е относително лесен за прилагане. Тъй като анулирането на кеша се счита широко за една от най-предизвикателните задачи в софтуерното инженерство, приемането на стратегия за повторен опит може да рационализира обработката на грешки и да подобри устойчивостта на системата.
Приемането на стратегия за повторен опит обаче, без да се вземат предвид потенциалните последствия, може да доведе до допълнителни усложнения.
Нека си представим, че един от нашите бекендове претърпява повреда. При такъв сценарий инициирането на повторни опити към неуспешния бекенд може да доведе до значително увеличение на обема на трафика. Този внезапен скок в трафика може да претовари бекенда, влошавайки повредата и потенциално причинявайки каскаден ефект в цялата система.
За да се справите с това предизвикателство, е важно да допълните шаблона за повторен опит с модела на прекъсвача. Прекъсвачът служи като предпазен механизъм, който следи процента на грешки на услугите надолу по веригата. Когато процентът на грешки надхвърли предварително определен праг, прекъсвачът прекъсва заявките към засегнатата услуга за определена продължителност. През този период системата се въздържа от изпращане на допълнителни заявки, за да позволи възстановяване на времето за неуспешна услуга. След определения интервал прекъсвачът предпазливо позволява ограничен брой заявки да преминат, проверявайки дали услугата се е стабилизирала. Ако услугата се възстанови, нормалният трафик постепенно се възстановява; в противен случай веригата остава отворена, като продължава да блокира заявките, докато услугата възобнови нормалната си работа. Чрез интегриране на модела на прекъсвача заедно с логиката за повторен опит, ние можем ефективно да управляваме ситуации на грешка и да предотвратим претоварване на системата по време на повреди в задната част.
В заключение, чрез прилагане на тези модели на устойчивост, ние можем да подсилим нашите приложения срещу извънредни ситуации, да поддържаме висока наличност и да предоставим безпроблемно изживяване на потребителите. Освен това бих искал да подчертая, че телеметрията е още един инструмент, който не бива да се пренебрегва при осигуряване на устойчивост на проекта. Добрите регистрационни файлове и показатели могат значително да подобрят качеството на услугите и да осигурят ценна информация за тяхното представяне, помагайки за вземането на информирани решения за тяхното допълнително подобряване.