Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes
Questo articolo aiuterà a capire come funziona il bilanciamento del carico in Kubernetes, cosa accade durante la scalabilità delle connessioni a lungo termine e perché vale la pena considerare il bilanciamento lato client se si utilizzano HTTP/2, gRPC, RSockets, AMQP o altri protocolli a lungo termine. 

Un breve accenno a come viene ridistribuito il traffico in Kubernetes 

Kubernetes fornisce due comode astrazioni per il deploy delle applicazioni: servizi (Services) e deploy (Deployments).

I deploy descrivono come e quante copie della tua applicazione devono essere eseguite in qualsiasi momento. Ogni applicazione viene distribuita come pod (Pod) e le viene assegnato un IP.

I servizi funzionano in modo simile a un bilanciatore di carico. Sono progettati per distribuire il traffico tra più pod.

Vediamo come appare.

  1. Nella diagramma sottostante puoi vedere tre istanze di un'applicazione e un bilanciatore di carico:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  2. Il bilanciatore di carico si chiama servizio (Service) e gli è stato assegnato un IP. Qualsiasi richiesta in entrata viene reindirizzata a uno dei pod:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  3. Lo scenario di deploy determina il numero di istanze dell'applicazione. In realtà non dovrai mai eseguire direttamente un pod:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  4. A ciascun pod viene assegnato un indirizzo IP:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

È utile considerare i servizi come un insieme di indirizzi IP. Ogni volta che accedi a un servizio, uno degli indirizzi IP viene selezionato dalla lista e utilizzato come indirizzo di destinazione.

Questo è come appare.

  1. Una richiesta curl a 10.96.45.152 al servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  2. Il servizio seleziona uno dei tre indirizzi dei pod come destinazione:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  3. Il traffico viene reindirizzato a un particolare pod:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Se la tua applicazione è composta da un frontend e un backend, avrai sia un servizio che un deploy per ciascuno.

Quando il frontend effettua una richiesta al backend, non ha bisogno di sapere quanti pod stanno servendo il backend: possono essere uno, dieci o centodieci.

Inoltre, il frontend non conosce gli indirizzi dei pod che servono il backend.

Quando il frontend effettua una richiesta al backend, usa l'indirizzo IP del servizio backend, che non cambia.

Ecco come appare.

  1. Il pod 1 fa una richiesta a un componente interno del backend. Invece di selezionare un pod specifico del backend, effettua una richiesta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  2. Il servizio seleziona uno dei pod del backend come indirizzo di destinazione:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  3. Il traffico va dal pod 1 al pod 5, selezionato dal servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  4. Il pod 1 non sa quanti pod simili al pod 5 siano nascosti dietro il servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Ma come distribuisce esattamente il servizio le richieste? Si sembra utilizzi un bilanciamento round-robin? Vediamo. 

Bilanciamento nei servizi di Kubernetes

I servizi di Kubernetes non esistono. Non esiste un processo per un servizio che abbia un indirizzo IP e una porta dedicati.

Puoi verificarlo accedendo a qualsiasi nodo del cluster e eseguendo il comando netstat -ntlp.

Non riuscirai nemmeno a trovare l'indirizzo IP assegnato al servizio.

L'indirizzo IP del servizio è collocato nello layer di controllo, nel controller, ed è registrato nel database — etcd. Questo stesso indirizzo è utilizzato da un altro componente — kube-proxy.
Kube-proxy riceve un elenco di indirizzi IP per tutti i servizi e forma un insieme di regole iptables su ciascun nodo del cluster.

Queste regole dicono: "Se vediamo l'indirizzo IP del servizio, dobbiamo modificare l'indirizzo di destinazione della richiesta e inviarla a uno dei pod."

L'indirizzo IP del servizio è utilizzato solo come punto di ingresso e non è gestito da alcun processo che ascolti su quell'indirizzo IP e quella porta.

