Здравейте, Хабр!
В контекста на пазарите заради коронавируса, редица интернет услуги започнаха да получават увеличен натиск. Например, , тъй като нямаше достатъчно ресурси. И не винаги е възможно да се ускори сървъра, просто добавяйки по-мощно оборудване, но запитванията на клиентите трябва да се обработват (иначе те ще се обърнат към конкуренцията).
В тази статия накратко ще представя популярни практики, които ще позволят да се създаде бърза и надеждна услуга. Въпреки това, от възможните схеми за разработка, избрах само тези, които в момента са лесно достъпни. За всяка точка имате или готови библиотеки, или възможност да решите задачата с помощта на облачна платформа.
Хоризонтално мащабиране
Най-простата и известна точка. Условно, най-често срещаните две схеми за разпределение на натоварването са хоризонтално и вертикално мащабиране. вие позволявате на услугите да работят паралелно, като по този начин разпределяте натоварването между тях. включва поръчка на по-мощни сървъри или оптимизация на кода.
За пример ще взема абстрактно облачно хранилище на файлове, тоест аналог на OwnCloud, OneDrive и т.н.
Стандартната схема на подобна структура е по-долу, но тя само демонстрира сложността на системата. Защото по някакъв начин трябва да синхронизираме услугите. Какво ще се случи, ако потребител запази файл от таблета, а след това иска да го види на телефона?

Разликата между подходите: при вертикалното мащабиране сме готови да увеличим мощността на възлите, а при хоризонталното — да добавим нови възли, за да разпределим натоварването.
CQRS
е доста важен шаблон, тъй като позволява на различни клиенти не само да се свързват с различни услуги, но и също така да получават идентични потоци от събития. Неговите предимства не са толкова очевидни за просто приложение, но той е изключително важен (и прост) за натоварена услуга. Същността му е, че входящите и изходящите потоци данни не трябва да се пресичат. Тоест, не можете да изпратите запитване и да очаквате отговор; вместо това изпращате запитване до услуга A, но получавате отговор в услуга Б.
Първият бонус на този подход е възможността за прекъсване на връзката (в широк смисъл на думата) по време на изпълнение на дълга заявка. За пример, нека вземем една по-или по-малко стандартна последователност:
- Клиентът изпрати заявка до сървъра.
- Сървърът стартира дълга обработка.
- Сървърът отговори на клиента с резултата.
Да си представим, че в точка 2 е настъпило прекъсване на връзката (или мрежата се е пренастроила, или потребителят е преминал на друга страница, прекъсвайки връзката). В този случай на сървъра ще му бъде трудно да изпрати отговор на потребителя с информация какво точно е обработено. Приложението на CQRS ще предизвика нова последователност:
- Клиентът се абонира за актуализации.
- Клиентът изпрати заявка до сървъра.
- Сървърът отговори "заявката е приета".
- Сървърът отговори с резултата през канала от точка "1".

Както се вижда, схемата е малко по-сложна. Повече от това, интуитивният подход request-response тук липсва. Въпреки това, както се вижда, прекъсването на връзката по време на обработката на заявката няма да доведе до грешка. Освен това, ако потребителят наистина е свързан с услугата от няколко устройства (например, от мобилен телефон и таблет), може да се направи така, че отговорът да стигне до двете устройства.
Интересното е, че кодът за обработка на входящите съобщения става идентичен (не на 100%) както за събитията, на които е повлиял самият клиент, така и за другите събития, включително от други клиенти.
Въпреки това, в реалността получаваме допълнителни бонуси, свързани с факта, че еднопосочният поток може да се обработва по функционален начин (използвайки RX и аналози). И тук вече става сериозен плюс, тъй като по същество приложението може да се направи напълно реактивно, с прилагане на функционален подход. За сложни приложения това може значително да спести ресурси за разработка и поддръжка.
Ако комбинираме този подход с хоризонтално мащабиране, бонусът е възможността да изпращаме заявки до един сървър, а да получаваме отговори от друг. По този начин клиентът може сам да избере удобната за него услуга, а системата вътре все пак ще може да обработи събитията коректно.
Event Sourcing
Както знаете, едно от основните свойства на разпределената система е отсъствието на общо време и обща критична секция. За един процес можете да направите синхронизация (на същите мютекси), в която сте сигурни, че никой друг не изпълнява този код. Но за разпределена система подобно е опасно, тъй като това ще изисква разходи, а също така ще унищожи цялото удоволствие от мащабирането — все пак всички компоненти ще чакат един и същ.
Оттук получаваме важен факт — бърза разпределена система не може да бъде синхронизирана, защото тогава ще намалим производителността. От една страна, често ни е нужно определено съответствие на компонентите. И за това може да се използва подход с , където се гарантира, че при отсъствие на промени в данните след определен период от време след последното обновление („в крайна сметка“) всички заявки ще връщат последната обновена стойност.
Важно е да разберете, че за класическите бази данни често се прилага , при която всеки възел притежава еднаква информация (такова често се постига, когато транзакцията се счита за установена само след отговор от втория сървър). Тук има някои отслабвания поради нивата на изолация, но основната същност остава същата — можете да живеете в напълно съвместим свят.
Но да се върнем към първоначалната задача. Ако част от системата може да бъде изградена с , то може да се изработи следната схема.

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

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

