Pe internet (service mesh), și iată încă unul. Ura! Dar de ce? Pentru că vreau să-mi expun opinia că ar fi fost mai bine ca serviciile-mesh să fi apărut acum 10 ani, înainte de apariția platformelor de containere, cum ar fi Docker și Kubernetes. Nu susțin că opinia mea este mai bună sau mai proastă decât altele, dar având în vedere că serviciile-mesh sunt animale destul de complexe, diversitatea punctelor de vedere va ajuta la o mai bună înțelegere a lor.
Voi vorbi despre platforma dotCloud, care a fost construită pe baza a peste o sută de microservicii și a susținut mii de aplicații în containere. Voi explica problemele cu care ne-am confruntat în timpul dezvoltării și lansării ei, și cum serviciile-mesh ar fi putut ajuta (sau nu).
Istoria dotCloud
Am scris deja despre istoria dotCloud și despre alegerea arhitecturii pentru această platformă, dar am spus puțin despre nivelul de rețea. Dacă nu doriți să vă adânciți în lectura despre dotCloud, iată pe scurt esența: aceasta este o platformă ca serviciu PaaS, care permite clienților să ruleze o gamă largă de aplicații (Java, PHP, Python…), cu suport pentru o varietate de servicii de date (MongoDB, MySQL, Redis…) și un flux de lucru similar cu Heroku: încarci codul tău pe platformă, aceasta construiește imagini ale containerelor și le desfășoară.
Voi explica cum era direcționat traficul către platforma dotCloud. Nu pentru că ar fi fost ceva deosebit (deși pentru vremea sa sistemul funcționa destul de bine!), ci în primul rând pentru că, folosind instrumentele moderne, un design de acest fel poate fi realizat cu ușurință într-un timp scurt de o echipă modestă, dacă au nevoie de un mod de a ruteze traficul între o mulțime de microservicii sau aplicații. Astfel, se pot compara opțiunile: ce se întâmplă dacă dezvolți totul singur sau folosești un serviciu-mesh existent. Alegerea standard: a face singur sau a cumpăra.
Rutarea traficului pentru aplicațiile găzduite
Aplicațiile de pe dotCloud pot oferi puncte finale HTTP și TCP.
Punctele finale HTTP sunt adăugate dinamic în configurația clusterului de echilibratori de sarcină . Este similar cu ceea ce fac astăzi resursele în Kubernetes și un echilibrator de sarcină precum .
Clienții se conectează la punctele finale HTTP prin domeniile corespunzătoare, cu condiția ca numele de domeniu să indice spre echilibratorii de sarcină dotCloud. Nimic special.
Punctele finale TCP sunt legate de numărul portului, care este apoi transmis tuturor containerelor din acea stivă prin intermediul variabilelor de mediu.
Clienții se pot conecta la punctele finale TCP folosind numele de gazdă corespunzător (ceva de genul gateway-X.dotcloud.com) și numărul portului.
Acest nume de gazdă se rezolvă pe un cluster de servere „nats“ (nu are legătură cu ), care vor ruta conexiunile TCP începătoare către containerul corect (sau, în cazul serviciilor cu echilibrare a încărcării, către containerele corecte).
Dacă ești familiarizat cu Kubernetes, probabil că îți va aminti de serviciile .
Pe platforma dotCloud nu a existat un echivalent pentru serviciile : pentru simplificare, accesul la servicii era la fel atât din interior, cât și din exteriorul platformei.
Totul a fost organizat destul de simplu: implementările inițiale ale rețelelor de rutare HTTP și TCP, probabil, aveau doar câteva sute de linii de Python. Algoritmi simpli (aș spune, naivi) care au fost perfecționați pe măsură ce platforma a crescut și au apărut cerințe suplimentare.
O refactorizare extinsă a codului existent nu a fost necesară. În special, pot folosi direct adresa obținută prin variabilele de mediu.
Cum se diferențiază acest lucru de un mesh de servicii modern?
Limitată vizibilitate. Nu aveam deloc metrici pentru rețeaua de rutare TCP. În ceea ce privește rutarea HTTP, în versiunile ulterioare au apărut metrici HTTP detaliate cu coduri de eroare și timpi de răspuns, dar osuri mele moderne merg și mai departe, asigurând integrarea cu sisteme de colectare a metricelor, precum Prometheus, de exemplu.
Vizibilitatea este importantă nu doar din perspectiva operațională (pentru a ajuta la rezolvarea problemelor), ci și atunci când se lansează noi funcții. Se referă la un și .
Eficiența rutării de asemenea, este limitat. În rețeaua de rutare dotCloud, tot traficul trebuia să treacă printr-un cluster de noduri de rutare dedicate. Acest lucru a însemnat o posibilă traversare a mai multor limite AZ (zone de disponibilitate) și o întârziere semnificativă. Îmi amintesc cum am dezgheat probleme cu codul care efectua mai bine de o sută de interogări SQL pe pagină și pentru fiecare interogare deschidea o nouă conexiune la serverul SQL. La lansarea locală, pagina se încarcă instantaneu, dar în dotCloud, încărcarea durează câteva secunde, deoarece pentru fiecare conexiune TCP (și fiecare interogare SQL ulterioară) sunt necesare zeci de milisecunde. În acest caz particular, problema a fost rezolvată prin conexiuni persistente.
Mediile de servicii moderne gestionează mai bine astfel de probleme. În primul rând, ele verifică ce conexiuni sunt rutate la sursă. Fluxul logic este același: client → mesh → serviciu, dar acum mesh-ul funcționează local, nu pe noduri îndepărtate, așa că conexiunea client → mesh este locală și foarte rapidă (microsecunde în loc de milisecunde).
Mediile de servicii moderne implementează, de asemenea, algoritmi de echilibrare a încărcării mai inteligenți. Prin monitorizarea funcționalității backend-urilor, acestea pot trimite mai mult trafic către backend-uri mai rapide, ceea ce duce la îmbunătățirea performanței generale.
Securitate de asemenea, este mai bine. Rețeaua de rutare dotCloud a funcționat complet pe EC2 Classic și nu criptase traficul (presupunând că, dacă cineva a reușit să instaleze un sniffer pe traficul de rețea EC2, aveți deja probleme mari). Mediile de servicii moderne protejează transparent tot traficul nostru, de exemplu, prin autentificare mutuală TLS și criptare ulterioară.
Rutarea traficului pentru serviciile platformei
Bine, am discutat despre traficul dintre aplicații, dar ce despre platforma însăși dotCloud?
Platforma însăși consta în aproximativ o sută de microservicii responsabile pentru diverse funcții. Unele primeau cereri de la altele, iar unele erau lucrători de fundal care se conectau la alte servicii, dar nu primeau conexiuni. Oricum, fiecare serviciu trebuie să cunoască punctele finale ale adreselor la care trebuie să se conecteze.
Multe servicii de înalt nivel pot utiliza rețeaua de rutare descrisă mai sus. De fapt, multe dintre cele peste o sută de microservicii dotCloud au fost desfășurate ca aplicații obișnuite pe platforma dotCloud. Cu toate acestea, un număr mic de servicii de nivel inferior (în special cele care implementează rețeaua de rutare) aveau nevoie de ceva mai simplu, cu mai puține dependențe (deoarece pentru a funcționa, nu puteau depinde de ele însele – binecunoscuta problemă a găinii și a oului).
Aceste servicii de nivel inferior, esențiale, au fost desfășurate prin lansarea containerelor direct pe câteva noduri cheie. Acest proces nu a implicat serviciile standard ale platformei: orchestratorul, programatorul și runner-ul. Dacă doriți să comparați cu platformele moderne de containere, este similar cu lansarea planului de control de docker run direct pe noduri, în loc să delegați sarcina Kubernetes. Asta este destul de asemănător cu conceptul de , pe care le utilizează sau la pornirea unui cluster autonom.
Aceste servicii erau expuse într-un mod simplu și brut: în fișierul YAML erau enumerate numele și adresele lor; iar fiecare client trebuia să preia o copie a acestui fișier YAML pentru desfășurare.
Pe de o parte, acest lucru este extrem de fiabil, deoarece nu necesită suport pentru un sistem extern de stocare a cheilor/valorilor, cum ar fi Zookeeper (nu uitați, la acea vreme nu existau etcd sau Consul). Pe de altă parte, acest lucru îngreuna mutarea serviciilor. De fiecare dată când se realiza o mutare, toți clienții trebuiau să obțină un fișier YAML actualizat (și, potențial, să se repornescă). Nu era foarte convenabil!
Ulterior, am început să implementăm o nouă schemă, unde fiecare client se conecta la un server proxy local. În loc să știe adresa și portul, era suficient să cunoască doar numărul portului serviciului și să se conecteze prin localhost. Serverul proxy local gestionează această conexiune și o direcționează către serverul real. Acum, când backend-ul este mutat pe o altă mașină sau scalat, în loc să actualizăm toți clienții, trebuie să actualizăm doar toate aceste proxie locale; și repornirea nu mai este necesară.
(De asemenea, era planificat să se incapsuleze traficul în conexiuni TLS și să se adauge un alt server proxy pe partea de primire, precum și să se verifice certificatele TLS fără intervenția serviciului de primire, care este configurat să accepte conexiuni doar pe localhost).
Acesta este foarte similar cu de la Airbnb, dar diferența esențială este că SmartStack este implementat și desfășurat în producție, în timp ce sistemul intern de rutare dotCloud a fost pus deoparte când dotCloud s-a transformat în Docker.
Consider că SmartStack este unul dintre precursorii unor astfel de sisteme, precum Istio, Linkerd și Consul Connect, deoarece toate urmează un model comun:
- Lansarea unui proxy pe fiecare nod.
- Clienții se conectează la proxy.
- Planul de management actualizează configurația serverului proxy atunci când backend-urile se schimbă.
- … Profit!
Implementarea modernă a rețelei de servicii
Dacă am dori să implementăm o rețea similară astăzi, am putea utiliza principii similare. De exemplu, să configurăm o zonă internă DNS, asociind numele serviciilor cu adresele din spațiul 127.0.0.0/8. Apoi să lansăm HAProxy pe fiecare nod din cluster, acceptând conexiuni pe fiecare adresă a serviciului (în această subrețea 127.0.0.0/8) și redirecționând/balansând încărcătura către backend-urile corespunzătoare. Configurarea HAProxy poate fi gestionată , permițând stocarea informațiilor despre backend în etcd sau Consul și împingerea automată a configurației actualizate pe HAProxy, atunci când este necesar.
Așa funcționează Istio! Dar cu unele diferențe:
- Utilizează în loc de HAProxy.
- Menține configurația backend-ului prin Kubernetes API în loc de etcd sau Consul.
- Serviciile primesc adrese în rețeaua internă (adresele Kubernetes ClusterIP) în loc de 127.0.0.0/8.
- Are un component suplimentar (Citadel) pentru a adăuga autentificarea mutuală TLS între client și servere.
- Suportă noi funcții, cum ar fi ruperea circuitului (circuit breaking), trasarea distribuită, desfășurarea canarilor etc.
Să examinăm pe scurt unele dintre diferențe.
Envoy Proxy
Envoy Proxy a fost scris de compania Lyft [concurent al Uber pe piața taxiurilor – n.tr.]. Este în mare parte similar cu alte proxy-uri (de exemplu, HAProxy, Nginx, Traefik…), dar Lyft și-a scris propriul proxy pentru că aveau nevoie de funcționalități lipsă în alte proxy-uri, și le-a părut mai logic să construiască unul nou decât să extindă unul existent.
Envoy poate fi folosit singur. Dacă am un serviciu specific care trebuie să se conecteze la alte servicii, pot să-l configurez pentru a se conecta la Envoy și apoi să configurez și să reconfigurez dinamic Envoy cu locația altor servicii, obținând astfel multe funcționalități suplimentare excelente, cum ar fi observabilitatea. În loc de o bibliotecă client personalizată sau de a încorpora cod pentru urmărirea apelurilor, direcționăm traficul către Envoy, care strânge metricile pentru noi.
Dar Envoy este capabil să funcționeze și ca plan de date (data plane) pentru service mesh. Aceasta înseamnă că pentru acest service mesh Envoy este configurat plan de control (control plane).
Planul de control
În planul de control, Istio se bazează pe API-ul Kubernetes. Acesta nu diferă foarte mult de utilizarea confd, care se bazează pe etcd sau Consul pentru a vizualiza un set de chei în stocarea de date. Istio, prin API-ul Kubernetes, vizualizează un set de resurse Kubernetes.
Între timp: personal, mi s-a părut util acest , care afirmă:
Serverul API Kubernetes este un „server prost”, care oferă stocare, gestionare a versiunilor, validare, actualizare și semantică a resurselor API.
Istio este conceput pentru a funcționa cu Kubernetes; iar dacă doriți să-l folosiți în afara Kubernetes, trebuie să rulați o instanță a serverului API Kubernetes (și a serviciului de suport etcd).
Adresele serviciilor
Istio se bazează pe adresele ClusterIP oferite de Kubernetes, astfel încât serviciile Istio primesc o adresă internă (nu în intervalul 127.0.0.0/8).
Traficul către adresa ClusterIP pentru un anumit serviciu din clusterul Kubernetes fără Istio este interceptat de kube-proxy și trimis către serverul din spatele acestui proxy. Dacă sunteți interesat de detalii tehnice, kube-proxy stabilește reguli iptables (sau load balancers IPVS, în funcție de cum a fost configurat) pentru a rescrie adresele IP de destinație ale conexiunilor care trec prin adresa ClusterIP.
După instalarea Istio în clusterul Kubernetes, nimic nu se schimbă până când nu este activat explicit pentru acel consumator sau chiar pentru tot spațiul de nume, prin introducerea containerului sidecar în poduri personalizate. Acest container va rula o instanță de Envoy și va stabili o serie de reguli iptables pentru interceptarea traficului direcționat către alte servicii și redirecționarea acestui trafic către Envoy.
În integrarea cu DNS Kubernetes, aceasta înseamnă că codul nostru se poate conecta prin numele serviciului și totul «funcționează pur și simplu». Cu alte cuvinte, codul nostru face solicitări de tipul http://api/v1/users/4242, apoi api rezolvă solicitarea la 10.97.105.48, regulile iptables interceptază conexiunile cu 10.97.105.48 și le redirecționează către proxy-ul local Envoy, iar acest proxy local va direcționa solicitarea către backend-ul API real. Uf!
Bucăți suplimentare
Istio oferă, de asemenea, criptare end-to-end și autentificare prin mTLS (mutual TLS). De acest lucru se ocupă un component numit Citadel.
Există, de asemenea, un component Mixer, pe care Envoy îl poate solicita pentru fiecare solicitare, pentru a lua o decizie specială cu privire la această solicitare în funcție de diferiți factori, cum ar fi capetele de cerere, încărcarea backend-ului etc... (nu vă faceți griji: există multe instrumente pentru a menține funcțional Mixer, iar chiar dacă acesta nu mai funcționează, Envoy va continua să funcționeze normal ca proxy).
Și, desigur, am menționat observabilitatea: Envoy colectează o cantitate uriașă de metrici, oferind în același timp trasabilitate distribuită. În arhitectura microserviciilor, dacă o solicitare API trebuie să treacă prin microserviciile A, B, C și D, atunci la intrarea în sistem, trasabilitatea distribuită va adăuga la solicitare un identificator unic și va păstra acel identificator prin sub-solicitările către toate aceste microservicii, permițând înregistrarea tuturor apelurilor corelate, întârzierile acestora etc.
A dezvolta sau a cumpăra
Istio are reputația unei sisteme complexe. În schimb, construirea rețelei de rutare, pe care am descris-o la începutul acestei postări, este relativ simplă cu instrumentele existente. Deci, are sens să construim propriul serviciu mesh în loc?
Dacă avem nevoi modeste (nu avem nevoie de observabilitate, întrerupător de lanț și alte finețuri), ne vin în minte gânduri despre dezvoltarea propriului instrument. Dar dacă folosim Kubernetes, s-ar putea să nu fie necesar, deoarece Kubernetes oferă deja instrumente de bază pentru descoperirea serviciilor și echilibrarea sarcinilor.
Însă, dacă avem cerințe avansate, atunci „cumpararea” unui serviciu mesh pare a fi mult mai bună opțiune. (Nu este întotdeauna „cumpărare”, deoarece Istio este open source, dar tot avem nevoie de timp de inginerie pentru a înțelege cum funcționează, a-l desfășura și a-l gestiona).
Ce să alegi: Istio, Linkerd sau Consul Connect?
Deocamdată am discutat doar despre Istio, dar acesta nu este singurul service mesh. O alternativă populară este , iar mai există și .
Ce să alegi?
Sincer, nu știu. În acest moment, nu mă consider suficient de competent pentru a răspunde la această întrebare. Există câteva cu comparația acestor instrumente și chiar .
O abordare promițătoare este utilizarea unui instrument precum . Acesta implementează un strat de abstractizare pentru a simplifica și unifica API-urile furnizate de service mesh-uri. În loc să studiem API-urile specifice (și, în opinia mea, relativ complicate) ale diferitelor service mesh-uri, putem folosi construcții mai simple din SuperGloo - și să comutăm ușor de la unul la altul, de parcă am avea un format intermediar de configurare care descrie interfețele HTTP și backendurile, capabil să genereze configurația efectivă pentru Nginx, HAProxy, Traefik, Apache…
Am experimentat puțin cu Istio și SuperGloo, iar în următorul articol vreau să arăt cum se adaugă Istio sau Linkerd într-un cluster existent folosind SuperGloo și cât de bine va funcționa acesta, adică va permite comutarea de la un service mesh la altul fără a rescrie configurațiile.
Sursa: habr.com