Guardiamo questo caso

  1. Consideriamo un cluster con tre nodi. Su ciascun nodo ci sono pod:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  2. I pod correlati, colorati in beige, sono parte del servizio. Poiché il servizio non esiste come processo, è rappresentato in grigio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  3. Il primo pod richiede il servizio e deve raggiungere uno dei pod correlati:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  4. Ma il servizio non esiste, non c'è un processo. Come funziona?

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  5. Prima che la richiesta lasci il nodo, passa attraverso le regole iptables:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  6. Le regole iptables sanno che il servizio non esiste e sostituiscono il suo indirizzo IP con uno degli indirizzi IP dei pod correlati a quel servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  7. La richiesta riceve un indirizzo IP valido come indirizzo di destinazione e viene elaborata correttamente:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  8. A seconda della topologia di rete, la richiesta alla fine raggiunge il pod:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Gli iptables sono in grado di bilanciare il carico?

No, gli iptables vengono utilizzati per il filtraggio e non sono stati progettati per il bilanciamento.

Tuttavia, c'è la possibilità di scrivere un insieme di regole che fungano da finto bilanciatore.

Ed è esattamente quello che viene realizzato in Kubernetes.

Se hai tre pod, kube-proxy scriverà le seguenti regole:

  1. Selezionare il primo pod con una probabilità del 33%, altrimenti passare alla regola successiva.
  2. Selezionare il secondo pod con una probabilità del 50%, altrimenti passare alla regola successiva.
  3. Selezionare il terzo pod.

Questo sistema porta a una selezione casuale del pod con una probabilità del 33%.

Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Non c'è alcuna garanzia che il pod 2 venga selezionato dopo il pod 1.

Nota: iptables utilizza un modulo statistico con distribuzione casuale. Così, l'algoritmo di bilanciamento si basa su una scelta casuale.

Ora che è chiaro come funzionano i servizi, esaminiamo scenari più interessanti.

Le connessioni a lungo termine in Kubernetes non vengono scalate per default.

Ogni richiesta HTTP dal frontend al backend viene gestita tramite una singola connessione TCP, che viene aperta e chiusa.

Se il frontend invia 100 richieste al secondo al backend, vengono aperte e chiuse 100 diverse connessioni TCP.

È possibile ridurre il tempo di elaborazione delle richieste e diminuire il carico aprendo una connessione TCP e utilizzandola per tutte le successive richieste HTTP.

Nel protocollo HTTP è prevista una funzionalità chiamata HTTP keep-alive, o riutilizzo della connessione. In questo caso, una connessione TCP viene utilizzata per inviare e ricevere molteplici richieste e risposte HTTP:

Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Questa funzionalità non è attivata per default: sia il server che il client devono essere configurati di conseguenza.

La configurazione stessa è semplice e disponibile per la maggior parte dei linguaggi di programmazione e ambienti.

Ecco alcuni link a esempi in diversi linguaggi:

Cosa succede se usiamo il keep-alive nel servizio Kubernetes?
Supponiamo che sia il frontend che il backend supportino il keep-alive.

Abbiamo una copia del frontend e tre istanze del backend. Il frontend fa la prima richiesta e apre una connessione TCP al backend. La richiesta raggiunge il servizio, e uno dei pod del backend viene scelto come indirizzo di destinazione. Il pod del backend invia una risposta, e il frontend la riceve.

A differenza di una situazione normale, in cui la connessione TCP viene chiusa dopo la ricezione della risposta, ora rimane aperta per le successive richieste HTTP.

Cosa succede se il frontend invia altre richieste al backend?

Per inoltrare queste richieste verrà utilizzata la connessione TCP già aperta, tutte le richieste andranno allo stesso pod del backend a cui è andata la prima richiesta.

Non dovrebbe iptables ribilanciare il traffico?

Non in questo caso.

Quando si crea una connessione TCP, passa attraverso le regole di iptables, che selezionano un particolare pod del backend dove andrà il traffico.

Poiché tutte le richieste successive passano attraverso una connessione TCP già aperta, le regole di iptables non vengono più richiamate.

Vediamo come appare.

  1. Il primo pod invia la richiesta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  2. Sai già cosa succederà dopo. Il servizio non esiste, ma ci sono regole di iptables che gestiranno la richiesta:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  3. Uno dei pod del backend sarà scelto come indirizzo di destinazione:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  4. La richiesta raggiunge il pod. A questo punto, la connessione TCP permanente tra i due pod sarà stabilita:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  5. Qualsiasi richiesta successiva dal primo pod passerà attraverso la connessione già stabilita:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Di conseguenza hai un tempo di risposta più veloce e una maggiore capacità, ma hai perso la possibilità di scalare il backend.

