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

Напълно осъзнавайки необходимостта и важността от прехода към облачни приложения, много организации все още не знаят откъде да започнат. В тази статия ще разгледаме редица принципи, спазването на които при разработването на контейнерни приложения ще позволи да се реализира потенциалът на облачните платформи и да се постигне надеждно функциониране и мащабиране на приложенията дори при сериозни откази на ниво ИТ инфраструктура. Основната цел на изложените тук принципи е да се научим как да създаваме приложения, които могат да се управляват автоматично от облачни платформи като Kubernetes.
Принципи на проектиране на софтуер
В света на програмирането принципите се разбират като доста общи правила, които трябва да се спазват при разработването на софтуер. Те могат да се прилагат при работа с всеки език за програмиране. Всеки принцип има свои цели, чийто инструмент за постигане обикновено са шаблони и практики. Съществуват и редица основополагающи принципи за създаване на качествен софтуер, от които произтичат всички останали. Ще приведем примери за основополагающи принципи:
- (Keep it simple, stupid) – не усложнявайте;
- (Don’t repeat yourself) – не повтаряйте;
- (You aren’t gonna need it) – не създавайте това, от което нямате непосредствена нужда;
- (Separation of concerns) – разделяйте отговорностите.
Както се вижда, тези принципи не задават конкретни правила, а се отнасят до така наречените съображения на здравия разум на базата на практически опит, с които много разработчици се съгласяват и на които те редовно се позовават.
Освен това, съществува – набор от първите пет принципа на обектно-ориентираното програмиране и проектиране, формулирани от Робърт Мартин. SOLID включва обобщени и открити за интерпретация взаимодопълващи принципи, които – ако се прилагат в комплекс – помагат за изграждането на по-качествени софтуерни системи и за по-добрата им поддръжка в дългосрочен план.
Принципите на SOLID се отнасят до сферата на ООП и се формулират на език от концепции като класове, интерфейси и наследяване. По аналогия, за облачните приложения също могат да се формулират принципи на разработка, само че основният елемент тук ще бъде не клас, а контейнер. Спазвайки тези принципи, могат да се изграждат контейнерни приложения, които по-добре отговарят на целите и задачите на облачни платформи като Kubernetes.
Контейнерно-ориентирани решения: подходът на Red Hat
Днес е сравнително лесно да се опаковат почти всяко приложение в контейнери. Но, за да могат приложенията да бъдат ефективно автоматизирани и оркестрирани в рамките на облачна платформа като Kubernetes, е необходимо да се положат допълнителни усилия.
Основата за изложените по-долу идеи беше методологията и много други трудове по различни аспекти на създаването на уеб приложения, от управление на изходния код до модели на мащабиране. Описаните принципи се отнасят само до разработката на контейнерни приложения, които са изградени на основата на микросервизи и са предназначени за облачни платформи, като Kubernetes. Основният елемент в нашите разисквания е образът на контейнера, а под целевата среда за изпълнение на контейнерите разбираме платформата за оркестрация на контейнери. Целта на предложените принципи е да се създават контейнери, за които на повечето платформи за оркестрация могат да се автоматизират задачи по диспетчизация (scheduling – избор на хост за стартиране на инстанция на контейнера), мащабиране и мониторинг. Принципите се изложени в произволен ред.
Принцип на единствената задача (Single Concern Principle, SCP)
Този принцип до голяма степен наподобява принципа на единствената отговорност (Single Responsibility Principle, ), който влиза в набора SOLID и гласи, че всеки обект трябва да има една отговорност, а тази отговорност трябва да бъде напълно инкапсулирана в класа. Същността на SRP е, че всяка отговорност е причина за промени, а класът трябва да има една и единствена причина за промяна.
В SCP вместо думата «отговорност» (responsibility) използваме думата «задача» (concern), за да укажем на по-високо ниво на абстракция и по-широко предназначение на контейнера в сравнение с ООП-класа. И ако целта на SRP е да има само една причина за промени, то зад SCP стои желанието да се разширят възможностите за повторно използване и замяна на контейнерите. Следвайки SRP и създавайки контейнер, който решава една единствена задача и го прави по функционално завършен начин, вие увеличавате шансовете за повторно използване на образа на този контейнер в различни контексти на приложението.
Принципът SCP гласи, че всеки контейнер трябва да решава една единствена задача и да прави това добре. Освен това, SCP в света на контейнерите се постига по-лесно, отколкото SRP в света на ООП, тъй като контейнерите обикновено изпълняват един единствен процес, и по-голямата част от времето този процес решава една единствена задача.
Ако някой контейнерен микросервис трябва да решава едновременно няколко задачи, можем да го разделим на еднозадачни контейнери и да ги обединим в рамките на един pod (единица за разгръщане на контейнерна платформа) с помощта на шаблони sidecar и init-контейнери. Освен това, SCP улеснява замяната на стар контейнер (например, уеб сървър или брокер на съобщения) с нов, който решава същата задача, но има разширена функционалност или по-добра мащабируемост.