Бонуси от подобна комбинация:
- Услугите за обработка на информация са разделени. Опашките също са разделени. Ако ни е необходимо да увеличим пропускателната способност на системата, трябва просто да стартираме повече услуги на по-голям брой сървъри.
- Когато получаваме информация от потребителя, не е необходимо да изчакваме пълното записване на данните. Обратно, достатъчно е да отговорим с "ок", а след това постепенно да започнем работа. В същото време опашката изглажда пиковете, тъй като добавянето на нов обект става бързо, а потребителят не трябва да чака за цялото преминаване през цикъла.
- За пример добавих услуга за дедубликация, която се опитва да обединява идентични файлове. Ако тя работи дълго време в 1% от случаите, клиентът практически няма да забележи това (вж. по-горе), което е голям плюс, тъй като от нас вече не се изисква стотинна скорост и надеждност.
Но веднага се виждат и минусите:
- Нашата система е загубила строгата си последователност. Това означава, че, например, ако се абонирате за различни услуги, теоретично можете да получите различно състояние (тъй като една от услугите може да не успее да приеме уведомлението от вътрешната опашка). Като друго следствие, системата вече няма общо време. Тоест не може, например, да се сортират всички събития просто по времето на достигане, тъй като часовниците между сървърите може да не са синхронизирани (по-важно е, че едно и също време на два сървъра е утопия).
- Никаки събития не могат сега просто да бъдат отменени (както би могло да се направи с база данни). Вместо това е необходимо да се добави ново събитие — , което ще променя последното състояние на необходимото. Като пример от подобна област: без презапис на историята (което е лошо в редица случаи) в git не може да се отмени комит, но може да се направи специален , който по същество просто ще възстанови старото състояние. Въпреки това в историята ще остане и грешният комит, и rollback.
- Системата от данни може да се променя от версия на версия, но старите събития вече не могат да бъдат актуализирани по новия стандарт (тъй като събитията по принцип не могат да се променят).
Както се вижда, Event Sourcing отлично се комбинира с CQRS. По-важното е, че реализирането на система с ефективни и удобни опашки, но без разделяне на потоците данни, вече само по себе си е сложно, тъй като ще трябва да добавим точки за синхронизация, които ще неутрализират целия положителен ефект от опашките. Приложението на двата подхода едновременно изисква незначителни корекции в кода на програмата. В нашия случай, при изпращане на файл на сървъра, в отговора получаваме само „ок“, което означава само, че „операцията за добавяне на файл е запазена“. Формално, това не означава, че данните вече са достъпни на другите устройства (например, услугата за дедупликация може да пренарежда индекса). Въпреки това след известно време клиентът ще получи уведомление в стил „файл Х е запазен“.
В резултат на това:
- Броят на статусите на изпращане на файлове се увеличава: вместо класическото „файлът е изпратен“ получаваме два: „файлът е добавен в опашката на сървъра“ и „файлът е запазен в хранилището“. Последното означава, че другите устройства вече могат да започнат да получават файла (с поправка, че опашките работят с различна скорост).
- Поради факта, че информацията за изпращането сега идва по различни канали, е необходимо да измислим решения, за да получим статус на обработката на файла. Следователно, за разлика от класическия request-response, клиентът може да бъде перезапуснат по време на обработката на файла, но статусът на самата обработка ще бъде коректен. Всъщност тази точка работи, по същество, от кутията. В резултат: сега сме по-толерантни към отказите.
Sharding
Както вече беше описано по-горе, в системи с event sourcing няма строга последователност. Това означава, че можем да използваме няколко хранилища без никаква синхронизация между тях. Приближавайки се до нашата задача, можем:
- Да разделяме файловете по типове. Например, изображенията/видеота могат да бъдат декодирани и да се избере по-ефективен формат.
- Да разделяме акаунтите по страни. Поради много закони, подобно може да бъде необходимо, но тази архитектурна схема дава такава възможност автоматично.

