Удобни архитектурни модели

Хей Хабр!

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

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

Хоризонтално мащабиране

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

Например ще взема абстрактно хранилище за облачни файлове, тоест някакъв аналог на OwnCloud, OneDrive и т.н.

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

Удобни архитектурни модели
Разликата между подходите: при вертикално мащабиране сме готови да увеличим мощността на възлите, а при хоризонтално мащабиране сме готови да добавим нови възли, за да разпределим натоварването.

CQRS

Разделяне на отговорността за запитване на команди Доста важен модел, тъй като позволява на различни клиенти не само да се свързват с различни услуги, но и да получават едни и същи потоци от събития. Предимствата му не са толкова очевидни за просто приложение, но е изключително важно (и просто) за натоварена услуга. Неговата същност: входящите и изходящите потоци от данни не трябва да се пресичат. Тоест не можете да изпратите заявка и да очаквате отговор; вместо това изпращате заявка до услуга A, но получавате отговор от услуга B.

Първият бонус на този подход е възможността да се прекъсне връзката (в широкия смисъл на думата), докато се изпълнява дълга заявка. Например, нека вземем повече или по-малко стандартна последователност:

  1. Клиентът изпрати заявка до сървъра.
  2. Сървърът започна дълго време за обработка.
  3. Сървърът отговори на клиента с резултата.

Нека си представим, че в точка 2 връзката е прекъсната (или мрежата се е свързала отново, или потребителят е отишъл на друга страница, прекъсвайки връзката). В този случай ще бъде трудно за сървъра да изпрати отговор на потребителя с информация какво точно е обработено. Използвайки CQRS, последователността ще бъде малко по-различна:

  1. Клиентът се е абонирал за актуализации.
  2. Клиентът изпрати заявка до сървъра.
  3. Сървърът отговори „заявката е приета“.
  4. Сървърът отговори с резултата през канала от точка “1”.

Удобни архитектурни модели

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

Интересното е, че кодът за обработка на входящите съобщения става един и същ (не 100%) както за събития, които са били повлияни от самия клиент, така и за други събития, включително такива от други клиенти.

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

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

Източник на събития

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

От тук получаваме важен факт - бърза разпределена система не може да бъде синхронизирана, защото тогава ще намалим производителността. От друга страна, често се нуждаем от определена последователност между компонентите. И за това можете да използвате подхода с евентуална последователност, където е гарантирано, че ако няма промени в данните за известен период от време след последната актуализация („евентуално“), всички заявки ще върнат последната актуализирана стойност.

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

Да се ​​върнем обаче на първоначалната задача. Ако част от системата може да бъде изградена с евентуална последователност, тогава можем да изградим следната диаграма.

Удобни архитектурни модели

Важни характеристики на този подход:

  • Всяка входяща заявка се поставя в една опашка.
  • Докато обработва заявка, услугата може също да постави задачи в други опашки.
  • Всяко входящо събитие има идентификатор (който е необходим за дедупликация).
  • Опашката идеологически работи по схемата „само добавяне“. Не можете да премахвате елементи от него или да ги пренареждате.
  • Опашката работи по схемата FIFO (съжалявам за тавтологията). Ако трябва да направите паралелно изпълнение, тогава на един етап трябва да преместите обекти в различни опашки.

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

Удобни архитектурни модели

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

А за двама потребители диаграмата ще изглежда така (услугите, предназначени за различни потребители, са обозначени с различни цветове):

Удобни архитектурни модели

Бонуси от такава комбинация:

  • Услугите за обработка на информация са обособени. Опашките също са разделени. Ако трябва да увеличим пропускателната способност на системата, тогава просто трябва да стартираме повече услуги на повече сървъри.
  • Когато получим информация от потребител, не е нужно да чакаме, докато данните бъдат напълно запазени. Напротив, просто трябва да отговорим „добре“ и след това постепенно да започнем да работим. В същото време опашката изглажда пиковете, тъй като добавянето на нов обект става бързо и потребителят не трябва да чака пълно преминаване през целия цикъл.
  • Като пример добавих услуга за дедупликация, която се опитва да обедини идентични файлове. Ако работи дълго време в 1% от случаите, клиентът едва ли ще го забележи (виж по-горе), което е голям плюс, тъй като вече не се изисква да сме XNUMX% бързи и надеждни.