Anche se hai due pod nel backend, con una connessione permanente, il traffico andrà sempre a uno di essi.

Si può risolvere questo problema?

Poiché Kubernetes non sa come bilanciare le connessioni permanenti, questo compito è affidato a te.

I servizi sono un insieme di indirizzi IP e porte che vengono chiamati endpoint.

La tua applicazione può ottenere un elenco di endpoint dal servizio e decidere come distribuire le richieste tra di essi. Puoi aprire una connessione permanente con ogni pod e bilanciare le richieste tra queste connessioni utilizzando il round-robin.

Oppure applicare algoritmi di bilanciamento più complessi..

Il codice lato client responsabile del bilanciamento deve seguire questa logica:

  1. Ottenere un elenco di endpoint dal servizio.
  2. Per ogni endpoint aprire una connessione permanente.
  3. Quando è necessario fare una richiesta, utilizzare una delle connessioni aperte.
  4. Aggiornare regolarmente l'elenco degli endpoint, creando nuove connessioni permanenti o chiudendo quelle vecchie in caso di modifiche dell'elenco.

Ecco come apparirà..

  1. Invece che il primo pod invii una richiesta al servizio, puoi bilanciare le richieste sul lato client:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  2. Devi scrivere un codice che chieda quali pod fanno parte del servizio:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  3. Non appena ricevi l'elenco, conservalo sul lato client e usalo per connetterti ai pod:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

  4. Sei tu a dover gestire l'algoritmo di bilanciamento del carico:

    Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Ora sorge la domanda: questo problema riguarda solo il HTTP keep-alive?

Bilanciamento del carico lato client

HTTP non è l'unico protocollo che può utilizzare connessioni TCP persistenti.

Se la tua applicazione utilizza un database, la connessione TCP non viene aperta ogni volta che hai bisogno di eseguire una query o recuperare un documento dal database. 

Invece, viene aperta e utilizzata una connessione TCP persistente al database.

Se il tuo database è distribuito in Kubernetes e l'accesso è fornito come servizio, ti troverai di fronte agli stessi problemi descritti nella sezione precedente.

Una replica del database sarà più sotto carico rispetto alle altre. Kube-proxy e Kubernetes non aiuteranno a bilanciare le connessioni. Dovrai occuparti del bilanciamento delle richieste al tuo database.

A seconda della libreria che utilizzi per connetterti al database, potresti avere diverse opzioni per risolvere questo problema.

Di seguito è riportato un esempio di accesso al cluster di database MySQL da Node.js:

var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();

var endpoints = /* recupera gli endpoint dal Servizio */

for (var [index, endpoint] of endpoints) {
  poolCluster.add(`mysql-replica-${index}`, endpoint);
}

// Esegui query al database MySQL clusterizzato

Esistono molti altri protocolli che utilizzano connessioni TCP persistenti:

  • WebSockets e WebSockets protetti
  • HTTP/2
  • gRPC
  • RSockets
  • AMQP

Dovresti già essere familiare con la maggior parte di questi protocolli.

Ma se questi protocolli sono così popolari, perché non esiste una soluzione standardizzata per il bilanciamento? Perché è necessario modificare la logica del client? Esiste una soluzione nativa per Kubernetes?

Kube-proxy e iptables sono stati creati per coprire la maggior parte degli scenari standard nell'implementazione in Kubernetes. Questo è stato fatto per comodità.

Se utilizzi un servizio web che fornisce un'API REST, sei fortunato: in questo caso non vengono utilizzate connessioni TCP persistenti, puoi utilizzare qualsiasi servizio Kubernetes.

Ma non appena inizi a utilizzare connessioni TCP persistenti, dovrai affrontare il problema di come distribuire equamente il carico sui backend. Kubernetes non contiene soluzioni pronte a riguardo.

Tuttavia, ci sono sicuramente opzioni che possono aiutare.

Bilanciamento delle connessioni persistenti in Kubernetes

In Kubernetes esistono quattro tipi di servizi:

  1. ClusterIP
  2. NodePort
  3. LoadBalancer
  4. Headless

I primi tre servizi funzionano su un indirizzo IP virtuale, che viene utilizzato da kube-proxy per costruire le regole iptables. Ma la base fondamentale di tutti i servizi è il servizio headless.