Ако искате да прехвърлите данни от едно хранилище в друго, стандартните средства вече не са достатъчни. За съжаление, в такъв случай е необходимо да спрем опашката, да направим миграцията, а след това да я стартираме отново. Обикновено данни не могат да бъдат прехвърлени 'на лето', но ако опашката на събитията се съхранява напълно и имате копия на предишните състояния на хранилището, можем да репродуцираме събитията по следния начин:
- В Event Source всяко събитие има свой идентификатор (в идеалния случай — не намаляващ). Значи в хранилището можем да добавим поле — id на последния обработен елемент.
- Дублираме опашката, така че всички събития да могат да се обработват за няколко независими хранилища (първото — е това, в което в момента се съхраняват данните, а второто — новото, но все още празно). Втората опашка, разбира се, в момента не се обработва.
- Стартираме втората опашка (тоест започваме репродуцирането на събитията).
- Когато новата опашка стане относително празна (т.е. средната разлика във времето между добавянето на елемент и неговото извличане е приемлива), можем да започнем да преминаваме читателите към новото хранилище.
Както се вижда, в нашата система никога не е имало и няма строга съгласуваност. Има само eventual consistency, тоест гаранция, че събитията се обработват в еднакъв ред (обаче, може да има различна забавяне). И, ползвайки това, можем сравнително лесно да прехвърлим данни без спиране на системата на другия край на света.
Следователно, продължавайки нашия пример за онлайн хранилище за файлове, подобна архитектура вече ни предоставя редица бонуси:
- Можем да местим обекти по-близо до потребителите, с динамичен подход. Така можем да повишим качеството на услугата.
- Можем да съхраняваме част от данните в рамките на компаниите. Например, Enterprise потребителите често изискват данните им да се съхраняват в контролирани дата центрове (за избягване на изтичане на данни). Благодарение на sharding можем лесно да го поддържаме. И задачата става още по-проста, ако клиента има съвместимо облако (например, ).
- Но най-важното е, че не е нужно да го правим. За начало, изцяло ще ни е достатъчно едно хранилище за всички акаунти (за да можем да започнем работа по-бързо). А ключовата характеристика на тази система — въпреки че е разширяема, в началния етап е достатъчно проста. Просто не е необходимо веднага да пишем код, който работи с милион отделни независими опашки и т.н. Ако е нужно, можем да го направим в бъдеще.
Статично съдържание
Точката може да изглежда очевидна, но все пак е необходима за по-повече или по-малко стандартно натоварено приложение. Основната идея е проста: цялото статично съдържание не се раздава от същия сървър, където е приложението, а от специални, предназначени именно за тази цел сървъри. В следствие на това, тези операции се извършват по-бързо (условният nginx предоставя файлове по-бързо и по-евтино от Java сървър). Плюс архитектурата на CDN () позволява да разположим нашите файлове по-близо до крайните потребители, което положително влияе на удобството на работа със сервиза.
Най-простият и стандартен пример за статично съдържание е набор от скриптове и изображения за сайта. С тях всичко е просто — те са известни предварително, след което архивът се качва на сървърите на CDN, откъдето се раздава на крайните потребители.
Но на практика, за статично съдържание може да се приложи подход, по подобие на архитектурата на Lambda. Нека се върнем на нашата задача (онлайн хранилище на файлове), за която е необходимо да раздадем файлове на потребителите. Най-простото решение в прав текст — да направим услуга, която за всяка заявка на потребителя извършва всички необходими проверки (авторизация и т.н.), а след това изтегля файла директно от нашето хранилище. Главният недостатък на такъв подход е, че статичното съдържание (което файловете с определена версия по същество са статично съдържание) се раздава от същия сървър, който съдържа бизнес логиката. Вместо това, можем да направим следната схема:
- Сървърът издава URL за изтегляне. Той може да бъде от вида file_id + key, където key — мини цифрова подпис, даваща право за достъп до ресурса за следващите 24 часа.
- Разпределението на файла се занимава с прост nginx с следните опции:
- Кеширане на съдържанието. Тъй като този сервис може да бъде на отделен сървър, оставихме си резерв за бъдещето с възможност да съхраняваме всички последно изтеглени файлове на диска.
- Проверка на ключа при създаване на връзка.
- Опционално: поточно предаване на съдържанието. Например, ако компресираме всички файлове в сервиза, можем да извършим разархивиране директно в този модул. В резултат: IO операциите се извършват там, където им е най-место. Архиваторът на Java лесно би заделил много излишна памет, но пренаписването на сервиза с бизнес логика на условни Rust/C++ също може да се окаже неефективно. В нашия случай се използват различни процеси (или дори услуги), затова можем доста ефективно да разделим бизнес логиката и IO операциите.

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