Недостатъците обаче се виждат веднага:

  • Нашата система е загубила строгата си последователност. Това означава, че ако например се абонирате за различни услуги, тогава теоретично можете да получите различно състояние (тъй като една от услугите може да няма време да получи известие от вътрешната опашка). Друго следствие е, че системата вече няма общо време. Тоест невъзможно е, например, да сортирате всички събития просто по време на пристигане, тъй като часовниците между сървърите може да не са синхронни (още повече, че едно и също време на два сървъра е утопия).
  • Вече никакви събития не могат просто да бъдат върнати назад (както може да се направи с база данни). Вместо това трябва да добавите ново събитие − компенсационно събитие, което ще промени последното състояние на необходимото. Като пример от подобна област: без пренаписване на хронологията (което е лошо в някои случаи), не можете да върнете ангажимент в git, но можете да направите специален ангажимент за връщане назад, което по същество просто връща старото състояние. Въпреки това както грешният ангажимент, така и връщането назад ще останат в историята.
  • Схемата на данните може да се променя от издание на издание, но старите събития вече няма да могат да се актуализират до новия стандарт (тъй като събитията не могат да се променят по принцип).

Както можете да видите, Event Sourcing работи добре с CQRS. Освен това внедряването на система с ефективни и удобни опашки, но без разделяне на потоците от данни, вече е трудно само по себе си, защото ще трябва да добавите точки за синхронизация, които ще неутрализират целия положителен ефект от опашките. Прилагайки и двата подхода наведнъж, е необходимо леко да коригирате програмния код. В нашия случай, при изпращане на файл към сървъра, отговорът идва само „ок“, което означава само, че „операцията по добавяне на файла е запазена“. Формално това не означава, че данните вече са налични на други устройства (например услугата за дедупликация може да възстанови индекса). След известно време обаче клиентът ще получи известие в стила на „файлът X е запазен“.

Като резултат:

  • Броят на статусите за изпращане на файлове се увеличава: вместо класическото „файлът е изпратен“, получаваме две: „файлът е добавен към опашката на сървъра“ и „файлът е записан в хранилището“. Последното означава, че други устройства вече могат да започнат да получават файла (с коригиране на факта, че опашките работят с различни скорости).
  • Поради факта, че информацията за изпращане сега идва по различни канали, трябва да измислим решения, за да получим статуса на обработка на файла. Като следствие от това: за разлика от класическата заявка-отговор, клиентът може да бъде рестартиран, докато обработва файла, но статусът на самата обработка ще бъде правилен. Освен това този артикул работи по същество извън кутията. Като следствие: сега сме по-толерантни към провалите.

Sharding

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

  • Разделете файловете по тип. Например, снимки/видеоклипове могат да бъдат декодирани и може да бъде избран по-ефективен формат.
  • Отделни акаунти по държави. Поради много закони това може да се изисква, но тази архитектурна схема предоставя такава възможност автоматично

Удобни архитектурни модели

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

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

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

По този начин, продължавайки нашия пример за онлайн съхранение на файлове, такава архитектура вече ни дава редица бонуси:

  • Можем да преместваме обекти по-близо до потребителите по динамичен начин. По този начин можете да подобрите качеството на услугата.
  • Може да съхраняваме някои данни в компании. Например корпоративните потребители често изискват техните данни да се съхраняват в контролирани центрове за данни (за да се избегне изтичане на данни). Чрез шардинг можем лесно да поддържаме това. И задачата е още по-лесна, ако клиентът разполага със съвместим облак (напр. Azure се хоства самостоятелно).
  • И най-важното е, че не трябва да правим това. В края на краищата, като начало, бихме били доста доволни от едно хранилище за всички акаунти (за да започнем да работим бързо). И ключовата характеристика на тази система е, че въпреки че е разширяема, в началния етап е доста проста. Просто не е нужно веднага да пишете код, който работи с милион отделни независими опашки и т.н. Ако е необходимо, това може да се направи в бъдеще.

Хостинг на статично съдържание

Тази точка може да изглежда доста очевидна, но все пак е необходима за повече или по-малко стандартно заредено приложение. Същността му е проста: цялото статично съдържание се разпространява не от същия сървър, където се намира приложението, а от специални, посветени специално на тази задача. В резултат на това тези операции се извършват по-бързо (условният nginx обслужва файлове по-бързо и по-евтино от Java сървър). Плюс CDN архитектура (Мрежа за доставяне на съдържание) ни позволява да локализираме файловете си по-близо до крайните потребители, което има положителен ефект върху удобството при работа с услугата.

Най-простият и стандартен пример за статично съдържание е набор от скриптове и изображения за уебсайт. С тях всичко е просто - те са известни предварително, след което архивът се качва на CDN сървъри, откъдето се разпространяват до крайните потребители.

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

  • Сървърът предоставя URL за изтегляне. Може да бъде във формата file_id + key, където key е мини-цифров подпис, който дава право на достъп до ресурса за следващите XNUMX часа.
  • Файлът се разпространява от прост nginx със следните опции:
    • Кеширане на съдържание. Тъй като тази услуга може да бъде разположена на отделен сървър, ние сме си оставили резерв за в бъдеще с възможността да съхраняваме всички най-нови изтеглени файлове на диск.
    • Проверка на ключа по време на създаване на връзка
  • По избор: обработка на поточно съдържание. Например, ако компресираме всички файлове в услугата, тогава можем да направим разархивиране директно в този модул. Като следствие: IO операциите се извършват там, където им е мястото. Архиватор в Java лесно ще разпредели много допълнителна памет, но пренаписването на услуга с бизнес логика в условни условия на Rust/C++ също може да бъде неефективно. В нашия случай се използват различни процеси (или дори услуги) и следователно можем доста ефективно да разделим бизнес логиката и IO операциите.

Удобни архитектурни модели

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

Като друг пример (за подсилване): ако сте работили с Jenkins/TeamCity, тогава знаете, че и двете решения са написани на Java. И двата са процес на Java, който обработва както оркестрацията на изграждането, така и управлението на съдържанието. По-специално, и двамата имат задачи като „прехвърляне на файл/папка от сървъра“. Като пример: издаване на артефакти, прехвърляне на изходен код (когато агентът не изтегля кода директно от хранилището, но сървърът го прави вместо него), достъп до регистрационни файлове. Всички тези задачи се различават по своето IO натоварване. Тоест, оказва се, че сървърът, отговорен за сложната бизнес логика, трябва в същото време да може ефективно да прокарва през себе си големи потоци от данни. И най-интересното е, че такава операция може да бъде делегирана на същия nginx по абсолютно същата схема (с изключение на това, че ключът за данни трябва да бъде добавен към заявката).

Въпреки това, ако се върнем към нашата система, ще получим подобна диаграма:

Удобни архитектурни модели

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

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

Заключение

Всички тези подходи са били известни преди. Същият VK отдавна използва идеята за хостинг на статично съдържание за показване на изображения. Много онлайн игри използват схемата Sharding, за да разделят играчите на региони или да отделят игрови локации (ако самият свят е един). Подходът за източник на събития се използва активно в имейла. Повечето приложения за търговия, където постоянно се получават данни, всъщност са изградени на базата на CQRS подход, за да могат да филтрират получените данни. Е, хоризонталното мащабиране се използва в много услуги от доста дълго време.

Но най-важното е, че всички тези модели са станали много лесни за прилагане в съвременни приложения (ако са подходящи, разбира се). Облаците предлагат шардинг и хоризонтално мащабиране веднага, което е много по-лесно, отколкото сами да поръчате различни специализирани сървъри в различни центрове за данни. CQRS стана много по-лесен дори само поради развитието на библиотеки като RX. Преди около 10 години рядък уебсайт можеше да поддържа това. Event Sourcing също е невероятно лесен за настройка благодарение на готовите контейнери с Apache Kafka. Преди 10 години това би било иновация, сега е нещо обичайно. Същото е и с хостинг на статично съдържание: поради по-удобните технологии (включително факта, че има подробна документация и голяма база данни с отговори), този подход стана още по-прост.

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

И най-важното: моля, не използвайте тези подходи, ако имате просто приложение. Да, красиви и интересни са, но за сайт с пикова посещаемост от 100 души често може да се мине с класически монолит (поне отвън всичко вътре може да се раздели на модули и т.н.).

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

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