Kubernetes Event-Driven Autoscaling (KEDA) este o unealtă open source, care permite scalarea automată a workloadurilor bazate pe evenimente în Kubernetes. Dar ce înseamnă asta pentru noi, ca dezvoltatori de aplicații sau administratori? Haideți să aruncăm o privire!
Scalabilitatea reprezintă una dintre cele mai relevante atribute ale unei aplicații la momentul actual. Abilitatea de a ridica sau elimina instanțe - în funcție de traficul pe care îl avem - este necesară pentru o utilizare eficientă a resurselor computaționale, dar și pentru a asigura disponibilitatea aplicației. Importanța utilizării eficiente a resurselor rezidă în considerente de ordin financiar, dar și legate de mediul înconjurător, disponibilitatea fiind crucială pentru o experiență bună a utilizatorului.
În primul rând, pentru a putea scala un volum de muncă într-un mod eficient, acesta trebuie împărțit în bucăți cât mai mici. Spre exemplu, dacă avem un număr ridicat de utilizatori pe front-end și dorim să scalăm, nu putem face acest lucru dacă avem o structură monolitică (front-end și back-end în același container).
Un container este o unitate standard de software, care împachetează codul și dependințele sale, astfel încât aplicația să poată rula rapid și fiabil.
Unul dintre marile avantaje ale containerelor este faptul că acestea virtualizează sistemul de operare și creează un spațiu izolat pentru aplicație. În acest mod, putem rula containere pe orice mașină, indiferent de sistemul de operare, versiunea sa și mediul de rulare.
Drept urmare, este mult mai ușor să lansăm noi instanțe ale unei aplicații dacă aceasta este containerizată.
Odată cu creșterea popularității containerelor, inginerii software au întâmpinat o nouă problemă: cum pot fi administrate flote de containere într-un mod eficient și automat? Pe 9 septembrie 2014, Google a venit cu o soluție revoluționară: Kubernetes.
Kubernetes este un sistem open-source de orchestrare a containerelor pentru automatizarea implementării, scalării și gestionării software-ului. Pe scurt, haideți să înțelegem componenta principală a acestui sistem.
Podul reprezintă cea mai mică unitate care poate fi creată și administrată în Kubernetes, fiind un grup de unul sau mai multe containere, cu stocare și rețea comune și o specificație care explică cum trebuie rulate containerele. Ne putem gândi la un Pod ca la un înveliș pentru containere, care le face compatibile cu clusterul de Kubernetes.
Așadar, Podul este unitatea pe care dorim să o scalăm în funcție de volumul de lucru pe care îl are de procesat.
În Kubernetes, un HorizontalPodAutoscaler actualizează automat o resursă de workload (cum ar fi un Deployment sau StatefulSet), cu scopul de a scala automat numărul de Poduri pentru a se potrivi cererii. Aceasta este o resursă nativă în Kubernetes și poate fi folosită direct, fără nevoia unei extensii.
Scalare orizontală înseamnă că răspunsul la sarcina crescută este de a ridica mai multe Poduri. Acest lucru este diferit de scalarea verticală, care pentru Kubernetes ar însemna alocarea mai multor resurse - de exemplu: memorie sau CPU - Podurilor care rulează deja.
În ciuda importanței sale, principala limitare a HorizontalPodAutoscalerului este faptul că el scalează Podurile doar în funcție de cât CPU și memorie utilizează. De exemplu, dacă media de utilizare a CPU-ului pentru setul nostru de Poduri este mai mare decât valoarea dorită, vom ridica mai multe Poduri pentru a distribui workloadul. Dacă media este mai mică, vom elimina un număr de Poduri pentru a folosi resursele eficient.
Acest mod de scalare este folositor, dar el nu poate acoperi toate cazurile de utilizare exact așa cum ne-am dori. Deoarece HorizontalPodAutoscalerul este nativ pentru Kubernetes, el se uită doar în interiorul clusterului. Drept urmare, el nu ne poate rezolva problema scalării în funcție de stimuli externi.
Kubernetes Event-Driven Autoscaling (KEDA) este o componentă ușoară care poate fi adăugată în orice cluster Kubernetes. KEDA funcționează alături de componente standard Kubernetes, cum ar fi HorizontalPodAutoscaler și poate extinde funcționalitatea sa.
După cum îi spune numele, KEDA ne permite scalarea condusă de evenimente. Pentru a înțelege necesitatea sa, vom considera un caz inspirat din viața de zi cu zi.
Suntem managerul unui supermarket și trebuie să decidem când trebuie deschise/închise casele de plată. Inițial, avem o singură casă deschisă, deoarece traficul nu este mare dimineața. Dar, pe măsură ce ne îndreptăm către orele de vârf, supermarketul are tot mai mulți cumpărători. În funcție de care metrică vom decide dacă trebuie deschisă o nouă casă?
Dacă ne-am orienta după HorizontalPodAutoscaler, am deschide o nouă casă când utilizarea resurselor noastre (răbdarea casierilor) ar ajunge la limită. Aceasta nu pare o soluție optimă în cazul de față. Mai degrabă, am alege să deschidem o nouă casă atunci când numărul de cumpărători care așteaptă la coadă crește peste un anumit număr (10, spre exemplu). Astfel, scalăm în funcție de abilitatea noastră de a procesa cumpărătorii, care este, de fapt, ceea ce urmărim să facem. Dacă numărul de cumpărători care așteaptă la coadă crește peste 10, este evident că nu dispunem de numărul necesar de case pentru procesare.
Revenind de la acest exercițiu de gândire, vom încerca să transpunem problema într-o aplicație software. Înlocuim cumpărătorii cu mesaje care trebuie procesate, casierii cu containerele noastre și coada cu un queue (Kafka, RabbitMQ etc).
Figura 1. Analogie supermarket - aplicație
Pentru a reuși să obținem același mod de scalare, în funcție de mesaje (evenimente), trebuie să găsim un mod de a extinde acel HorizontalPodAutoscaler. Pentru asta vom folosi KEDA!
Primul pas pe care îl vom face este să instalăm KEDA în clusterul nostru de Kubernetes. Această instalare ne definește o resursă personalizată (CustomResource) de tip ScaledObject. Această resursă o vom conecta la Deploymentul nostru de Poduri și ea va defini procesul de scalare prin intermediul unei configurații.
Instalarea nu este un proces dificil - ea poate fi efectuată cu ajutorul unui Helm chart.
Odată ce avem KEDA instalat în cluster, putem folosi resursele pe care ni le oferă. În exemplul de mai jos, vom utiliza un ScaledObject pe care îl vom conecta la aplicația noastră containerizată, denumită function. Această aplicație procesează mesaje dintr-un queue (Azure Service Bus Queue) și le afișează pe ecran.
Mai jos avem configurația resursei de tip ScaledObject.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: {{ include "function.fullname" . }}
spec:
scaleTargetRef:
# apiVersion: {api-version-of-target-resource}
# Opțional. Implicit: apps/v1
# kind: {kind-of-target-resource}
# Opțional. Implicit: Deployment
name: {{ include "function.fullname" . }}
# Obligatoriu. Trebuie să fie în același namespace
#cu ScaledObject-ul
# envSourceContainerName: {container-name}
# Opțional. Implicit:
#.spec.template.spec.containers[0]
pollingInterval: 30
# Opțional. Implicit: 30 de secunde
cooldownPeriod: 30
# Opțional. Implicit: 300 de secunde
idleReplicaCount: 0
# Opțional. Implicit: ignorat, trebuie să fie mai
# mic decât minReplicaCount
minReplicaCount: 1 # Opțional. Implicit: 0
maxReplicaCount: 10 # Opțional. Implicit: 100
După cum putem vedea, avem de a face cu o serie de parametri care trebuie setați:
scaleTargetRef este folosit pentru a identifica resursa pe care dorim să o scalăm. În cazul nostru, aceasta este obținută prin "function.fullname" și ea reprezintă deploymentul funcției noastre.
pollingInterval este intervalul de timp, în secunde, între două "verificări" făcute de KEDA. Aceste "verificări" au ca scop aflarea lungimii queue-ului. Trebuie să știm lungimea cozii pentru a decide dacă și cum trebuie efectuată scalarea.
Starea idle apare atunci când coada noastră este goală pentru acea perioadă de timp specificată (30 de secunde, în cazul nostru). În imagine, idleReplicaCount este setat la 0, ceea ce înseamnă că vom opri toate instanțele aplicației noastre dacă ne aflăm în starea idle.
Acest procedeu deosebit se numește scalare la zero și este un atribut dezirabil pentru orice aplicație. În mod tradițional, dacă nu folosim KEDA, HorizontalPodAutoscalerul ar avea nevoie de cel puțin o instanță care rulează pentru a putea scala, datorită modului în care a fost construit. Însă, cu ajutorul KEDA, putem opri această instanță și apoi să o pornim doar atunci când vom avea din nou trafic, ca și cum nimic nu s-ar fi întâmplat.
minReplicaCount și maxReplicaCount sunt doi parametri sugestivi - ei setează minimul (1) și maximul (10) de instanțe pe care le putem avea atunci când nu ne aflăm în starea idle.
Excelent! Am reușit să configurăm scalarea cu doar cinci numere și un cuvânt!
Totuși, ceea ce ne rămâne de făcut este să îi transmitem lui KEDA care este acel queue la care trebuie să asculte și când trebuie declanșată scalarea.
triggers:
- type: azure-servicebus
metadata:
# Necesar: queueName SAU topicName
# ȘI subscriptionName
queueName: {{ .Values.queueName }}
# Opțional, necesar când pod identity este
# folosită
# namespace: service-bus-namespace
# Opțional, se poate folosi și
# TriggerAuthentication
connectionFromEnv: {{ .Values.env.name }}
messageCount: "5"
# Opțional. Numărul de mesaje la care este
# declanșată scalarea. Implicit: 5 mesaje
activationMessageCount: "2"
# cloud: Private # Opțional.
# Implicit: AzurePublicCloud
# endpointSuffix: servicebus.airgap.example
# Necesar când cloud=Private
Pentru acest lucru, vom folosi secțiunea de triggers (declanșatoare). Aici specificăm queue-ul la care ascultă KEDA (un Azure Service Bus Queue) și modul în care ne conectăm la acesta (connectionFromEnv), care este, de fapt, un connection string.
Ultimul parametru important este messageCount, numărul de mesaje la care vom lua măsuri. Dacă avem peste cinci mesaje adunate în queue, vom considera că viteza de procesare nu este suficientă și vom scala orizontal, adăugând Poduri.
După cum putem observa, KEDA este vendor-agnostic, suportând a gamă variată de furnizori și de evenimente declanșatoare, Azure Service Bus Queue fiind doar unul dintre ele.
Singurul pas care ne-a rămas este să testăm dacă scalarea are loc așa cum ne așteptăm. Pentru a testa, vom trimite un număr uriaș de mesaje (10000) deodată în queue din portalul Azure. După câteva secunde, surprinsă de ultimele evenimente întâmplate, KEDA receptează schimbarea majoră care a avut loc la nivelul lungimii queue-ului și începe să ridice automat și rapid o întreagă flotă de Poduri pentru procesare. În imagine, am crescut numărul maxim de Poduri la 50 pentru a vizualiza mai ușor "cireașa de pe tort".
După ce toate mesajele sunt procesate și coada devine goală, KEDA va opri Podurile și nu vom rămâne cu niciunul, din moment ce ne vom afla în starea idle.
Într-o eră a microserviciilor, unde scalabilitatea reprezintă un aspect cheie și comunicarea asincronă prin queues, topics sau alte moduri de transmitere a mesajelor este des întâlnită în majoritatea aplicațiilor software, KEDA ne oferă o soluție simplă și extensibilă. KEDA facilitează astfel comunicarea asincronă, cel mai bun mod de comunicare între microservicii. Acest mod de comunicare nu necesită ca microserviciile să se aștepte unul pe celălalt precum într-un model Request-Reply.
Pe de altă parte, utilizarea eficientă a resurselor are atât un impact financiar important, cât și unul asupra mediului înconjurător.
În final, nu putem să uităm de cel mai mare avantaj pe care ni-l oferă KEDA: satisfacția inegalabilă de a urmări în timp real cum zeci sau chiar sute de Poduri pornesc instantaneu și ne "devorează" în întregime coada plină de mesaje.