Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

Un po' di come viene ridistribuito il traffico in Kubernetes 

Kubernetes fornisce due comode astrazioni per la distribuzione delle applicazioni: Servizi e Distribuzioni.

Le distribuzioni descrivono come e quante copie della tua applicazione dovrebbero essere eseguite in un dato momento. Ogni applicazione viene distribuita come pod e le viene assegnato un indirizzo IP.

I servizi hanno una funzione simile a un bilanciatore del carico. Sono progettati per distribuire il traffico su più pod.

Vediamo come appare.

  1. Nel diagramma seguente puoi vedere tre istanze della stessa applicazione e un bilanciatore del carico:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  2. Il sistema di bilanciamento del carico è chiamato Servizio e gli viene assegnato un indirizzo IP. Qualsiasi richiesta in entrata viene reindirizzata a uno dei pod:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  3. Lo scenario di distribuzione determina il numero di istanze dell'applicazione. Non dovrai quasi mai espandere direttamente sotto:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

È utile pensare ai servizi come a una raccolta di indirizzi IP. Ogni volta che si accede al servizio, uno degli indirizzi IP viene selezionato dall'elenco e utilizzato come indirizzo di destinazione.

Sembra così.

  1. Una richiesta curl 10.96.45.152 viene ricevuta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  3. Il traffico viene reindirizzato a un pod specifico:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

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

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

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

Ecco come appare.

  1. Sotto 1 richiede il componente backend interno. Invece di selezionarne uno specifico per il backend, effettua una richiesta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  4. Under 1 non sa esattamente quanti pod come Under 5 si nascondono dietro il servizio:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

Ma come distribuisce esattamente il servizio le richieste? Sembra che venga utilizzato il bilanciamento round-robin? Scopriamolo. 

Bilanciamento nei servizi Kubernetes

I servizi Kubernetes non esistono. Non esiste alcun processo per il servizio a cui viene 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 si trova nel livello di controllo, nel controller e registrato nel database, ecc. Lo stesso indirizzo viene utilizzato da un altro componente: kube-proxy.
Kube-proxy riceve un elenco di indirizzi IP per tutti i servizi e genera un set 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 viene utilizzato solo come punto di ingresso e non è servito da alcun processo in ascolto su tale indirizzo IP e porta.

Diamo un'occhiata a questo

  1. Consideriamo un cluster di tre nodi. Ogni nodo ha pod:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  2. Fanno parte del servizio le cialde legate verniciate di beige. Poiché il servizio non esiste come processo, viene visualizzato in grigio:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  3. Il primo pod richiede un servizio e deve andare a uno dei pod associati:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  4. Ma il servizio non esiste, il processo non esiste. Come funziona?

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

È possibile bilanciare il carico di iptables?

No, iptables viene utilizzato per il filtraggio e non è stato progettato per il bilanciamento.

Tuttavia, è possibile scrivere una serie di regole che funzionino come pseudo-equilibratore.

E questo è esattamente ciò che viene implementato in Kubernetes.

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

  1. Seleziona il primo sottotitolo con una probabilità del 33%, altrimenti vai alla regola successiva.
  2. Scegli la seconda con una probabilità del 50%, altrimenti vai alla regola successiva.
  3. Seleziona il terzo sotto.

Questo sistema fa sì che ciascun pod venga selezionato con una probabilità del 33%.

Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

E non vi è alcuna garanzia che il Pod 2 venga scelto dopo il Pod 1.

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

Ora che hai compreso come funzionano i servizi, esaminiamo scenari di servizi più interessanti.

Le connessioni di lunga durata in Kubernetes non vengono scalate per impostazione predefinita

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

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

Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

Questa funzionalità non è abilitata per impostazione predefinita: sia il server che il client devono essere configurati di conseguenza.

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

Ecco alcuni link ad esempi in diverse lingue:

Cosa succede se utilizziamo il keep-alive in un servizio Kubernetes?
Supponiamo che sia il frontend che il backend supportino il keep-alive.

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

A differenza della solita situazione in cui la connessione TCP viene chiusa dopo aver ricevuto una risposta, ora viene mantenuta aperta per ulteriori richieste HTTP.

Cosa succede se il frontend invia più richieste al backend?

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

Iptables non dovrebbe ridistribuire il traffico?

Non in questo caso.

Quando viene creata una connessione TCP, passa attraverso le regole iptables, che selezionano un backend specifico dove andrà il traffico.

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

Vediamo come appare.

  1. Il primo pod invia una richiesta al servizio:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  2. Sai già cosa succederà dopo. Il servizio non esiste, ma esistono regole iptables che elaboreranno la richiesta:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  3. Uno dei pod di backend verrà selezionato come indirizzo di destinazione:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

Il risultato è un tempo di risposta più rapido e un throughput più elevato, ma si perde la capacità di scalare il backend.

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

Può essere risolto?

Poiché Kubernetes non sa come bilanciare le connessioni persistenti, questo compito spetta a te.

I servizi sono una raccolta 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 a ciascun pod e bilanciare le richieste tra queste connessioni utilizzando il round robin.

Oppure applicane di più algoritmi di bilanciamento complessi.

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

  1. Ottieni un elenco di endpoint dal servizio.
  2. Apri una connessione permanente per ciascun endpoint.
  3. Quando è necessario effettuare una richiesta, utilizzare una delle connessioni aperte.
  4. Aggiorna regolarmente l'elenco degli endpoint, creane di nuovi o chiudi le vecchie connessioni persistenti se l'elenco cambia.

Ecco come apparirà.

  1. Invece che sia il primo pod a inviare la richiesta al servizio, puoi bilanciare le richieste sul lato client:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

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

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  3. Una volta ottenuto l'elenco, salvalo sul lato client e utilizzalo per connetterti ai pod:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

  4. Sei responsabile dell'algoritmo di bilanciamento del carico:

    Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

Ora sorge la domanda: questo problema si applica solo al keep-alive HTTP?

Bilanciamento del carico lato client

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

Se la tua applicazione utilizza un database, una connessione TCP non viene aperta ogni volta che devi effettuare una richiesta o recuperare un documento dal database. 

Viene invece aperta e utilizzata una connessione TCP permanente al database.

Se il tuo database è distribuito su Kubernetes e l'accesso viene fornito come servizio, incontrerai gli stessi problemi descritti nella sezione precedente.

Una replica del database verrà caricata più delle altre. Kube-proxy e Kubernetes non aiuteranno a bilanciare le connessioni. È necessario fare attenzione a bilanciare le query nel 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 a un cluster di database MySQL da Node.js:

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

var endpoints = /* retrieve endpoints from the Service */

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

// Make queries to the clustered MySQL database

Esistono molti altri protocolli che utilizzano connessioni TCP persistenti:

  • WebSocket e WebSocket protetti
  • HTTP / 2
  • gRPC
  • RSocket
  • AMQP

Dovresti già avere familiarità con la maggior parte di questi protocolli.

Ma se questi protocolli sono così popolari, perché non esiste una soluzione di bilanciamento standardizzata? Perché la logica del client deve cambiare? Esiste una soluzione Kubernetes nativa?

Kube-proxy e iptables sono progettati per coprire i casi d'uso più comuni durante la distribuzione su Kubernetes. Questo è per comodità.

Se stai utilizzando un servizio Web che espone 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 tra i backend. Kubernetes non contiene soluzioni già pronte per questo caso.

Tuttavia, ci sono sicuramente opzioni che possono aiutare.

Bilanciamento delle connessioni di lunga durata in Kubernetes

Esistono quattro tipi di servizi in Kubernetes:

  1. IP cluster
  2. NodoPort
  3. Load Balancer
  4. Senza testa

I primi tre servizi funzionano in base a un indirizzo IP virtuale, che viene utilizzato da kube-proxy per creare regole iptables. Ma la base fondamentale di tutti i servizi è un servizio senza testa.

Al servizio headless non è associato alcun indirizzo IP e fornisce solo un meccanismo per recuperare un elenco di indirizzi IP e porte dei pod (endpoint) ad esso associati.

Tutti i servizi si basano sul servizio headless.

Il servizio ClusterIP è un servizio headless con alcune aggiunte: 

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

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

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

Se la tua applicazione è già distribuita, questa attività potrebbe sembrare impossibile. Tuttavia, esiste un'opzione alternativa.

Service Mesh ti aiuterà

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

All'avvio dell'applicazione:

  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.

Una volta che l'applicazione vuole effettuare una richiesta,:

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

Questi passaggi funzionano sia per le connessioni WebSocket, gRPC che AMQP.

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

Tuttavia, puoi utilizzare invece mesh di servizi come Istio o Linkerd.

Service Mesh potenzia la tua applicazione con un processo che:

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

Service Mesh aiuta a gestire il traffico all'interno del cluster, ma richiede un uso intensivo delle risorse. Altre opzioni utilizzano librerie di terze parti come Netflix Ribbon o proxy programmabili come Envoy.

Cosa succede se ignori i problemi di bilanciamento?

Puoi scegliere di non utilizzare il bilanciamento del carico e comunque non notare alcun cambiamento. Diamo un'occhiata ad alcuni scenari di lavoro.

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

Diciamo che ci sono cinque client che si connettono a due server. Anche in assenza di bilanciamento verranno utilizzati entrambi i server:

Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

Le connessioni potrebbero non essere distribuite equamente: forse quattro client connessi allo stesso server, ma ci sono buone probabilità che vengano utilizzati entrambi i server.

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

Se disponi di meno client e più server, le tue risorse potrebbero essere sottoutilizzate e potrebbe verificarsi un potenziale collo di bottiglia.

Diciamo che ci sono due client e cinque server. Nel migliore dei casi ci saranno due connessioni permanenti a due server su cinque.

I restanti server saranno inattivi:

Bilanciamento del carico e scalabilità delle connessioni di lunga durata in Kubernetes

Se questi due server non sono in grado di gestire le richieste dei client, il ridimensionamento orizzontale non sarà di aiuto.

conclusione

I servizi Kubernetes sono progettati per funzionare nella maggior parte degli scenari di applicazioni Web standard.

Tuttavia, una volta che si inizia a lavorare con protocolli applicativi che utilizzano connessioni TCP persistenti, come database, gRPC o WebSocket, i servizi non sono più adatti. Kubernetes non fornisce meccanismi interni per bilanciare le connessioni TCP persistenti.

Ciò significa che è necessario scrivere applicazioni tenendo presente il bilanciamento lato client.

Traduzione preparata dal team Kubernetes aaS da Mail.ru.

Cos'altro leggere sull'argomento:

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

Fonte: habr.com

Aggiungi un commento