Както изглежда, системата стана радикално сложна. Сега това не е просто мини-процес, който съхранява файлове локално. Сега се изисква не само поддръжка, ами и контрол на версиите на API и т.н. Затова след като всички диаграми са нарисувани, най-добре е да оцените детайлно дали разширяемостта струва такива разходи. Въпреки това, ако искате да имате възможност за разширяване на системата (включително за работа с още повече потребители), ще трябва да опитате подобни решения. Въпреки това, в резултат, архитектурата на системата е готова за увеличаване на натоварването (практически всеки компонент може да бъде клониран за хоризонтално мащабиране). Системата може да бъде обновявана без да се спира (просто някои операции ще забавят леко).
Както вече споменах в самото начало, сега редица интернет услуги започнаха да получават увеличено натоварване. И някои от тях просто започнаха да престават да работят коректно. По същество системите отказаха точно в момента, в който бизнесът трябваше да генерира приходи. Тоест вместо отложена доставка, вместо предложение на клиентите „планирайте доставката за следващите месеци“, системата просто каза „идете при конкурентите“. Всъщност, това е цената на ниската производителност: загубите ще настъпят точно тогава, когато печалбата би била най-висока.
Заключение
Всички тези подходи бяха известни и по-рано. Същият VK отдавна използва идеята за Хостинг на Статично Съдържание за предоставяне на изображения. Много онлайн игри използват схемата Sharding за разделяне на играчите по региони или за разделяне на игрови локации (ако светът е един). Подходът Event Sourcing активно се използва в електронната поща. Повечето приложения на търговците, където непрекъснато постъпват данни, всъщност са изградени на подхода CQRS, за да могат да филтрират получените данни. А хоризонталното мащабиране съществува от доста време в много услуги.
Но най-важното е, че всички тези модели стана много лесно да се прилагат в съвременните приложения (разбира се, ако са уместни). Облакът предлага Sharding и хоризонтално скалиране веднага, което е много по-лесно в сравнение с поръчването на различни отделни сървъри в различни центрове за данни. CQRS стана много по-лесно поради развитието на библиотеки като RX. Преди 10 години рядък уеб сайт можеше да поддържа нещо такова. Event Sourcing също се настройва невероятно лесно благодарение на готовите контейнери с Apache Kafka. Преди 10 години това щеше да е иновация, а сега е обичайно. Подобно е и със Static Content Hosting: благодарение на по-удобни технологии (включително защото има подробна документация и голяма база от отговори), подобен подход стана още по-прост.
В заключение, реализацията на редица доста сложни архитектурни модели сега стана много по-лесна, следователно си струва да им се обърне внимание предварително. Ако в десетгодишно приложение е било отказано от едно от решенията по-горе поради висока цена за внедряване и експлоатация, то сега в ново приложение или след рефакториране, може да бъде създадено обслужване, което архитектурно ще бъде както разширяемо (от гледна точка на производителността), така и готово за новите изисквания от клиентите (например, за локализация на лични данни).
И най-важното: моля, не използвайте тези подходи, ако имате просто приложение. Да, те са красиви и интересни, но за сайт с пикова посещаемост от 100 души често може да се справите с класически монолит (поне отвън, отвътре всичко може да се разбие на модули и т.н.).
Източник: habr.com