Un servizio headless non è associato a un indirizzo IP e fornisce solo un meccanismo per ottenere un elenco di indirizzi IP e porte associate ai pod collegati (endpoint).

Tutti i servizi si basano su un servizio headless.

Il servizio ClusterIP è un servizio headless con alcune aggiunte: 

  1. Il layer di gestione gli assegna un indirizzo IP.
  2. Kube-proxy forma le regole iptables necessarie.

In questo modo puoi ignorare kube-proxy e utilizzare direttamente l'elenco degli endpoint ottenuti dal servizio headless per bilanciare il carico nella tua applicazione.

Ma come aggiungere una logica simile a tutte le applicazioni distribuite nel cluster?

Se la tua applicazione è già distribuita, tale compito potrebbe sembrare impossibile. Tuttavia, c'è un'opzione alternativa.

Il Service Mesh può aiutarti

Probabilmente hai già notato che la strategia di bilanciamento del carico sul lato client è abbastanza standard.

Quando l'applicazione si avvia, essa:

  1. Ottiene un elenco di indirizzi IP dal servizio.
  2. Apre e mantiene un pool di connessioni.
  3. Aggiorna periodicamente il pool, aggiungendo o rimuovendo endpoint.

Non appena l'applicazione desidera effettuare una richiesta, essa:

  1. Seleziona una connessione disponibile, utilizzando una logica qualsiasi (ad esempio, round-robin).
  2. Esegue la richiesta.

Questi passaggi funzionano sia per le connessioni WebSockets che per gRPC e AMQP.

Puoi estrarre questa logica in una libreria separata e usarla nelle tue applicazioni.

Tuttavia, in alternativa, puoi utilizzare i service mesh, come Istio o Linkerd.

Il Service Mesh completa la tua applicazione con un processo che:

  1. Cerca automaticamente gli indirizzi IP dei servizi.
  2. Controlla le connessioni, come WebSockets e gRPC.
  3. Bilancia le richieste usando il protocollo corretto.

Il Service Mesh aiuta a gestire il traffico all'interno del cluster, ma è piuttosto impegnativo in termini di risorse. Altre opzioni includono l'uso di librerie di terze parti, come Netflix Ribbon, o proxy programmabili, come Envoy.

Cosa succede se si ignorano le questioni di bilanciamento?

Puoi non utilizzare il bilanciamento del carico e non notare alcun cambiamento. Diamo un'occhiata a diversi scenari operativi.

Se hai più clienti che server, non è un grosso problema.

Supponiamo che ci siano cinque clienti che si connettono a due server. Anche in assenza di bilanciamento, entrambi i server verranno utilizzati:

Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Le connessioni possono essere distribuite in modo non uniforme: è possibile che quattro clienti si siano connessi allo stesso server, ma c'è una buona probabilità che entrambi i server vengano utilizzati.

Ciò che è più problematico è lo scenario opposto.

Se hai meno clienti e più server, le tue risorse potrebbero non essere utilizzate a sufficienza, creando un potenziale collo di bottiglia.

Supponiamo che ci siano due clienti e cinque server. Nella migliore delle ipotesi, ci saranno due connessioni permanenti a due server tra i cinque.

Gli altri server resteranno inattivi:

Bilanciamento del carico e scalabilità delle connessioni a lungo termine in Kubernetes

Se questi due server non possono gestire le richieste dei clienti, la scalabilità orizzontale non aiuterà.

Conclusione

I servizi Kubernetes sono progettati per funzionare nella maggior parte degli scenari standard delle applicazioni web.

Tuttavia, una volta che inizi a lavorare con i protocolli delle applicazioni che utilizzano connessioni TCP permanenti, come i database, gRPC o WebSockets, i servizi non sono più adatti. Kubernetes non fornisce meccanismi interni per bilanciare le connessioni TCP permanenti.

Questo significa che devi scrivere applicazioni tenendo conto della possibilità di bilanciare dal lato client.

La traduzione è stata preparata dal team Kubernetes aaS di Mail.ru.

Ulteriori letture sull'argomento:

  1. Tre livelli di autoscaling in Kubernetes e come utilizzarli in modo efficace
  2. Kubernetes nello spirito della pirateria con un modello di implementazione.
  3. Il nostro canale Telegram sulla trasformazione digitale.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster