Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes
Questo articolo ti aiuterà a comprendere come funziona il bilanciamento del carico in Kubernetes, cosa succede durante il ridimensionamento delle connessioni persistenti e perché dovresti considerare il bilanciamento sul lato client se utilizzi HTTP/2, gRPC, RSockets, AMQP o altri protocolli a lunga durata. 

Un po' su come viene redistribuito il traffico in Kubernetes 

Kubernetes fornisce due astrazioni pratiche per il deployment delle applicazioni: i servizi (Services) e i deployment (Deployments).

I deployment 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 indirizzo IP.

I servizi hanno funzioni simili a quelle di un bilanciatore di carico. Sono progettati per distribuire il traffico tra più pod.

Vediamo come appare.

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  2. Il bilanciatore di carico è chiamato servizio (Service) e ha un indirizzo IP assegnato. Qualsiasi richiesta in ingresso viene reindirizzata a uno dei pod:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  3. Lo scenario di deployment definisce il numero di istanze dell'applicazione. Non dovrai quasi mai eseguire direttamente un pod:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  4. A ogni pod viene assegnato il proprio indirizzo IP:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

Appare nel seguente modo.

  1. Una richiesta curl 10.96.45.152 viene inviata al servizio:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  3. Il traffico viene reindirizzato a un pod specifico:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

Quando il frontend effettua una richiesta al backend, non ha bisogno di sapere quanti pod stanno servendo il backend: potrebbero essercene uno, dieci o cento.

Inoltre, il frontend non sa nulla sugli indirizzi dei pod che servono il backend.

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

Ecco come appare.

  1. Il pod 1 richiede un componente interno del backend. Invece di scegliere un pod specifico del backend, effettua una richiesta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Ma come distribuisce il servizio le richieste? Sembra che venga utilizzato un bilanciamento di tipo round-robin? Vediamo di capire. 

Bilanciamento nei servizi Kubernetes

I servizi Kubernetes non esistono. Non esiste un processo a cui è assegnato un indirizzo IP e una porta.

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

Non sarai nemmeno in grado di trovare l'indirizzo IP assegnato al servizio.

L'indirizzo IP del servizio è collocato nello layer di gestione, nel controller, ed è registrato nel database — etcd. Questo stesso indirizzo è utilizzato anche da un altro componente — kube-proxy.
Kube-proxy riceve l'elenco degli indirizzi IP per tutti i servizi e forma un insieme di regole iptables su ogni 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 tale indirizzo IP e porta.

Diamo un'occhiata a questo

  1. Consideriamo un cluster di tre nodi. Su ogni nodo ci sono dei pod:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti 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 collegati a quel servizio:

    Bilanciamento del carico e scalabilità delle connessioni persistenti 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 persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Gli iptables possono bilanciare il carico?

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

Tuttavia, è possibile scrivere un insieme di regole che funzionano come un bilanciatore simulato.

E questo è ciò 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. Scegli il secondo pod con una probabilità del 50%, altrimenti passa alla regola successiva.
  3. Scegli il terzo pod.

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

Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

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

Ora che hai capito come funzionano i servizi, vediamo scenari di lavoro più interessanti.

Le connessioni a lungo termine in Kubernetes non si scalano per default.

Ogni richiesta HTTP dal frontend al backend è gestita da 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 alleggerire il carico aprendo una singola connessione TCP e utilizzandola per tutte le successive richieste HTTP.

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

Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

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

Ecco alcuni link a esempi in diversi linguaggi:

Cosa succede se utilizziamo il keep-alive nel servizio Kubernetes?
Consideriamo che sia il frontend che il backend supportano il keep-alive.

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

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

Cosa succede se il frontend invia altre richieste al backend?

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

Non dovrebbe iptables ridistribuire il traffico?

Non in questo caso.

Quando viene creato un collegamento TCP, passa attraverso le regole di iptables, che selezionano il particolare pod del backend a cui verrà inviato il traffico.

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

Vediamo come appare.

  1. Il primo pod invia una richiesta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  2. Già sai cosa succederà dopo. Il servizio non esiste, ma ci sono regole di iptables che tratteranno la richiesta:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Di conseguenza, hai ottenuto una risposta più rapida 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ò correggere questo?

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

