Continuăm cu partea a doua a acestui articol despre istoria cloudului.
Scalabilitatea verticală era limitată, în mare parte, la capabilitățile aplicațiilor care rulau pe acel server. Creșterea performanțelor la nivelul unei singure aplicații începea să coste din ce în ce mai mult, pentru rezultate modeste (o creștere de 1-2%). Era timpul pentru o schimbare de concepție. Fiind deja folosit, modelul Horizontally Scaling Compute [29] promitea o creștere mult mai mare a performanțelor, ca sistem și nu ca aplicație singulară. Astfel a început migrarea în masa de la arhitecturi gen monolit la microservicii. Acest lucru era deja posibil și înainte, prin event-driven development realizat de obicei cu RabbitMQ cluster drept coadă de mesaje (uneori implementat ca remote procedure call, în cazuri mai complicate ca și CQRS - vezi Queue-Centric Workflow [29]).
De asemenea, se observă trecerea cât mai mare de la centru propriu de date la cloud public.
Este o aplicație monolit, după cum sugerează și numele, care conține toate piesele într-un singur loc/proces (totul sau nimic).
Conform [17] o astfel de soluție oferă beneficii:
Este ușor de implementat - număr limitat de limbaje de programare (toate cu o versiune fixă), aceleași IDE, un singur codebase .
Presupune o testare ușoară - testarea automată însemna doar pornirea aplicației și rularea testelor (de exemplu, Selenium).
Se instalează ușor - un singur artefact , war sau structură de directoare.
Din păcate, o astfel de arhitectură este statică și nu poate evolua independent. Nu este posibil să se folosească o versiune mai nouă sau un cu totul alt limbaj de programare doar pentru un anumit layer/zona din cod [vezi 22 pentru Layered Software Architecture]. De exemplu:
folosirea Spring Security (Java) într-un proiect Python/Node.Js doar pentru autorizare
Cu cât aplicația devine mai mare, mentenanța devine mai greoaie deoarece este din ce în ce mai mult cod de analizat, schimbat, compilat și împachetat. Un singur defect într-un modul poate afecta alte zone (de exemplu, un memory leak într-un modul care este folosit rar, poate afecta toată aplicația).
Ideea din spatele microserviciilor este de a crea un serviciu prin agregarea mai multor servicii independente, fără stare (de preferat) și predictibile (același input, același output), care lucrează împreună.
Fiecare serviciu are propriile responsabilități, care, fiind independente, pot fi implementate, testate și instalate în mod independent.
Echipele care se ocupă de ele pot lucra, de asemenea, independent, o dată ce au convenit asupra interfețelor și modului de versionare - de exemplu, pentru REST[24]:
uri - de exemplu myapi/v1;
accept header - de exemplu application/vnd.company.myapp.customer-v3+json ;
O astfel de arhitectură necesită coordonare atât între echipele care se ocupă de implementare, testare și instalare/release, dar și între microservicii (de exemplu, pentru scalare dinamică).
Folosind modelul Strangler, o aplicație monolit poate fi transformată într-o aplicație bazată pe microservicii, fără rescriere completă (în cazul aplicațiilor mari ar fi imposibilă din motive de business), rescriind ca microserviciu funcționalitățile noi. În timp, aplicația veche va avea din ce în ce mai puțin cod, iar microserviciile nou scrise vor prelua toată funcționalitatea aplicației vechi.
Acest procedeu seamănă cu transformarea codului legacy fără teste, în cod rescris și testat [vezi 26].
Deoarece în arhitectura monolit toate obiectele erau disponibile în memorie (același proces), viteza lor de obținere era foarte mare. Într-o arhitectură bazată pe microservicii, localizarea serviciilor care comunică des este foarte importantă pentru că apar întârzieri cauzate și de rețea.
Această problemă a fost rezolvată prin Colocate Pattern [29] - microserviciile care comunică des sunt localizate cât mai aproape unul de altul.
Dat fiind că microserviciile sunt separate, este greu, uneori, de observat care serviciu și/sau utilizator a făcut o anumită acțiune și când. Pentru debugging/tracing/monitoring se folosesc sisteme de tracing distribuite ( de exemplu, Zipkin) sau unelte APM ( precum, Raygun. Iar pentru auditul datelor se folosesc implementări ale Event Sourcing Pattern (de exemplu, AWS CloudTrail) - salvarea schimbărilor suferite de date și inferarea valorii finale din acele schimbări. De exemplu , pentru un cont în bancă se salvează operațiunile efectuate asupra contului cum ar fi retragere de bani, iar balanța curentă se calculează pe baza acestor operațiuni. De asemenea, este importantă persoana care a efectuat operațiunea și când a fost făcută operațiunea. Acest patern este, de obicei, folosit de CQRS și poate fi implementat cu Kafka Streams (out-of-the-box fault-tolerance, scalabilitate și availibilitate [35]).
Pentru a minimiza costurile și a avea capacitate de calcul la cerere, este nevoie de scalare dinamică. Aceasta se poate obține aplicând Registry Pattern la nivel de microservicii [10] - utilizat de către alte obiecte/servicii pentru a găsi obiecte/servicii. Este folosit de către load balancer pentru a scala în mod dinamic un serviciu - noul serviciu pornit automat se înregistrează în registrul load balancer-ul, ca în cazul Netflix Eureka [36].
Cu cât sunt mai multe microservicii, cu atât șansele ca unul dintre ele să nu funcționeze cresc. Se dorește a avea un sistem care să continue să funcționeze, uneori chiar parțial. CircuitBreaker [13] propune o rezolvare a acestei probleme înlocuind rezultatul de la serviciul care nu funcționează cu valori implicite. De exemplu, dacă serviciul cu filmele favorite nu funcționează, în locul lor se prezintă clientului o colecție fixă (aceeași pentru toți utilizatori), de filme preferate, de multe ori clienții nesesizând diferența.
Distribuirea microserviciilor/serverelor și nevoia de performanță a aplicațiilor a dus la apariția folosirii cachingului pentru date/cereri și fișier. Cache-Aside Pattern [11] propune o rezolvare prin obținerea datelor din baza de date doar după verificarea propriului cache (implementările de ORM au de obicei mai multe nivele de caching) și CDN Pattern - caching distribuit cum ar fi cloudflare.com pentru fișiere rezolvă această problemă.
A/B testing (utilizatorul A, ales arbitrar, primește un set de funcționalități, utilizatorul B un alt set) și canary releases (funcționalități pentru grupe de utilizatori/ zone geografice) au apărut din necesitatea de a obține feedback cât mai repede de la utilizatori asupra noilor funcționalități. De asemenea, detectează și dacă vechile funcționalități merită păstrate.
Feature Toggles Pattern [14] rezolvă această problemă permițând schimbarea funcționalității codului fără schimbări de cod, prin comutarea on/off a unor felii verticale (complete) de funcționalități. De exemplu, pentru o aplicație gen cumpărare/vânzare de acțiuni căreia i se adaugă în backend și frontend , suport pentru predicția de preț a acțiunilor: scoaterea acestei funcționalități trebuie să se poată face dintr-un singur loc (cel mai probabil din baza de date).
Acest tip de patern poate modifica inclusiv modul de lucru cu sistemele revision control gen Git, deoarece pe lângă modurile de lucru trunk based development sau gitflow [15], se poate lucra direct în trunk, release-ul însemnând doar comutarea funcționalității noi (până nu este gata, funcționalitatea rămâne oprită) [37].
În această perioadă a început migrarea către cloudul public a aplicațiilor vechi. Apare necesitatea definirii a ceea ce înseamnă o aplicație cloud, așa că în 2011 cei de la Heroku au definit o metodologie pentru crearea de aplicații SaaS (cloud native) care se numește Twelve-Factor App.
O aplicație care respectă următoarele puncte nu va avea probleme în a rula în cloud [30][39]:
Codebase - folosirea unei aplicații de version control gen Git unde fiecare aplicație are propriul repository.
Dependențe - declarate explicit și izolate (aplicația trebuie să le conțină și să nu intre în conflict cu cele instalate pe serverul unde se face instalarea).
Configurația - aplicația și partea de configurare să fie independente.
Backing services - fiecare serviciu auxiliar (gen RabbitMQ, Memcached) să poată fi ușor schimbat cu un altul (gen ZeroMQ, Redis) prin configurare.
Build, release, run - fiecare etapă să fie separată de cealaltă (pot fi combinate, de exemplu, un singur build, release și instalare pentru test și stage).
Procese - procesele trebuie să fie fără stare (stateless) astfel încât rutarea în load balancer să nu conteze către care server fizic este executată; starea este persistată în baza de date.
Port binding - portul la care ascultă serviciul trebuie să poată fi modificabil (clienții să poată să îl modifice).
Concurența - să poată fi executate în paralel (scalabilitate).
Disposability - aplicația să se poată opri oricând; la oprire, trebuie să-și curețe resursele și să termine de servit cererile preluate (graceful shutdown).
Paritate Dev/prod - mediile de development, staging și producție să fie cât mai asemănătoare; orice schimbare de configurare OS să poată fi aplicată cu încredere în oricare alt mediu, iar orice eroare să se reproducă în celelalte medii.
Loguri - tratate ca event stream și nu ca fișiere disparate; se pot folosi unelte ca ELK, Hadoop, Splunk, AWS CloudWatch.
Conform [34][40], aplicațiile cloud-native au următoarele atribute:
predictibile - construite conform unui contract (12-factor);
abstractizează OS - programatorii se axează pe cod și nu pe infrastructură;
capacitate dinamică - alocarea dinamică a resurselor, scalabilitate, orchestrarea serviciilor;
colaborare - facilitează DevOps ("You build it, you run it");
continuous delivery - release-uri mici făcute cât mai des (răspuns rapid la necesitățile clienților și feedback rapid din partea lor);
servicii independente - folosesc cele mai bune unelte pentru ceea ce trebuie făcut și pot să se dezvolte independent;
scalare automată - se elimină erorile umane;
recuperare rapidă - orchestrarea automată oferă atât scalare automată cât și recuperare/repornire în cazul unor erori de aplicație/microserviciu sau infrastructură;
Conform [28][45], se identifică următoarele modele:
Cloud Native Databases Per Component - fiecare componentă cu propria bază de date.
Event Streaming - comunicare asincronă bazată pe mesaje dintr-un domeniu a căror prelucrare este delegată consumatorilor de mesaje din acel domeniu.
Event Sourcing - persistarea stărilor trecute într-un mod atomic și imutabil și extragerea stării curente prin prelucrarea acestui șir de evenimente.
Data Lake - colectarea, stocarea și indexarea tuturor evenimentelor în format raw și suport pentru audit, reluare (replay) și analitice.
Stream Circuit Breaker - tratarea cazurilor de eroare în prelucrarea evenimentelor.
Trilateral API - mai multe interfețe pentru aceeași componentă (de exemplu, versiune sincronă și asincronă).
API Gateway - barieră la marginile unui sistem cloud native* folosită pentru validări (de exemplu, verificări de securitate) sau optimizări (de exemplu, caching), înainte de a intra în sistem cererea clientului.
Command Query Responsibility Segregation - separarea scrierii stării față de citire stării.
Offline-First Database - persistarea datelor utilizatorului local și sincronizarea cu cloudul prin evenimente (când sursa este clientul) și view-uri materializate (când schimbările au ca sursă baza de date).
Backend For Frontend - fiecare client are propriul backend în loc de un backend pentru toate aplicațiile client (mobile Android, mobile iOS, web, desktop).
External Service Gateway - integrarea sistemelor externe are propriile riscuri astfel încât External Service Gateway face legătura cu exteriorul, oferind un nou layer de securitate.
Event Collaboration - mai multe sisteme lucrează împreună, comunicând între ele când apare o schimbare/eveniment.
Event Orchestration - folosirea unui mediator care orchestrează comunicarea între componente.
Saga - implementează anularea unei tranzacții la nivel sistem de microservicii, atunci când unul dintre microservicii nu reușește să comită starea nouă, folosindu-se de evenimentele generate de acea tranzacție și anulându-le una câte una.
Bulkhead - izolarea cererilor (toleranță la erori).
Sidecar - decorator pentru un serviciu existent, extinzându-i funcționalitățile.
Throttling - limitarea cererilor pentru o perioadă de timp fie prin respingerea noilor cereri, oprirea temporară a unei funcționalități sau delegarea către servicii cu prioritate mai mică.
Sharding - partiționarea pe orizontală a datelor (aceeași schemă, date diferite).
Federated Identity - delegarea autentificării către un serviciu extern.
Service mesh poate părea o generalizare a API Gateway în sensul că oferă servicii asemănătoare (criptare, autentificare, autorizare, load balancing, rutare, monitorizare, analitice și tracing), doar că face aceasta la un alt nivel. Service mesh funcționează la comunicarea între servicii, pe când API gateway la comunicarea dintre client și servicii.
Pe lângă aceste servicii, mai oferă descoperirea serviciilor și suport pentru circuit breaker. Reușește să implementeze aceste funcționalități folosind un proxy pentru fiecare serviciu, numit sidecar. Acest serviciu poate fi configurat independent de serviciu propriu-zis.
Printre furnizorii de soluții service mesh se află Istio, Lyft, Linkerd și Hashicorp Consul.
NATS este un sistem de mesaje cloud native, IoT și arhitectura microservicii (bus de mesaje). Suportă publish/subscribe, competing consumers și request /reply , iar mesajele sunt text [41].
Exemplu de mesaj:
PUB FOO 11NATS!
gRPC este o librărie construită peste HTTP2 și care ajută la load balancing, tracing, la verificarea sănătății și la autentificare. Mesajele sunt schimbate în format binar pe TCP, utilizându-se optimizări HTTP2 pentru multiplexare și streaming.
Sursa - https://landscape.cncf.io/images/landscape.png
În imaginea de mai jos se pot observa multitudinea de aplicaţii dezvoltate în jurul conceptului de cloud native applications [52].
Odată cu schimbările din backend a apărut și nevoia optimizării frontendului - încercarea unor tehnologii mai noi, deployment independent, împărțirea implementării între mai multe echipe autonome. Astfel a apărut necesitatea de a trece de la o aplicație frontend monolit la ceva asemănător cu microserviciile. Rezultatul a fost micro frontends [32]; mai multe aplicații frontend care împreună să se comporte ca o singură aplicație frontend.
Micro Frontends se poate implementa în mai multe moduri, vom aminti câteva:
server-side rendering - gateway care redirecționează cererile către serviciul cerut;
client-side iframe - interfața aplicației ca sumă de iframe-uri (fiecare cu câte o micro interfață);
client-side JavaScript - fiecare micro frontend are un entry-point care este folosit pentru rutare către interfața dorită;
O dată cu simplificarea folosirii platformelor și diversificarea lor, a apărut posibilitatea de a rula direct în cloud cod/funcții (FaaS) sau de a folosi API care folosește un cloud (BaaS).
Acest tip de arhitectură se numește serverless computing și are câteva beneficii importante:
infrastructura este abstractizată - management automat al infrastructurii;
costuri mici - plata se face în funcție de utilizarea serviciului;
scalabilitate automată;
instalare rapidă;
Dar și câteva dezavantaje:
performanță redusă din cauza cold-start-urilor; (Pornirea serviciului înseamnă uneori pornirea unui container.)
folosirea API-urilor nestandardizate. Aceasta duce la riscul de a fi legat de furnizor (vendor lock-in).
Cold start înseamnă că a fost făcută o cerere, iar containerul, care trebuie să răspundă, este oprit, ceea ce adaugă la timpul de răspuns și timpul necesar downloadului și pornirii containerului și al microserviciului adăugat la răspunsul propriu-zis de la microserviciu.
Warm start înseamnă că deja este pornit containerul și poate să răspundă. Dacă timp de câteva minute containerul nu mai primește cereri, se va opri singur. Pentru AWS Lambda există posibilitatea de a elimina cold-starts folosind Provisioned Concurrency [49].
Mikhail Shilkov, în articolul său [31], a prezentat un grafic în legătură cu timpul cold start în funcție de limbajul de programare.Conform măsurătorilor sale, funcțiile scrise în Node.js au cel mai mic cold-start, depinzând bineînțeles și de dimensiunea codului.
Pornirea unei aplicații Spring Boot obișnuite poate să dureze între 10 și 20 secunde în funcție de complexitate. Există interes în a reduce acest timp - Spring Boot 2.2 a introdus lazy initialization [43] tocmai pentru a reduce din timpul de start al aplicațiilor bazate pe Spring Framework (pe lângă suportul pentru directivele ComponentScan (lazyInit = true) și Import.
Quarkus la fel ca Micronaut sau Helidon este un framework lightweight pentru microservicii.
Conform [18], o aplicație bazată pe Quarkus nativă/JVM comparabilă cu o versiune bazată pe Spring Boot, consumă de 50/1.5 de ori mai puțină memorie (2.1 MB, 74 MB versus 115 MB) și pornește de 500/250 ori mai repede (0.01/0.018 sec versus 5.27 sec).
O aplicație bazată pe Quarkus va economisi bani, folosind mai puține resurse și pornind mai repede.
Conform [5][6] se observă următoarele tendințe în adoptare soluțiilor de cloud:
costurile din ce în ce mai reduse ale cloudului vor duce la o migrare și mai mare a aplicațiilor în cloud, mai mult ca și lift-and-shift (transfer, așa cum sunt) decât ca și aplicații cloud native.
până în 2022, lipsa calificărilor legate de cloud și IaaS va încetini migrarea către aplicații cloud native.
până în 2023 furnizorii mari de cloud vor avea micro centre de date oferind o paletă restrânsa din propriile servicii, închiriabile în sistem gen bancomat
soluțiile multicloud vor reduce "vendor lock-in"; soluțiile cloud vor fi din ce în ce mai standardizate/federalizate [44].
conteinerizarea va lua avânt.
service meshurile vor deveni un lucru obișnuit.
întărirea securității odată cu adoptarea cloudului public.
Edge computing redefinește cloudul prin faptul că unele capacități de calcul se mută din centrul de date, cât mai aproape de utilizator, device, senzori etc, creându-se, astfel, o structură distribuită de cloud. International Data Corporation (IDC) o descrie ca: "o plasă/mesh peste micro centrele de data care procesează și stochează date locale esențiale și le trimit către un centru de date central sau DaaS, colocate în 10 metri pătrați."
Acest mod de lucru este important pentru IoT, unde este nevoie de procesarea unor cantități uriașe de date, cât mai local posibil, încât să nu fie pierderi de performanță cauzate de transferul prin rețea. De asemenea, datele de la senzori fiind prelucrate parțial, doar rezultatele/agregările trebuie trimise către centru, reducându-se astfel și costurile de rețea și stocare.
În acest articol am urmărit evoluția cloudului: de la definirea sa ca idee și expunerea primelor sale implementări la menționarea problemelor apărute și a soluțiilor de rezolvare a acestora. De asemenea, am enumerat câteva dintre tendințele de schimbare a cloud computing-ului.
https://medium.com/threat-intel/cloud-computing-e5e746b282f5
https://www.eweek.com/innovation/cloud-2020-canonical-s-six-trends-shaping-the-future-of-cloud
https://www.gartner.com/smarterwithgartner/4-trends-impacting-cloud-adoption-in-2020/
https://medium.com/solo-io/https-medium-com-solo-io-supergloo-ff2aae1fb96f
https://imelgrat.me/cloud/cloud-services-models-help-business/
https://blog.back4app.com/2019/07/24/backend-as-a-service-baas/
https://www.enterpriseintegrationpatterns.com/patterns/conversation/LoadBalancer.html
https://www.toptal.com/software/trunk-based-development-git-flow
https://dzone.com/articles/event-driven-microservices-patterns
https://dzone.com/articles/microservices-quarkus-vs-spring-boot
https://imelgrat.me/cloud/cloud-services-models-help-business/
https://www.cloudflare.com/learning/serverless/serverless-vs-containers/
https://dzone.com/articles/the-future-of-spring-cloud-microservices-after-net
https://medium.com/@priyalwalpita/software-architecture-patterns-layered-architecture-a3b89b71a057
https://medium.com/@SoftwareDevelopmentCommunity/what-is-service-oriented-architecture-fa894d11a7ec
https://dzone.com/articles/monolith-to-microservices-using-the-strangler-patt
Working Effectively with Legacy Code by Michael Feathers
Patterns of Enterprise Application Architecture by Martin Fowler
cloud Native Development Patterns and Best Practices by John Gilbert
cloud Architecture Patterns by Bill Wilder
https://thenewstack.io/nats-rest-alternative-provides-messaging-distributed-systems/
https://thenewstack.io/10-key-attributes-of-cloud-native-applications/
https://www.confluent.io/blog/event-sourcing-cqrs-stream-processing-apache-kafka-whats-connection/
https://devops.com/feature-branching-vs-feature-flags-whats-right-tool-job/
https://cloud.spring.io/spring-cloud-netflix/multi/multi__modules_in_maintenance_mode.html
https://spring.io/blog/2019/03/14/lazy-initialization-in-spring-boot-2-2
https://kubernetes.io/docs/concepts/cluster-administration/federation/#why-federationc
https://docs.microsoft.com/en-us/azure/architecture/patterns/bulkhead
https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html
de Andra Chicoș
de Iulia Creta , Bogdan Barza