Принципът на висока наблюдаемост (High Observability Principle, HOP)
Когато контейнерите се използват като унифициран метод за опаковане и стартиране на приложения, самите приложения се разглеждат като "черна кутия". Въпреки това, ако това са облачни контейнери, те трябва да предлагат на средата за изпълнение специални API интерфейси, за да контролират целостта на контейнерите и при нужда да предприемат съответните мерки. Без това не е възможно да се унифицира автоматизацията на обновлението на контейнерите и управлението на техния жизнен цикъл, което, от своя страна, ще влоши устойчивостта и удобството на софтуерната система.
На практика контейнерното приложение трябва поне да разполага с API за различни видове проверки на целостта: тестове за активност (liveness) и тестове за готовност (readiness). Ако приложението претендира за повече, то трябва да предлага и други средства за контрол на състоянието си. Например, регистриране на важни събития чрез STDERR и STDOUT за агрегиране на логове с помощта на Fluentd, Logstash и други подобни инструменти. А също така интеграция с библиотеки за проследяване и събиране на метрики, като OpenTracing, Prometheus и т.н.
В общи линии приложението все още може да се разглежда като "черна кутия", но трябва да бъде снабдено с всички API, необходими на платформата за оптимално мониториране и управление.
Принцип на съответствие с жизнения цикъл (Life-cycle Conformance Principle, LCP)
LCP е антитеза на HOP. Докато HOP заявява, че контейнерът трябва да предоставя на платформата API интерфейси за четене, LCP изисква от приложението способността да възприема информация от платформата. Контейнерът трябва не само да получава събития, но и да се приспособява, т.е. да реагира на тях. Затова принципът може да се разглежда като изискване за предоставяне на API интерфейси за запис на платформата.

Платформите разполагат с различни видове събития, които помагат за управлението на жизнения цикъл на контейнера. Но решението кои от тях да се възприемат и как да се реагира, трябва да бъде на самото приложение.
Очевидно, что некоторые события важнее других. Например, если приложението не е устойчиво на неочаквано прекратяване на работа, то то трябва да приема сигнали signal: terminate (SIGTERM) и възможно най-бързо да инициира процеса си на завършване, за да успее преди получаването на сигнала signal: kill (SIGKILL), който идва след SIGTERM.
Освен това, за жизнения цикъл на приложението могат да бъдат важни събития като PostStart и PreStop. Например, след стартиране на приложението може да е необходимо определено време за "подгряване", преди то да може да отговаря на заявки. Или приложението трябва да освобождава ресурси по специфичен начин при завършване.
Принципът на неизменност на контейнерния образ (Image Immutability Principle, IIP)
Общоприето е, че контейнерните приложения трябва да остават неизменни след изграждане, дори когато се стартират в различни среди. Оттук произтича необходимостта от екстернализиране на съхранението на данни в изпълнителната фаза (с други думи, да се използват външни средства за това), както и да се разчита на външни, конфигурирани за конкретна изпълнителна среда конфигурации, вместо да се модифицират или създават уникални контейнери за всяка среда. След всякакви промени в приложението, контейнерният образ трябва да се преизгради и внедри във всички използвани среди. Между другото, при управлението на ИТ системи се използва подобен принцип, известен като принципа на неизменността на сървърите и инфраструктурата.
Целта на IIP е да предотврати създаването на отделни контейнерни образи за различни изпълнителни среди и да се използва един и същ образ навсякъде с подходящата конфигурация за конкретната среда. Спазването на този принцип позволява реализирането на важни за автоматизацията на облачните системи практики, като откат (roll-back) и накат (roll-forward) на обновления на приложението.