I servizi sono un insieme di indirizzi IP e porte 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 ciascun pod e bilanciare le richieste tra queste connessioni usando il round-robin.

Oppure applicare algoritmi di bilanciamento più complessi.

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

  1. Ottenere un elenco di endpoint dal servizio.
  2. Per ciascun endpoint, aprire una connessione permanente.
  3. Quando è necessario effettuare una richiesta, utilizzare una delle connessioni aperte.
  4. Aggiornare regolarmente l'elenco degli endpoint, creando nuove o chiudendo vecchie connessioni permanenti in caso di modifica 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 persistenti in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  3. Appena ottieni l'elenco, conservalo sul lato client e utilizzalo per connetterti ai pod:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

  4. Sei tu stesso a rispondere dell'algoritmo di bilanciamento del carico:

    Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Ora sorge una domanda: questo problema riguarda solo 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 ottenere un documento dal DB. 

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

Se il tuo database è distribuito in Kubernetes e l'accesso è fornito sotto forma di servizio, ti imbatterai negli stessi problemi descritti nella sezione precedente.

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

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

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

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

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

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

// Effettua query al database MySQL clusterizzato

Ci sono molti altri protocolli che utilizzano connessioni TCP persistenti:

  • WebSockets e WebSockets sicuri
  • 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 in Kubernetes?

Kube-proxy e iptables sono stati creati per gestire la maggior parte degli scenari standard quando si distribuisce 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 una volta che inizi a utilizzare connessioni TCP persistenti, dovrai capire come distribuire uniformemente il carico sui backend. Kubernetes non contiene soluzioni pronte in questo caso.

Tuttavia, esistono naturalmente opzioni che possono aiutare.

Bilanciamento delle connessioni a lungo termine in Kubernetes

In Kubernetes ci sono quattro tipi di servizi:

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

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

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

Tutti i servizi sono basati sul servizio headless.

Il servizio ClusterIP è un servizio headless con alcune aggiunte: 

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

In questo modo, puoi ignorare kube-proxy e utilizzare direttamente l'elenco dei punti finali ottenuti dal servizio headless per il bilanciamento del carico nella tua applicazione.

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

Se la tua applicazione è già distribuita, questa attività può sembrare impossibile. Tuttavia, c'è un'alternativa.

Service Mesh ti aiuterà

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 punti finali.

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

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

Questi passaggi funzionano per le connessioni WebSockets, gRPC e AMQP.

Puoi estrapolare questa logica in una libreria separata e utilizzarla nelle tue applicazioni.

Tuttavia, puoi utilizzare le service mesh, ad esempio Istio o Linkerd.

Service Mesh arricchisce la tua applicazione con un processo che:

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

Service Mesh aiuta a gestire il traffico all'interno del cluster, ma è abbastanza dispendioso 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 del bilanciamento?

Puoi non utilizzare il bilanciamento del carico e non notare alcuna modifica. Esaminiamo alcuni scenari di funzionamento.

Se hai più clienti che server, non è un problema così grande.

Supponiamo di avere cinque clienti che si connettono a due server. Anche senza bilanciamento, entrambi i server verranno utilizzati:

Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Le connessioni possono essere distribuite in modo non uniforme: potrebbe esserci la possibilità che quattro clienti si connettano allo stesso server, ma c'è una buona possibilità 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, e potrebbe sorgere un potenziale collo di bottiglia.

Supponiamo di avere due clienti e cinque server. Nel migliore dei casi, ci saranno due connessioni permanenti a due server dei cinque.

Gli altri server saranno inattivi:

Bilanciamento del carico e scalabilità delle connessioni persistenti in Kubernetes

Se questi due server non riescono a 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, non appena inizi a lavorare con protocolli applicativi che utilizzano connessioni TCP permanenti, come database, gRPC o WebSockets, i servizi non sono più adatti. Kubernetes non fornisce meccanismi interni per bilanciare le connessioni TCP permanenti.

Ciò significa che devi scrivere applicazioni tenendo in considerazione la possibilità di bilanciamento client-side.

Traduzione preparata dal team Kubernetes aaS di Mail.ru.

Cosa leggere ancora sull'argomento:

  1. Tre livelli di autoscaling in Kubernetes e come utilizzarli efficacemente
  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, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster