În acest articol, vom descrie un sistem backend modern de microservicii, instalate în containere Docker în cadrul unei infrastructuri Cloud. Articolul va prezenta o viziune de ansamblu asupra diferitelor componente și tehnologii utilizate. Articolul nu recapitulează și nu face un rezumat al documentaţiei pe această temă, ci oferă o viziune generală care aduce argumente pro și contra la abordările și strategiile de succes pentru 3SS. Vom explica și vom detalia aceste structuri pe baza experienţei de dezvoltare a propriilor produse pentru a putea oferi exemple reale.
Când se începe construirea arhitecturii pentru infrastructura backend și cea de sistem a produsului, am ţinut cont de următoarele aspecte legate de clienţii noștri posibili:
Utilizare maximă în prime-time - Serviciile VOD și TV au puncte maxime de utilizare la anumite ore din zi și anumite zile din săptămână în timp ce consumul scade în celelalte perioade. Într-o abordare clasică, infrastructura trebuie dimensionată pentru a putea face faţă punctelor de maxim consum pentru a fi apoi utilizată mai puţin sau deloc pentru restul timpului, adică 80-90%, între punctele maxime.
Creșterea constantă a bazei de utilizatori - În mod tipic, noile servicii au un număr mai mic de utilizatori la început, numărul crescând pe parcurs. Prin urmare, este nevoie de o infrastructură care se scalează odată cu creșterea și care poate fi menţinută și actualizată cu efort minim.
Am decis să ne depărtăm de arhitectura backend monolitică, dorind o soluţie care ne permite scalarea și care nu ne restricţionează la nivel de alegeri tehnologice. De asemenea, am dorit să ne asigurăm că putem menţine, actualiza și dezvolta servicii individuale în mod independent, reușind în același timp să limităm impactul potenţial asupra erorilor în timpul actualizărilor de producţie.
Deși acum toate serviciile sunt scrise în node.js, acest lucru nu presupune că nu putem folosi python sau alte limbaje mai potrivite pe viitor.
După cum sugerează titlul, infrastructura pe care o descriem are următoarele componente:
Găzduire Cloud (Cloud Hosting) - Folosim o infrastructură de tip Cloud care găzduiește clusterul și containerele. Aceeași structură poate fi replicată local sau pe un anume mediu de găzduire, deși avantajul scalabilității lipsește. Găzduirea Cloud permite nu doar posibilitatea de a închiria capacitate CPU sau mașini virtuale AWS, Google Cloud etc., ci facilitează și o gamă largă de servicii și produse care fac instalarea și găzduirea mai convenabile.
Containere Docker și Cluster - Sistemul de containere pe care îl vom explica și descrie este Docker. Containerele sunt organizate în clustere folosind un tool de orchestrare care gestionează nodurile.
O perspectivă simplificată ar fi:
Vom oferi detalii suplimentare în continuare.
Nu vom oferi detalii exhaustive pe aceasta temă, deoarece aceasta este extrem de mare. Arhitectura bazată pe microservicii s-a dezvoltat în ultimii ani odată cu trecerea la aplicațiile web și la serviciile terțe. Principiul major din spatele acesteia se poate exprima astfel: "Fă un singur lucru și fă-l bine" în loc de a încerca să faci totul. Acest lucru se aplică foarte bine dezvoltării API care este o parte crucială a aplicațiilor web.
Din perspectiva programării, unul din marile beneficii ale arhitecturii bazate pe microservicii este că permite celor mai mici echipe posibile (sau programatorilor individuali) să le dețină, să le dezvolte și să le întrețină. Când se livrează noi builduri, părțile incrementale și domeniul de aplicabilitate sunt suficient de mici pentru a permite revizii la timp și evaluări care necesită efort minim, permițând roll-outuri cu risc minim.
Microserviciile sunt caracterizate de obicei ca fiind:
Lipsite de stare și stupide - Sunt construite pentru a deservi scopuri specifice. Acestea fie ignoră, fie au o atitudine neutră raportat la orice stare sau sesiune. Acestea ar trebui să funcționeze pe direcția "input - proces - output".
În dezvoltarea produselor noastre, serviciile backend comunică intens și funcționează ca mediatori între alte servicii web și backend în timp ce păstrează doar o mică capacitate a funcționalității lor inerente. Acest fapt face ca arhitectura bazată pe microservicii să fie extrem de potrivită pentru a obține performanță înaltă.
O imagine container este un pachet executabil care conține tot ce este necesar pentru a rula un sistem de operare și niște aplicații. Containerele izolează aplicațiile și sistemele de operare în medii diferite, ceea ce permite să nu existe conflicte între versiuni de pachete și dependențe ale aceleiași gazde. Containerele sunt portabile și pot fi executate pe orice gazdă care rulează engine-ul respectiv.
Architectura containerelor
Infrastructura mașinilor virtuale
Una dintre cele mai dese prejudecăți despre Docker este că acesta este un sistem complet virtualizat precum virtualbox sau vagrant. De fapt, Docker (și alte soluții de tip container) folosesc API la nivel de sistem de operare pentru a partaja și consuma resursele cu sistemul gazdă fără a replica o mașină în totalitate.
O infrastructură bazată pe Docker are patru componente:
Docker (Server și Client) - Engine-ul Docker care execută containerele Docker este puntea către sistemul gazdă. Clientul Docker este un tool în linie de comandă care comunică cu daemonul și care execută comenzile propriu-zise.
Imagine + Container - Imaginile conțin sistemul de operare, librăriile și aplicația. Sunt configurate cu un "Dockerfile", iar apoi sunt executate de Dockerengine.
Registru - Registrul conține imaginile Docker într-o bază de date centralizate. Există registre publice precum dockerhub. Pentru instalarea noastră am decis să folosim propriul registru cu Nexus OSS.
Docker însuși folosește o arhitectură client-server. Sistemul gazdă care ar trebui să ruleze containerele trebuie să aibă docker-server/daemon (Docker-Engine).
De ce ar trebui să optăm pentru această abordare?
Utilizarea containerelor Docker în defavoarea altor soluții (e.g. mașini virtuale, gazde virtuale etc.) aduce o serie de avantaje:
Folosind containere, toate mediile de la dezvoltare la producție sunt identice pentru aplicație. Una din marile probleme ce există în dezvoltarea aplicațiilor backend este că, în mod normal, mediile nu sunt identice când se instalează builduri și apar probleme de dependențe ("Oh, aceasta este versiunea 10.2.1.2 - la staging am folosit 10.2.1.1 și a funcționat"). Mediile de dezvoltare/staging/producție nu sunt identice, sunt configurate diferit, iar un test QA pe oricare din aceste medii nu garantează că nu vor fi probleme după instalare. În trecut, replicarea unei instalări complete era o provocare care necesita eforturi mari. Folosind containere, putem rula același mediu peste tot.
Comparate cu mașinile virtuale, gazdele noastre gestionează mai bine surplusul și livrează performanță mai bună. În acest punct, am experimentat vagrant ca soluție la problema noastră pe medii diferite. Deși problema se poate rezolva așa, utilizarea unui mediu virtual complet necesită mai multe resurse din partea gazdei pentru a le putea rula.
Nu lansăm cod sursă sau builduri, ci medii întregi. Când se folosesc containere, instalarea nu mai presupune actualizarea buildurilor pe mediile țintă ci și instalarea mediului. Acest fapt reduce posibilitatea ca buildurile să se strice, deoarece un pas din procesul de build eșuează sau nu funcționează conform așteptărilor. Deoarece instalăm un container care este gata de rulare, știm că ceea ce se află pe mediul țintă rulează corect și conține un build corect funcțional. Acesta este parte a CI-pipeline.
Exemplu de dockerfile:
#
# This is a simple Dockerfile
#
FROM ubuntu:latest
MAINTAINER John doe "john.doe@3ss.tv"
RUN apt-get update
RUN apt-get install -y python python-pip wget
RUN pip install Flask
ADD hello.py /home/hello.py
WORKDIR /home
Acest Dockerfile preia ultima imagine ubuntu disponibilă (la dockerhub) și rulează 3 comenzi pentru a instala python și actualiza pachetele. Apoi, creează un fișier și stabilește directorul de lucru. Ca în exemplul de mai sus, se pot executa orice comenzi și acțiuni prin intermediul unui Dockerfile. Acest lucru este util, spre exemplu, pentru a crea imagini bazate pe o pre-configurare, iar apoi pentru a adăuga pași suplimentari ce trebuie adaptați sau extinși în timp, e.g. instalarea de pachete suplimentare sau realizarea de pași suplimentari pentru a configura și rula un serviciu
5. Putem scala mediile noastre fără a influența aplicația în vreun fel. Folosind un cluster și orchestrare putem rula oricâte instanțe dorim de pe orice container pe baza unor reguli foarte simple pentru load-balancing. Deoarece folosim containere individuale pentru fiecare serviciu, putem scala doar un punct de intrare special care se dovedește a fi puternic utilizat.
Arhitectura bazată pe Cloud este o temă vastă. Utilizarea containerelor o face mai comprehensibilă în anumite aspecte, deoarece în procesul de implementare este nevoie doar de puterea computațională a serviciului Cloud prin alocarea de gazde care rulează docker-engines. Când ne îndepărtăm de partea experimentală și ajungem la o structură mai stabilă și mai apropiată de producție, mai putem întâlni probleme ce trebuie luate în calcul când alegem furnizorul și detaliile instalării:
Prețul - Înțelegerea modelelor de preț și impactul lor asupra modului de construire și instalare a sistemului nu este atât de ușor precum pare. Pe lângă calcularea costurilor cu resurse (CPU, RAM etc.) alți factori importanți sunt serviciile adiționale și toolurile furnizorului Cloud care sunt folosite pentru load-balancing, managing etc. .
Deoarece folosirea unei astfel de infrastructuri presupune că vor fi multe mașini care rulează, este crucial să se monitorizeze performanța instanțelor și a aplicațiilor. A fost o provocare să se găsească un setup corect datorită volumului și varietății surselor de date. Colectarea datelor este complexă deoarece a avea un număr atât de mare de containere multiple mărește volumul pentru sys-logs, service-logs și application logs, toate acestea având nevoie de colectare și structurare.
Am decis să folosim ELK stack pentru a realiza acest lucru. ELK este acronimul pentru Elastic Logstash and Kibana, un set de 3 tooluri utilizate împreună pentru a prelua (logstash), stoca și indexa (Elastic), dar și pentru a afișa (Kibana) datele. Pentru citirea și livrarea de log-files pentru mașini utilizăm filebeat, care poate citi loguri din stdin și din fișiere. Acesta păstrează o evidență a stărilor fiecărui log și se asigură că fiecare log-entry este livrat cu succes măcar o dată.
Pentru a strânge și organiza logurile am efectuat o serie de operații. Fiecare din serviciile noastre scrie loguri în format JSON ce conțin atribute bazate pe parametri de mediu ai containerelor care identifică serviciul printr-un service_name. Aceste loguri se scriu în fișierele de log stocate în foldere diferite bazate pe mediu/client/serviciu. În fiecare serviciu gazdă există deamonsets pentru filebeat - O capsulă rulează în fiecare gazdă care are aceeași etichetă ca nod selector al capsulei- ce rulează. Filebeat colectează logurile și le trimite către logstash. Logstash realizează filtrări în funcție de tipul de document (care este adăugat de filebeat pentru fiecare service log) și le salvează în indecși service-logs pentru Elastic. Pe baza service-names și service-types putem filtra și căuta în service-logs pentru a vedea loguri pentru servicii individuale și/sau medii etc. .
Deoarece folosim nginx ca punct de intrare pentru toate serviciile noastre, colectăm și nginx logs cu request time, upstream time și xforward pentru intrările din loguri, îmbogățindu-le cu informație de tip geolocație. Astfel, nu doar că înțelegem cât de bine lucrează serviciul, ci și cum se monitorizează performanța și cum se corelează informația.
Deși containerele Docker nu sunt mașini virtuale depline, acestea tot funcționează ca entități independente ce au nevoie de un sistem de operare și o configurare completă. De exemplu, pentru a rula un simplu container cu gazde și un nginx webserver, este nevoie de un sistem de operare care rulează nginx pentru a-l configura apoi nevoilor voastre.
Studii de caz tipice pentru containerele Docker presupun rularea de servicii web, în multe cazuri microservicii izolate. Mai mult, cum containerele Docker nu sunt mașini virtuale complete, se permite utilizarea unor sisteme de operare de capacitate redusă. Acest lucru este cu atât mai important cu cât unul dintre factorii care influențează performanța și managementul costului ține de dimensiunea imaginilor Docker. Cu cât imaginea este mai mică, cu atât mai puțin spațiu trebuie alocat și cu atât mai repede va porni containerul. Rămânând la cazul nginx-webserver, nu este nevoie să realizați o instalare completă de sistem de operare Debian cu toate librăriile și dependențele. Din acest motiv, arhitectura bazată pe containere a dus la popularitatea crescândă a distribuțiilor minimale de tip Linux, similare cu busybox. Aceste distribuții sunt facile, conținând un minimum de pachete care să ruleze aplicațiile necesare. Distribuții precum CoreOS sau RancherOS sunt construite pentru a rula containere (sau, mai specific, containere Docker) și a avea configurări și modificări specifice.
De obicei, un singur container Docker nu va asigura configurația pe care o căutați. Distracția, în ceea ce privește containerele Docker, începe când poți produce și îndepărta containere Docker oricând, din moment ce nevoia de resurse se modifică - în cel mai bun caz fără efort manual. Pentru a realiza acest lucru este nevoie de un tool de orchestrare. Docker-engine oferă această facilitate din moment ce puteți porni, opri sau instala containere Docker din simple instrucțiuni în linii de comandă. Totuși, aceste capabilități sunt prea limitate pentru o configurare automată, pentru care veți avea nevoie de un alt engine care monitorizează consumul de resurse, utilizarea și acuratețea cu care funcționează containerele atât în privința gestionării instanțelor, cât și a traficului.
Există o serie de soluții disponibile.
Acesta este un sistem de clustering nativ realizat de Docker. Este o parte componentă din Docker și este disponibilă din oficiu. Utilizează API-uri Docker standard și este cea mai apropiată de modul în care funcționează Docker. Este cel mai ușor de înțeles.
Kubernetes este un sistem de orchestrare și clustering, întreținut de Google, care are particularități raportat la operațiile Docker native. Totuși, Kubernetes este cel mai popular sistem când vine vorba de medii de producere scalate care trebuie să se adapteze la diferite cerințe în ceea ce privește resursele pe baza încărcării și a traficului, cu grad mare de încredere.
Kubernetes și Google Cloud sunt o combinație bună pentru infrastructura noastră, cu o serie de beneficii:
Migrarea clusterelor Kubernetes la o nouă versiune funcționează automat cu un singur click.
Coordonatorul clusterelor Kubernetes este întreținut de infrastructura Google, deci nu e nevoie de alocarea unui server special pentru aceasta.
Rapiditate în acțiunile de suport și fixare de buguri. Noile buguri sunt reparate în câteva zile.
Cel mai mare avantaj în utilizarea Kubernetes este instalarea automată de microservicii având la bază scripturi.
Configurarea unui load-balancer nu necesită multă informație sau experiență. Cu o singură comandă, Google Cloud rezervă IP-ul static și configurează load-balancer. Configurarea sistemului de filtrare și firewall este ușoară.
Din doar câteva comenzi pentru a selecta tipul de mașină și numărul de noduri pentru un grup, se poate configura un întreg cluster, împreună cu un dashboard și monitorizare prin Graphana și Heapster.
Actualizarea serviciilor sau schimbarea de proprietăți precum e cazul parametrilor ENV ai unei instalări sunt lucruri ușoare ce pot fi realizate automat prin Bash scripts, ceea ce ajută la mentenanța serviciilor.
Utilizând kubeadm se poate configura un cluster pe servere Linux. Ultima versiune pare a fi stabilă, ceea ce face scalarea clusterului ușoară prin adăugarea de gazde noi ca serviciu.Când acest lucru se întâmplă, capsulele sunt migrate și scalate automat către gazda nouă.
În timpul actualizării noii versiuni de Kubernetes am avut probleme, deci nu vom recomanda configurarea kubeadm pentru producție.
Rancher este tentant deoarece are un UI prietenos ce permite gestionarea facilă a containerelor și a clusterelor. Ca orice UI prietenos, vine cu prețul "ascunderii" elementelor interne Docker, făcând foarte grea monitorizarea și rezolvarea problemelor.
Rancher este foarte util în dezvoltare. Este ușor de configurat și instalat servicii, care mai apoi să fie configurate cu Rancher UI, pe care îl recomandăm pentru dezvoltarea rapidă a proiectelor de mici dimensiuni care utilizează un număr mic de containere. Este de asemenea logic să folosiți toată configurația de micro-servicii din aplicație atât din punctul de vedere al dezvoltării, cât și al structurării. Orice programator devops abil poate face acest lucru, iar sistemul final de producție se poate baza pe Kubernetes sau pe orice altă configurație.
În medii de producție, Rancher ar trebui utilizat cu grijă. Uneori, serviciile și capsulele nu au mai răspuns, nu au primit adrese IP. Aceste probleme se pot rezolva ușor pe mediul de lucru zilnic, apoi aplicat în producție, dar este nevoie de timp și atenție pentru a le rezolva.
Concluzie: Pentru configurarea mediului de producție care răspunde expectanțelor de scalabilitate, automatizare și încredere (cel puțin în arii non-enterprise, open-source) Kubernetes este cea mai bună soluție, deși are cea mai abruptă curbă de învățare.
Deoarece infrastructura noastră se bazează pe Google Cloud și Kubernetes, am decis să folosim soluția de the load-balancing oferită de Google Cloud.
Noi servicii (în acest caz, nginx) se publică utilizând "Ingress deployment" ce folosește numele serviciului backend, Secret (creat înainte de cheia și certificatul ssl), și portul public. La instalarea acestui ingress se creează automat un LB pentru GC, se alocă un IP static, se aplică regula de redirecționare și se adaugă serverele backend.
Toate serviciile comunică prin portul 80, SSL-termination este gestionat de Google-cloud load-balancers.
În Google Cloud, se poate activa auto scale pentru grupuri de noduri. Când loadul o solicită, Google Cloud adaugă încă o gazdă în cluster, distribuie capsule către acel server și configurează load-balancer automat pentru a face referire la acest server de asemenea.
Unul dintre cele mai mari avantaje ale existenței containerelor, din punctul de vedere al dezvoltării, este faptul că dezvoltarea, compilarea, testarea și instalarea pot fi separate foarte clar prin procese bine integrate. De asemenea, după configurarea inițială, programatorii devops pot gestiona singuri toate aceste componente sau pot automatiza procesul. Utilizarea containerelor permite și o gestionare corectă și mai coerentă a interdependențelor, deschide multe alte posibilități la nivel de tehnologie, inclusiv posibilitatea de a ne îndrepta spre un mod de programare mai modular, bazat pe componente. Cum compilarea unei imagini Docker presupune doar rularea unui set de comenzi, se realizează un pas mare spre automatizarea verificării, compilării, testării și instalării de microservicii, ceea ce economisește din efortul depus atunci când se realizează proiecte de dimensiuni medii sau mari. Nu trebuie să uităm de modularizare și de dezvoltarea bazată pe CI, ceea ce aduce eforturi suplimentare de gestionare a proiectului și a sarcinilor de lucru. Decizia de a opta pentru această soluție trebuie bine gândită.
Așa cum s-a discutat anterior, infrastructura Docker de bază conține un docker-engine, un container, o imagine și o bază de date. Când se integrează un CI-pipeline pentru Docker, taskul principal după o compilare cu succes este publicarea imaginilor în baza de date de unde pot fi luate de mediile țintă și executate. Aceasta este CI-pipeline pe scurt, deși e nevoie de automatizarea mai multor pași pentru o configurare completă:
Compilarea de imagini Docker din gitlab utilizând comanda "docker build". Astfel, se creează o nouă imagine pornind de la imaginea de bază și de la Dockerfile, elemente ce pot fi apoi instalate.
Trimiterea imaginilor Docker către baza de date prin comanda "docker push".
Nu este atât de ușor precum pare să alcătuiţi un sistem structurat și să faceţi arhitectura să funcţioneze. Când am început dezvoltarea, a trebuit să ţinem cont de faptul că software-ul utilizat era la începuturile dezvoltării sale. Noile releaseuri au introdus adesea schimbări nocive, în timp ce altele nu au funcţionat conform așteptărilor. Această situaţie s-a îmbunătăţit, dar încă ne confruntăm cu provocări.
Mai mult, construirea unui CI-pipeline s-a dovedit complicată. Construirea containerelor a eșuat des, iar identificarea cauzei a necesitat multe încercări și greșeli.
Ultimul pas, orchestrarea, a avut nevoie de multă testare și cercetare, deoarece nu au existat multe implementări de referinţă și nici documentaţie adecvată despre cum să se realizeze corect.
Când aceste provocări sunt depășite, beneficiile sunt enorme:
Instalarea este ușoară și rapidă. Aceasta se realizează automat pornind de la procesul vostru CI și puteţi fi siguri că ceea ce instalaţi funcţionează.
Iniţierea noilor programatori, începerea de servicii noi, testarea și build-ul au devenit mult mai ușoare și mai eficiente.
Ca orice soluţie, aceasta are teme ce necesită atenţie sporită. Cele care se aplică în cazul de faţă ar fi:
Generarea de release notes pentru servicii;
Automatizarea testării buildului generat;
Managementul scalabil și ușor de centralizat a serviciilor configurate;
Peisajul containerelor se dezvoltă foarte rapid în acest moment. Sisteme de operare optimizate relativ la containere, instrumente de orchestrare și management, toate sunt dezvoltate și publicate, iar unele din abordările noastre ar trebui să fie revizuite pentru a ţine pasul cu dezvoltarea.
de Bálint Ákos
de Andrei Oneț
de Raul Boldea
de Ioana Varga