В тази статия ще представим някои от основните подходи за мащабиране на софтуерна система. Типът системи, към които е насочена тази поредица от статии, са системите с интернет, които всички използваме всеки ден. Ще ви позволя да назовете вашия любим. Тези системи приемат заявки от потребители чрез уеб и мобилни интерфейси, съхраняват и извличат данни въз основа на потребителски заявки или събития (напр. система, базирана на GPS) и имат някои интелигентни функции като предоставяне на препоръки или предоставяне на известия въз основа на предишни потребителски взаимодействия.

Ще започнем с прост дизайн на системата и ще покажем как тя може да бъде мащабирана. В процеса ще бъдат въведени няколко концепции, които ще разгледаме много по-подробно по-късно в тази серия. Ето защо тази статия дава широк преглед на тези концепции и как те помагат за мащабируемостта - наистина една вихрушка! Ако сте пропуснали част 1 от тази серия, ето я!

Основна системна архитектура

На практика всички мащабни системи започват с малки и се разрастват благодарение на техния успех. Обичайно и разумно е да започнете с рамка за разработка като Ruby on Rails или Django или еквивалентна, която насърчава бързото развитие, за да накарате системата бързо да работи. Типична, много проста софтуерна архитектура за „стартерни“ системи, която много наподобява това, което получавате с рамки за бърза разработка, е показана на Фигура 1. Това включва ниво на клиента, ниво на услугата за приложения и ниво на база данни. Ако използвате Rails или еквивалент, вие също получавате рамка, която твърдо свързва шаблон Model-View-Controller (MVC) за обработка на уеб приложения и Object-Relational Mapper (ORM), който генерира SQL заявки.

С тази архитектура потребителите изпращат заявки към приложението от своето мобилно приложение или уеб браузър през Интернет. Магията на интернет мрежите доставя тези заявки до услугата за приложения, която работи на машина, хоствана в някой корпоративен или търговски облачен център за данни. Комуникациите използват стандартен мрежов протокол, обикновено HTTP.

Услугата за приложения изпълнява код, който поддържа интерфейс за програмиране на приложения (API), който клиентите използват за форматиране на данни и изпращане на HTTP заявки. При получаване на заявка услугата изпълнява кода, свързан със заявения API. В процеса той може да чете от или да записва в база данни, в зависимост от семантиката на API. Когато заявката е завършена, сървърът изпраща резултатите на клиента, за да ги покаже в тяхното приложение или браузър.

Много системи концептуално изглеждат точно така. Кодът на услугата за приложение използва среда за изпълнение, която позволява едновременното обработване на множество заявки от множество потребители. Има безброй от тези технологии за сървър на приложения - JEE и Spring за Java, Flask за Python - които се използват широко в този сценарий.

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

И все пак, докато натоварването на заявките остава сравнително ниско, тази архитектура на приложението може да е достатъчна. Услугата има капацитет да обработва заявки с постоянно ниска латентност. Ако натоварванията на заявките продължават да растат, това означава, че в крайна сметка закъсненията ще нараснат, тъй като сървърът няма достатъчен капацитет на процесора/паметта за обема на едновременните заявки и следователно обработката на заявките ще отнеме повече време. При тези обстоятелства единственият ни сървър е претоварен и се е превърнал в пречка.

В този случай първата стратегия за мащабиране обикновено е „увеличаване“ на хардуера за обслужване на приложения. Например, ако приложението ви работи на AWS, можете да надстроите сървъра си от скромен екземпляр t3.xlarge с 4 (виртуални) процесора и 16 GB памет до екземпляр t3.2xlarge, «което удвоява броя на vCPU и паметта, налична за приложението".

Увеличаването е просто. Той прави много приложения от реалния свят дълъг път за поддържане на по-големи натоварвания. Очевидно просто струва повече пари за хардуер, но това е мащабиране за вас.

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

Мащабиране

