«Kubernetes ha aumentato la latenza di 10 volte»: chi è il colpevole?

Nota del traduttore.: Questo articolo, scritto da Galo Navarro, che ricopre il ruolo di Principal Software Engineer in Adevinta, un'azienda europea, è un'affascinante e istruttiva "investigazione" nell'ambito dell'infrastruttura operativa. Il suo titolo originale è stato leggermente modificato nella traduzione per una ragione che l'autore spiega all'inizio.

«Kubernetes ha aumentato la latenza di 10 volte»: chi è il colpevole?

Nota dell'autore: Sembra che questa pubblicazione abbia attirato molta più attenzione di quanto ci si aspettasse. Continuo a ricevere commenti arrabbiati riguardo al fatto che il titolo dell'articolo è fuorviante e che alcuni lettori sono delusi. Comprendo le motivazioni alla base di ciò, quindi, nonostante il rischio di rovinare l'intera suspense, voglio spiegare subito di cosa tratta questo articolo. Durante la transizione dei team a Kubernetes ho notato una cosa interessante: ogni volta che si presenta un problema (ad esempio, un aumento della latenza dopo la migrazione), si accusa per prima cosa Kubernetes, ma poi si scopre che l'orchestratore, in fin dei conti, non c'entra. Questo articolo racconta uno di questi casi. Il suo titolo riprende l'esclamazione di uno dei nostri sviluppatori (poi vi renderete conto che Kubernetes non c’entra affatto). Non troverete rivelazioni inaspettate su Kubernetes, ma potete aspettarvi un paio di buone lezioni su sistemi complessi.

Un paio di settimane fa, il mio team si è occupato della migrazione di un microservizio sulla piattaforma principale, che include CI/CD, un ambiente basato su Kubernetes, metriche e altre utilità. La migrazione era di prova: avevamo intenzione di prenderla come base e trasferire altri circa 150 servizi nei prossimi mesi. Tutti questi servizi gestiscono alcune delle più grandi piattaforme online in Spagna (Infojobs, Fotocasa, ecc.).

Dopo aver distribuito l'applicazione su Kubernetes e diretto verso di essa parte del traffico, ci aspettava una sorpresa inquietante. La latenza (latency) delle richieste su Kubernetes era dieci volte superiore a quella su EC2. In sostanza, era necessario trovare una soluzione a questo problema o rinunciare alla migrazione del microservizio (e, forse, all'intero progetto).

Perché la latenza su Kubernetes è così alta rispetto a EC2?

Per identificare il collo di bottiglia, abbiamo raccolto metriche lungo tutto il percorso della richiesta. La nostra architettura è semplice: il gateway API (Zuul) fa da proxy per le richieste agli istanze del microservizio su EC2 o Kubernetes. Su Kubernetes utilizziamo il NGINX Ingress Controller, mentre i backend sono normali oggetti di tipo Deployment con un'applicazione JVM sulla piattaforma Spring.

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

Sembrava che il problema fosse legato alla latenza nelle prime fasi di elaborazione del backend (ho contrassegnato la parte problematica nel grafico come «xx»). Su EC2, la risposta dell'applicazione impiegava circa 20 ms. Su Kubernetes, la latenza saliva a 100-200 ms.

Abbiamo rapidamente escluso i sospetti legati al cambio dell'ambiente di esecuzione. La versione della JVM è rimasta invariata. Anche i problemi di containerizzazione non erano in causa: l'applicazione funzionava già con successo all'interno di container su EC2. Sovraccarico? Ma abbiamo osservato alte latenze anche con una richiesta al secondo. Anche le pause per la raccolta dei rifiuti potevano essere trascurate.

Uno dei nostri amministratori Kubernetes ha chiesto se l'applicazione avesse dipendenze esterne, dato che in passato le richieste DNS avevano causato problemi simili.

Ipotesi 1: risoluzione dei nomi DNS

Ad ogni richiesta, la nostra applicazione si rivolge da una a tre volte a un'istanza di AWS Elasticsearch in un dominio del tipo elastic.spain.adevinta.com. All'interno dei container abbiamo una shell, quindi possiamo verificare se la ricerca del dominio impiega davvero molto tempo.

Richieste DNS dal container:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

Richieste simili da una delle istanze EC2 su cui gira l'applicazione:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

Considerato che la ricerca richiede circa 30 ms, è diventato chiaro che la risoluzione DNS quando si accede a Elasticsearch contribuiva realmente all'aumento della latenza.

Tuttavia, questo era strano per due motivi:

  1. Abbiamo già molte applicazioni in Kubernetes che interagiscono con le risorse AWS, ma non accusano grandi latenze. Qualunque sia il motivo, è specifico di questo caso.
  2. Sappiamo che la JVM implementa la cache DNS in memoria. Nei nostri immagini, il valore TTL è impostato a $JAVA_HOME/jre/lib/security/java.security ed è fissato a 10 secondi: networkaddress.cache.ttl = 10. In altre parole, la JVM deve memorizzare nella cache tutte le richieste DNS per 10 secondi.

Per confermare la prima ipotesi, abbiamo deciso di sospendere temporaneamente le chiamate DNS e vedere se il problema scomparisse. Prima abbiamo deciso di riconfigurare l'applicazione affinché si connettesse direttamente a Elasticsearch via indirizzo IP, piuttosto che tramite nome di dominio. Questo avrebbe richiesto una modifica del codice e un nuovo deploy, quindi abbiamo semplicemente mappato il dominio al suo indirizzo IP in /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Ora il container riceveva l'IP praticamente immediatamente. Questo ha portato a un certo miglioramento, ma ci siamo avvicinati solo leggermente al livello di latenza previsto. Sebbene la risoluzione DNS richiedesse molto tempo, la vera causa continuava a sfuggirci.

Diagnosi di rete

Abbiamo deciso di analizzare il traffico dal container utilizzando tcpdump, per tracciare cosa succede effettivamente in rete:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

Poi abbiamo inviato alcune richieste e scaricato il loro capture (kubectl cp my-service:/capture.pcap capture.pcap) per ulteriori analisi in Wireshark.

Non c'era nulla di sospetto nelle richieste DNS (eccetto un piccolo dettaglio di cui parlerò più tardi). Ma c'erano alcune stranezze nel modo in cui il nostro servizio gestiva ogni richiesta. Di seguito è riportato uno screenshot del capture che mostra la ricezione della richiesta prima dell'invio della risposta:

«Kubernetes ha aumentato la latenza di 10 volte»: chi è il colpevole?

I numeri dei pacchetti sono riportati nella prima colonna. Per chiarezza, ho evidenziato in colori diversi i vari flussi TCP.

Il flusso verde, iniziando dal pacchetto 328, mostra come il client (172.17.22.150) ha stabilito una connessione TCP con il container (172.17.36.147). Dopo il handshake iniziale (328-330), il pacchetto 331 ha portato HTTP GET /v1/.. — una richiesta in arrivo al nostro servizio. L'intero processo è durato 1 ms.

Il flusso grigio (dal pacchetto 339) mostra che il nostro servizio ha inviato una richiesta HTTP all'istanza di Elasticsearch (l'handshake TCP è assente poiché viene utilizzata una connessione esistente). Questo ha richiesto 18 ms.

Finora tutto ok, e i tempi sono all'incirca corrispondenti alle latenze previste (20-30 ms in misurazioni dal client).

Tuttavia, la sezione blu richiede 86 ms. Cosa succede lì? Dal pacchetto 333, il nostro servizio ha inviato una richiesta HTTP GET a /latest/meta-data/iam/security-credentials, e subito dopo, sulla stessa connessione TCP, un'altra richiesta GET a /latest/meta-data/iam/security-credentials/arn:...

Abbiamo scoperto che questo si ripete con ogni richiesta nell'intera tracciatura. La risoluzione DNS è effettivamente un po' più lenta nei nostri container (la spiegazione di questo fenomeno è molto interessante, ma la riserverò per un articolo separato). Si è scoperto che la causa delle elevate latenze sono le chiamate al servizio AWS Instance Metadata ad ogni richiesta.

Ipotesi 2: richieste in eccesso a AWS

Entrambi gli endpoint appartengono a AWS Instance Metadata API.Il nostro microservizio utilizza questo servizio mentre lavora con Elasticsearch. Entrambe le chiamate fanno parte del processo di autorizzazione di base. L'endpoint a cui viene effettuata la richiesta alla prima chiamata restituisce il ruolo IAM associato all'istanza.

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

La seconda richiesta si rivolge al secondo endpoint per ottenere permessi temporanei per questa istanza:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

Il client può utilizzarli per un breve periodo di tempo e deve periodicamente ottenere nuovi certificati (fino alla loro Scadenza). Il modello è semplice: AWS ruota frequentemente le chiavi temporanee per motivi di sicurezza, ma i client possono memorizzarle in cache per alcuni minuti, compensando il calo delle prestazioni associato all'ottenimento di nuovi certificati.

L'AWS Java SDK dovrebbe occuparsi di gestire questo processo, ma per qualche motivo non lo fa.

Cercando tra le questioni su GitHub, ci siamo imbattuti in un problema. #1921Ci ha aiutato a identificare la direzione in cui continuare a 'scavare'.

L'AWS SDK aggiorna i certificati quando si verifica una delle seguenti condizioni:

  • La scadenza (Scadenza) rientra nel EXPIRATION_THRESHOLD, fissato nel codice a 15 minuti.
  • È trascorso più tempo dall'ultima tentativo di aggiornare i certificati rispetto a REFRESH_THRESHOLD, impostato a 60 minuti.

Per osservare la reale scadenza dei certificati che riceviamo, abbiamo eseguito i comandi cURL sopra riportati dal container e dall'istanza EC2. La durata del certificato ricevuto dal container si è rivelata molto più breve: esattamente 15 minuti.

Ora era chiaro: per la prima richiesta, il nostro servizio riceveva certificati temporanei. Poiché la loro durata non superava i 15 minuti, nella richiesta successiva l'AWS SDK decideva di aggiornarli. E ciò accadeva con ogni richiesta.

Perché la durata dei certificati è diventata più breve?

Il servizio AWS Instance Metadata è progettato per funzionare con le istanze EC2, non con Kubernetes. D'altra parte, non volevamo modificare l'interfaccia delle applicazioni. Per questo motivo abbiamo utilizzato KIAM — uno strumento che consente agli utenti (ingegneri che distribuiscono applicazioni nel cluster) di assegnare ruoli IAM ai contenitori nei pod come se fossero istanze EC2, utilizzando agenti su ogni nodo Kubernetes. KIAM intercetta le chiamate al servizio AWS Instance Metadata e le gestisce dalla sua cache, avendole precedentemente ricevute da AWS. Dal punto di vista dell'applicazione, nulla cambia.

KIAM fornisce certificati temporanei ai pod. Questo è ragionevole, considerando che la durata media di un pod è inferiore a quella di un'istanza EC2. Per impostazione predefinita, la durata di validità dei certificati è di 15 minuti.

Alla fine, se sovrapponiamo entrambi i valori predefiniti, si presenta un problema. Ogni certificato fornito all'applicazione scade dopo 15 minuti. Nel frattempo, l'AWS Java SDK forza l'aggiornamento di qualsiasi certificato che scade entro meno di 15 minuti.

Di conseguenza, il certificato temporaneo viene forzato a essere aggiornato con ogni richiesta, il che comporta diverse chiamate all'API AWS e porta a un significativo aumento della latenza. Abbiamo scoperto nell'AWS Java SDK feature request, che menziona un problema simile.

La soluzione si è rivelata semplice. Abbiamo semplicemente riconfigurato KIAM per richiedere certificati con una durata più lunga. Una volta fatto ciò, le richieste sono passate senza l'intervento del servizio AWS Metadata, e la latenza è scesa anche a livelli più bassi rispetto a EC2.

Conclusioni

Dalla nostra esperienza con le migrazioni, possiamo dire che una delle fonti più comuni di problemi non è dovuta a errori in Kubernetes o in altri elementi della piattaforma. Non sono nemmeno legati a difetti fondamentali nei microservizi che stiamo migrando. I problemi sorgono spesso semplicemente perché mettiamo insieme diversi elementi.

Mescoliamo sistemi complessi che non hanno mai interagito in precedenza, aspettandoci che insieme formino un'unica, più grande sistema. Purtroppo, più elementi ci sono, maggiore è spazio per gli errori, maggiore è l'entropia.

Nel nostro caso, l'alta latenza non era il risultato di errori o scelte sbagliate in Kubernetes, KIAM, AWS Java SDK o nel nostro microservizio. Era il risultato della combinazione di due parametri indipendenti impostati per impostazione predefinita: uno in KIAM, l'altro nell'AWS Java SDK. Separatamente, entrambi i parametri hanno senso: sia la politica attiva di aggiornamento dei certificati nell'AWS Java SDK, sia la breve durata dei certificati in KIAM. Ma se li uniamo, i risultati diventano imprevedibili. Due soluzioni indipendenti e logiche non devono necessariamente avere senso quando vengono unite.

P.S. dal traduttore

Per ulteriori informazioni sull'architettura dello strumento KIAM per l'integrazione AWS IAM con Kubernetes, puoi consultare il questo articolo dal suo creatore.

E nel nostro blog leggi anche:

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