Принципът на еднократност на процесите (Process Disposability Principle, PDP)
Едно от най-важните характеристики на контейнера е неговата ефемерност: инстанция на контейнера лесно може да бъде създадена и лесно унищожена, така че по всяко време може лесно да бъде заменена с друга инстанция. Причините за такава замяна могат да бъдат много: провал на теста за работоспособност, мащабиране на приложението, прехвърляне на друг хост, изчерпване на ресурсите на платформата или други ситуации.
Следствие от това е, че контейнерните приложения трябва да задържат своето състояние с помощта на външни средства или да използват вътрешни разпределени схеми с излишна надеждност. Освен това, приложението трябва да стартира бързо и да се затваря бързо, както и да бъде готово за внезапен фатален отказ на оборудването.
Една от практиките, която помага за реализирането на този принцип, е да се създават контейнери с малък размер. Облачните среди могат автоматично да избират хост за стартиране на контейнера, така че колкото по-малък е размерът на контейнера, толкова по-бързо ще се стартира – той просто ще бъде копиран по-бързо към целевия хост през мрежата.
Принцип на самообхватността (Self-containment Principle, S-CP)
Според този принцип, по време на изграждането всички необходими компоненти трябва да бъдат включени в контейнера. Контейнерът трябва да бъде проектиран с предположението, че в системата съществува само чисто ядро на Linux, затова всички нужни допълнителни библиотеки трябва да бъдат поставени вътре в самия контейнер. Там също така трябва да се намират неща като среда за изпълнение за съответния език за програмиране, платформа за приложения (при нужда) и други зависимости, които ще са необходими по време на работата на контейнерното приложение.

Изключения се правят единствено за конфигурации, които варират от среда на среда, и трябва да бъдат предоставени по време на изпълнение, например чрез Kubernetes ConfigMap.
Приложението може да включва няколко контейнеризирани компонента, например отделен контейнер за база данни в рамките на контейнерно уеб приложение. Според принципа S-CP, тези контейнери не трябва да се обединяват в един, а да се уверим, че контейнерът за база данни съдържа всичко необходимо за работа на базата данни, а контейнерът за уеб приложението съдържа всичко необходимо за работа на уеб приложението, например уеб сървър. В резултат, по време на изпълнение, контейнерът за уеб приложението ще зависи от контейнера за база данни и ще се свързва с него при необходимост.
Принцип на ограниченията по време на изпълнение (Runtime Confinement Principle, RCP)
Принцип S-CP определя нов как трябва да се изгражда контейнерът и какво трябва да съдържа двоичният файл на образа. Но контейнерът не е просто „черен кутия“, който има само една характеристика – размер на файла. По време на изпълнението контейнерът придобива и други измерения: обем на използваната памет, процесорно време и други системни ресурси.

И тук влиза принципът RCP, според който контейнерът трябва да декомпозира своите изисквания за системни ресурси и да ги предаде на платформата. Имайки ресурсни профили на всеки контейнер (колко ресурси ЦП, памет, мрежа и дискове са необходими), платформата може оптимално да осъществява разпределение и автоматично мащабиране, да управлява ИТ-ресурсите и да поддържа нива на SLA за контейнерите.
Освен да изпълнява изискванията за ресурси на контейнера, е важно приложението също да не надхвърля зададените от него рамки. В противен случай, при недостиг на ресурси, платформата с по-голяма вероятност ще го включи в списъка с приложения, които да бъдат прекратени или мигрирани.
Говорейки за облачна ориентация, имаме предвид преди всичко начина на работа.
По-горе формулирахме редица общи принципи, които задават методологичната основа за изграждането на качествени контейнерни приложения за облачни среди.
Следва да се отбележи, че освен тези общи принципи, ще са ви нужни и допълнителни напреднали методи и техники за работа с контейнери. Освен това имаме няколко кратки препоръки, които имат по-конкретен характер и трябва да се прилагат (или не) в зависимост от ситуацията:
- Стремете се да намалявате размерите на образите: изтривайте временни файлове и не инсталирайте ненужни пакети – колкото по-малък е размерът на контейнера, толкова по-бързо се изгражда и копира на целевия хост по мрежата.
- Ориентирайте се към произволни потребителски идентификатори: не използвайте командата sudo или специални идентификатори за стартиране на контейнерите си.
- Маркирайте важни портове: номера на портовете могат да се задават и по време на изпълнението, но е по-добре да се укажат с помощта на командата EXPOSE – на другите потребители и програми ще им бъде по-лесно да използват вашите образи.
- Съхранявайте постоянни данни на томове: данните, които трябва да останат след унищожаване на контейнера, трябва да бъдат записани на томовете.
- Определяйте метаданни на образа: етикетите, таговете и анотациите улесняват използването на образите – другите разработчици ще ви бъдат благодарни.
- Синхронизирайте хоста и образите: за някои контейнерни приложения е необходима синхронизация на контейнера с хоста по определени атрибути, като време или идентификатор на машината.
- В заключение споделяме шаблони и най-добри практики, които ще помогнат за по-ефективното прилагане на горепосочените принципи:
11 юни в 11:00
Какво ще научите:
- Непроменлив Red Hat Enterprise Linux CoreOS
- OpenShift service mesh
- Operator framework
- Knative framework
Източник: habr.com