Мащабирането разчита на способността да се репликира услуга в архитектурата и да се изпълняват множество копия на множество сървърни възли. Заявките от клиенти се разпределят между репликите, така че на теория, ако имаме N реплики, всеки сървърен възел обработва {#requests/N} заявки. Тази проста стратегия увеличава капацитета на приложението и следователно мащабируемостта.

За да мащабираме успешно приложение, имаме нужда от два основни елемента в нашия дизайн. Както е показано на фигура 2, това са:

Балансьор на натоварването: Всички потребителски заявки се изпращат до балансьор на натоварването, който избира реплика на услуга за обработка на заявката. Съществуват различни стратегии за избор на услуга, всички с основната цел всеки ресурс за обработка да бъде еднакво зает. Устройството за балансиране на натоварването също предава отговорите от услугата обратно на клиента. Повечето програми за балансиране на натоварването принадлежат към клас интернет компоненти, известни като „обратни проксита“, които контролират достъпа до сървърните ресурси за клиентски заявки. Като посредник, обратните проксита добавят допълнителен мрежов скок за заявка и следователно трябва да имат изключително ниска латентност, за да минимизират допълнителните разходи, които въвеждат. Има много готови решения за балансиране на натоварването, както и специфични за облачни доставчици, и ние ще разгледаме общите характеристики на тях много по-подробно в по-късна статия.

Услуги без състояние: За да бъде балансирането на натоварването ефективно и да споделя равномерно заявките, балансиращият натоварването трябва да е свободен да изпраща последователни заявки от един и същи клиент до различни екземпляри на услуга за обработка. Това означава, че внедряванията на API в услугите не трябва да запазват знания или състояние, свързани със сесията на отделен клиент. Когато потребител осъществи достъп до приложение, потребителска сесия се създава от услугата и уникална идентифицирана сесия се управлява вътрешно, за да се идентифицира последователността на потребителските взаимодействия и да се проследи състоянието на сесията. Класически пример за състояние на сесия е пазарска количка. За да се използва ефективно балансиращото натоварване, данните, представляващи текущото съдържание на количката на потребителя, трябва да се съхраняват някъде - обикновено хранилище за данни - така че всяка реплика на услуга да има достъп до това състояние, когато получи заявка като част от потребителска сесия. На фигура 2 това е обозначено като Session Store.

Мащабирането е привлекателно, тъй като на теория можете да продължите да добавяте нов (виртуален) хардуер и услуги, за да се справите с увеличените натоварвания от потребителски заявки и да поддържате латентностите постоянни и ниски. Веднага щом забележите нарастване на закъсненията, внедрявате друг екземпляр на услугата. Това не изисква промени в кода и следователно е сравнително евтино - просто плащате за хардуера, който внедрявате.

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

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

Мащабиране на базата данни с кеширане

Мащабирането чрез увеличаване на броя на процесорите, паметта и дисковете в сървър на база данни може да помогне значително за увеличаване на капацитета на системата. Например, към момента на писане Google Cloud Platform може да осигури SQL база данни на възел db-n1-highmem-96, който има 96 vCPU, 624 GB памет, 30 TB диск и може да поддържа 4000 връзки. Това ще струва някъде между $6K и $16K на година, което ми звучи добре. Мащабирането е много често срещана стратегия за мащабиране на бази данни.

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

Във връзка с увеличаването на мащаба, много ефективен подход е да правите заявки в базата данни възможно най-рядко във вашите услуги. Това може да се постигне чрез използване на разпределено кеширане в нивото на услугата. Кешът съхранява наскоро извлечени и често достъпни резултати от базата данни в паметта, така че да могат да бъдат бързо извлечени, без да натоварват базата данни. За данни, които се четат често и се променят рядко, вашата логика на обработка трябва да бъде променена, за да провери първо разпределен кеш, като например хранилище Redis или memcached. Тези кеш технологии по същество са разпределени хранилища на ключ-стойност с много прости API. Тази схема е илюстрирана на Фигура 3. Имайте предвид, че Session Store от Фигура 2 е изчезнал. Това е така, защото можем да използваме разпределен кеш с общо предназначение, за да съхраняваме идентификатори на сесии заедно с данни на приложението.

Достъпът до кеша изисква отдалечено обаждане от вашата услуга, но ако данните, от които се нуждаете, са в кеша, в бърза мрежа това е много по-евтино от запитване до екземпляра на базата данни. Въвеждането на слой за кеширане също изисква вашата логика на обработка да бъде модифицирана, за да проверява за кеширани данни. Ако това, което искате, не е в кеша — пропуск в кеша — вашият код все още трябва да прави запитвания към базата данни и да зарежда резултатите в кеша, както и да връща резултатите на повикващия. Също така трябва да решите кога да премахнете или обезсилите кешираните резултати - това зависи от толерантността на вашето приложение към обслужване на остарели резултати на клиенти и обема на данните, които кеширате.

Една добре проектирана схема за кеширане може да бъде абсолютно безценна при мащабиране на система. Кеширането работи чудесно за данни, които рядко се променят и са достъпни често, като данни за инвентар, събития и контакти. Ако можете да се справите с голям процент, като 80% или повече, заявки за четене от вашия кеш, тогава вие на практика купувате допълнителен капацитет във вашите бази данни, тъй като те не участват в обработката на заявки.

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

Разпространение на базата данни

През 2020 г. има повече технологии за разпределени бази данни, отколкото вероятно искате да си представите. Това е сложна област и ще я разгледаме обстойно в друга статия. Най-общо казано, има две основни категории:

  1. Разпределени SQL магазини от големи доставчици като Oracle и IBM. Те позволяват на организациите да мащабират своята SQL база данни сравнително безпроблемно, като съхраняват данните на множество дискове, които се запитват от множество реплики на машина за бази данни. Тези множество машини логично изглеждат за приложението като единна база данни, което минимизира промените в кода.
  2. Разпространени така наречените NoSQL магазини от цял ​​набор от доставчици. Тези продукти използват различни модели на данни и езици за заявки. Те разпространяват данни между множество възли, които изпълняват двигателя на базата данни, всеки със собствено локално прикачено хранилище. Отново местоположението на данните е прозрачно за приложението и обикновено се контролира от дизайна на модела на данни чрез функции за хеширане на ключовете на базата данни. Водещи продукти в тази категория са Cassandra, MongoDB и Neo4j.

Фигура 4 показва как нашата архитектура включва разпределена база данни. С нарастването на обемите от данни разпределената база данни има функции, позволяващи увеличаване на броя на възлите за съхранение. Тъй като възлите се добавят (или премахват), данните, управлявани във всички възли, се ребалансират, за да се опитат да гарантират, че капацитетът за обработка и съхранение на всеки възел се използва еднакво.

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

Ако използвате основен доставчик на облак, има и два варианта за внедряване за вашето ниво на данни. Можете да разположите свои собствени виртуални ресурси и да изградите, конфигурирате и администрирате свои собствени сървъри за разпределени бази данни. Като алтернатива можете да използвате бази данни, хоствани в облак, като DynamoDB. Последната категория опростява административното усилие, свързано с управлението, наблюдението и мащабирането на базата данни, тъй като много от тези задачи по същество стават отговорност на доставчика на облак, който изберете. Както обикновено, важи принципът без безплатен обяд.

Множество нива на обработка

Всяка реалистична система, която трябва да мащабираме, ще има много различни услуги, които взаимодействат, за да обработят заявка. Например, достъпът до уеб страница на сайта Amazon.com може да изисква повече от извикване на 100 различни услуги, преди да се върне общ отговор на потребителя.

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

Този дизайн също така насърчава наличието на различни услуги с балансиран товар на всяко ниво в архитектурата. Например Фигура 6 илюстрира две репликирани услуги за интернет, които и двете използват основна услуга, осигуряваща достъп до база данни. Всяка услуга е балансирана по натоварване и използва кеширане, за да осигури висока производителност и наличност. Този дизайн често се използва, например, за предоставяне на услуга за уеб клиенти и услуга за мобилни клиенти, всяка от които може да бъде мащабирана независимо въз основа на натоварването, което изпитват. Обикновено се нарича „модел на задния за предния край (BFF)“.

Освен това, като разделим приложението на множество независими услуги, можем да мащабираме всяка въз основа на търсенето на услугата. Ако например видим нарастващ обем заявки от мобилни потребители и намаляващи обеми от уеб потребители, можем да предоставим различен брой екземпляри за всяка услуга, за да задоволим търсенето. Това е основно предимство на преработването на монолитни приложения в множество независими услуги, които могат да бъдат отделно изграждани, тествани, внедрявани и мащабирани.

Повишаване на отзивчивостта

Повечето заявки за клиентско приложение очакват отговор. Потребителят може да иска да види всички артикули на търга за дадена продуктова категория или да види недвижимите имоти, които се предлагат за продажба на дадено място. В тези примери клиентът изпраща заявка и чака, докато се получи отговор. Интервалът от време между изпращането на заявката и получаването на резултата е латентността на заявката. Можем да намалим закъсненията, като използваме кеширане и предварително изчислени отговори, но много заявки пак ще доведат до достъп до базата данни.

Подобен сценарий съществува за заявки, които актуализират данни в приложение. Ако потребител актуализира своя адрес за доставка непосредствено преди да направи поръчка, новият адрес за доставка трябва да бъде запазен, така че потребителят да може да потвърди адреса, преди да натисне бутона „покупка“. Това е известно като „четете вашите собствени записи“. Закъснението в този случай включва времето за запис на базата данни, което се потвърждава от отговора, който потребителят получава.

На някои заявки за актуализиране обаче може да се отговори успешно, без да се запазват напълно данните в база данни. Например, скиорите и сноубордистите сред вас ще бъдат запознати със системите за сканиране на билети за лифтове, които проверяват дали имате валидна карта за каране на лифтове през този ден. Те също така записват кои лифтове използвате, времето, в което се качвате и т.н. Изперкалите скиори/сноубордисти могат след това да използват мобилното приложение на курорта, за да видят колко лифта карат на ден.

Докато човек чака да се качи на асансьора, скенер потвърждава пропуска с помощта на четец на RFID чип. След това информацията за ездача, лифта и времето се изпраща по интернет до услуга за събиране на данни, управлявана от ски курорта. Водачът на асансьора не трябва да чака тази актуализация да се случи, тъй като забавянето може да забави процеса на зареждане на асансьора. Също така няма очаквания от оператора на асансьора, че може незабавно да използва своето приложение, за да гарантира, че тези данни са заснети. Те просто се качват на асансьора, говорят с приятелите си и планират следващото си бягане.

Реализациите на услугата могат да използват този тип сценарии, за да намалят закъсненията и да подобрят отзивчивостта. Данните за събитието се изпращат до услугата, която потвърждава получаването и едновременно съхранява данните в отдалечена опашка за последващо записване в базата данни. Писането на съобщение в опашка е много по-бързо от писането в база данни и това позволява заявката да бъде успешно потвърдена много по-бързо. Друга бекенд услуга е разгърната, за да чете съобщения от опашката и да записва данните в базата данни. Когато потребителят провери своите пътувания с лифта - може би 3 часа или 3 дни по-късно - данните са запазени успешно.

Основната архитектура за прилагане на този подход е илюстрирана на фигура 7.

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

Ключът е, че данните в крайна сметка се запазват. Евентуално обикновено означава най-много няколко секунди, но случаите на употреба, които използват този дизайн, трябва да са устойчиви на по-големи закъснения, без това да повлияе на потребителското изживяване.

Резюме и допълнителна литература

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

Друга област, която тази глава заобиколи, е темата за софтуерната архитектура. Използвали сме термина услуги за разпределени компоненти в архитектура, които прилагат бизнес логика на приложението и достъп до база данни. Тези услуги са независимо разгърнати процеси, които комуникират чрез механизми за отдалечена комуникация като HTTP. В архитектурно отношение тези услуги са най-близко огледални от тези в модела Service Oriented Architecture (SOA), утвърден архитектурен подход за изграждане на разпределени системи. По-модерна еволюция на този подход се върти около микроуслугите. Те обикновено са по-сплотени, капсулирани услуги, които насърчават непрекъснато развитие и внедряване.

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

Mark Richards и Neal Ford, Fundamentals of Software Architecture: An Engineering Approach 1st Edition, O’Reilly Media, 2020

И накрая, има клас софтуерни архитектури за големи данни, които се занимават с някои от проблемите, които излизат на преден план при много големи колекции от данни. Един от най-известните е повторната обработка на данни. Това се случва, когато необработените данни, които вече са били съхранени и анализирани, трябва да бъдат повторно анализирани поради промени в кода. Тази повторна обработка може да възникне поради софтуерни корекции или въвеждането на нови алгоритми, които могат да извлекат повече информация от оригиналните необработени данни. В тази статия има добра дискусия за архитектурите Lambda и Kappa, които са видни в това пространство.

Джей Крепс, „Разпитване на ламбда архитектурата“,