“Kubernetes ha aumentato la latenza di 10 volte”: di chi è la colpa?

Nota. trad.: Questo articolo, scritto da Galo Navarro, che ricopre la posizione di Principal Software Engineer presso la società europea Adevinta, è un'affascinante e istruttiva "indagine" nel campo delle operazioni infrastrutturali. Il titolo originale è stato leggermente ampliato nella traduzione per un motivo che l'autore spiega all'inizio.

“Kubernetes ha aumentato la latenza di 10 volte”: di chi è la colpa?

Nota dell'autore: Sembra questo post attratto molta più attenzione del previsto. Ricevo ancora commenti arrabbiati secondo cui il titolo dell'articolo è fuorviante e alcuni lettori sono rattristati. Capisco le ragioni di quanto sta accadendo, quindi, nonostante il rischio di rovinare l'intero intrigo, voglio dirvi subito di cosa tratta questo articolo. Una cosa curiosa che ho visto durante la migrazione dei team a Kubernetes è che ogni volta che si verifica un problema (come una maggiore latenza dopo una migrazione), la prima cosa che viene incolpata è Kubernetes, ma poi si scopre che l'orchestratore non è realmente responsabile. colpa. Questo articolo racconta uno di questi casi. Il suo nome ripete l'esclamazione di uno dei nostri sviluppatori (più tardi vedrai che Kubernetes non c'entra niente). Qui non troverai rivelazioni sorprendenti su Kubernetes, ma puoi aspettarti un paio di buone lezioni sui sistemi complessi.

Un paio di settimane fa, il mio team stava migrando un singolo microservizio su una piattaforma principale che includeva CI/CD, un runtime basato su Kubernetes, metriche e altre funzionalità. Il trasloco era di natura sperimentale: prevedevamo di prenderlo come base e di trasferire circa altri 150 servizi nei prossimi mesi. Tutti loro sono responsabili del funzionamento di alcune delle più grandi piattaforme online in Spagna (Infojobs, Fotocasa, ecc.).

Dopo aver distribuito l'applicazione su Kubernetes e reindirizzato parte del traffico su di essa, ci attendeva una sorpresa allarmante. Ritardo (latenza) le richieste in Kubernetes erano 10 volte superiori rispetto a EC2. In generale, era necessario trovare una soluzione a questo problema o abbandonare la migrazione del microservizio (e, possibilmente, dell'intero progetto).

Perché la latenza è molto più alta in Kubernetes che in EC2?

Per individuare il collo di bottiglia, abbiamo raccolto parametri lungo l'intero percorso della richiesta. La nostra architettura è semplice: un gateway API (Zuul) inoltra le richieste alle istanze di microservizi in EC2 o Kubernetes. In Kubernetes utilizziamo NGINX Ingress Controller e i backend sono oggetti ordinari come Distribuzione con un'applicazione JVM sulla piattaforma Spring.

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

Il problema sembrava essere correlato alla latenza iniziale nel backend (ho contrassegnato l'area problematica sul grafico come "xx"). Su EC2, la risposta dell'applicazione ha richiesto circa 20 ms. In Kubernetes la latenza è aumentata a 100-200 ms.

Abbiamo rapidamente allontanato i probabili sospetti legati al cambio di runtime. La versione JVM rimane la stessa. Anche i problemi di containerizzazione non c'entravano nulla: l'applicazione funzionava già con successo nei container su EC2. Caricamento? Ma abbiamo osservato latenze elevate anche a 1 richiesta al secondo. Anche le pause per la raccolta dei rifiuti potrebbero essere trascurate.

Uno dei nostri amministratori Kubernetes si chiedeva se l'applicazione avesse dipendenze esterne perché le query DNS avevano causato problemi simili in passato.

Ipotesi 1: risoluzione dei nomi DNS

Per ogni richiesta, la nostra applicazione accede a un'istanza AWS Elasticsearch da una a tre volte in un dominio come elastic.spain.adevinta.com. All'interno dei nostri contenitori c'è una conchiglia, così possiamo verificare se la ricerca di un dominio richiede effettivamente molto tempo.

Query DNS dal contenitore:

[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 in cui è in esecuzione 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

Considerando che la ricerca ha richiesto circa 30 ms, è apparso chiaro che la risoluzione DNS durante l’accesso a Elasticsearch stava effettivamente contribuendo all’aumento della latenza.

Tuttavia, questo era strano per due motivi:

  1. Disponiamo già di tantissime applicazioni Kubernetes che interagiscono con le risorse AWS senza soffrire di latenza elevata. Qualunque sia il motivo, si riferisce specificamente a questo caso.
  2. Sappiamo che la JVM esegue la memorizzazione nella cache DNS in memoria. Nelle nostre immagini è scritto il valore TTL $JAVA_HOME/jre/lib/security/java.security e impostato su 10 secondi: networkaddress.cache.ttl = 10. In altre parole, la JVM dovrebbe memorizzare nella cache tutte le query DNS per 10 secondi.

Per confermare la prima ipotesi, abbiamo deciso di sospendere per un po' le chiamate DNS e vedere se il problema si risolveva. Innanzitutto, abbiamo deciso di riconfigurare l'applicazione in modo che comunicasse direttamente con Elasticsearch tramite indirizzo IP, anziché tramite un nome di dominio. Ciò richiederebbe modifiche al codice e una nuova distribuzione, quindi abbiamo semplicemente mappato il dominio al suo indirizzo IP /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Ora il contenitore ha ricevuto un IP quasi istantaneamente. Ciò ha comportato qualche miglioramento, ma eravamo solo leggermente più vicini ai livelli di latenza attesi. Sebbene la risoluzione DNS abbia richiesto molto tempo, la vera ragione continuava a sfuggirci.

Diagnostica tramite rete

Abbiamo deciso di analizzare il traffico dal contenitore utilizzando tcpdumpper vedere cosa sta succedendo esattamente sulla rete:

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

Abbiamo quindi inviato diverse richieste e scaricato la loro acquisizione (kubectl cp my-service:/capture.pcap capture.pcap) per ulteriori analisi in Wireshark.

Non c'era nulla di sospetto nelle query DNS (tranne una piccola cosa di cui parlerò più avanti). Ma c'erano alcune stranezze nel modo in cui il nostro servizio gestiva ogni richiesta. Di seguito è riportato uno screenshot dell'acquisizione che mostra la richiesta accettata prima dell'inizio della risposta:

“Kubernetes ha aumentato la latenza di 10 volte”: di chi è la colpa?

I numeri dei pacchi sono mostrati nella prima colonna. Per chiarezza, ho codificato a colori i diversi flussi TCP.

Il flusso verde che inizia con il pacchetto 328 mostra come il client (172.17.22.150) ha stabilito una connessione TCP al contenitore (172.17.36.147). Dopo la stretta di mano iniziale (328-330), è stato portato il pacco 331 HTTP GET /v1/.. — una richiesta in arrivo al nostro servizio. L'intero processo ha richiesto 1 ms.

Il flusso grigio (dal pacchetto 339) mostra che il nostro servizio ha inviato una richiesta HTTP all'istanza Elasticsearch (non esiste un handshake TCP perché utilizza una connessione esistente). Ci sono voluti 18 ms.

Finora va tutto bene e i tempi corrispondono grosso modo ai ritardi previsti (20-30 ms se misurati dal client).

Tuttavia, la sezione blu dura 86 ms. Cosa sta succedendo? Con il 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 ciò si ripeteva con ogni richiesta durante la traccia. La risoluzione DNS è infatti un po' più lenta nei nostri contenitori (la spiegazione di questo fenomeno è piuttosto interessante, ma la salverò per un articolo a parte). Si è scoperto che la causa dei lunghi ritardi erano le chiamate al servizio AWS Instance Metadata su ciascuna richiesta.

Ipotesi 2: chiamate non necessarie ad AWS

Entrambi gli endpoint appartengono a API dei metadati dell'istanza AWS. Il nostro microservizio utilizza questo servizio durante l'esecuzione di Elasticsearch. Entrambe le chiamate fanno parte del processo di autorizzazione di base. L'endpoint a cui si accede alla prima richiesta emette 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 richiede al secondo endpoint autorizzazioni temporanee 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 cliente può utilizzarli per un breve periodo di tempo e deve periodicamente ottenere nuovi certificati (prima che siano Expiration). Il modello è semplice: AWS ruota frequentemente le chiavi temporanee per motivi di sicurezza, ma i clienti possono memorizzarle nella cache per alcuni minuti per compensare la penalizzazione delle prestazioni associata all'ottenimento di nuovi certificati.

L'SDK Java AWS dovrebbe assumersi la responsabilità dell'organizzazione di questo processo, ma per qualche motivo ciò non accade.

Dopo aver cercato i problemi su GitHub, abbiamo riscontrato un problema #1921. Ci ha aiutato a determinare la direzione in cui “scavare” ulteriormente.

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

  • Data di scadenza (Expiration) Cadere in EXPIRATION_THRESHOLD, codificato a 15 minuti.
  • È trascorso più tempo dall'ultimo tentativo di rinnovare i certificati REFRESH_THRESHOLD, hardcoded per 60 minuti.

Per vedere la data di scadenza effettiva dei certificati che riceviamo, abbiamo eseguito i comandi cURL sopra sia dal container che dall'istanza EC2. Il periodo di validità del certificato ricevuto dal container si è rivelato molto più breve: esattamente 15 minuti.

Adesso tutto è diventato chiaro: per la prima richiesta il nostro servizio ha ricevuto certificati temporanei. Poiché non erano validi per più di 15 minuti, l'SDK AWS decideva di aggiornarli in una richiesta successiva. E questo è successo con ogni richiesta.

Perché il periodo di validità dei certificati è stato ridotto?

I metadati delle istanze AWS sono progettati per funzionare con le istanze EC2, non con Kubernetes. D'altra parte non volevamo cambiare l'interfaccia dell'applicazione. Per questo abbiamo usato KIAM - uno strumento che, utilizzando agenti su ciascun nodo Kubernetes, consente agli utenti (ingegneri che distribuiscono applicazioni in un cluster) di assegnare ruoli IAM ai contenitori nei pod come se fossero istanze EC2. KIAM intercetta le chiamate al servizio AWS Instance Metadata e le elabora dalla sua cache, dopo averle precedentemente ricevute da AWS. Dal punto di vista applicativo non cambia nulla.

KIAM fornisce certificati a breve termine ai pod. Ciò ha senso considerando che la vita media di un pod è inferiore a quella di un’istanza EC2. Periodo di validità predefinito per i certificati pari agli stessi 15 minuti.

Di conseguenza, se si sovrappongono entrambi i valori predefiniti uno sopra l'altro, si verifica un problema. Ogni certificato fornito a un'applicazione scade dopo 15 minuti. Tuttavia, l'SDK AWS Java impone il rinnovo di qualsiasi certificato che abbia meno di 15 minuti rimasti prima della data di scadenza.

Di conseguenza, il certificato temporaneo è costretto a rinnovarsi ad ogni richiesta, il che comporta un paio di chiamate all'API AWS e provoca un aumento significativo della latenza. Nell'SDK Java AWS abbiamo trovato richiesta di funzionalità, che menziona un problema simile.

La soluzione si è rivelata semplice. Abbiamo semplicemente riconfigurato KIAM per richiedere certificati con un periodo di validità più lungo. Una volta accaduto ciò, le richieste hanno iniziato a fluire senza la partecipazione del servizio AWS Metadata e la latenza è scesa a livelli ancora più bassi rispetto a EC2.

risultati

In base alla nostra esperienza con le migrazioni, una delle fonti di problemi più comuni non sono i bug in Kubernetes o altri elementi della piattaforma. Inoltre, non risolve alcun difetto fondamentale nei microservizi di cui stiamo effettuando il porting. Spesso i problemi sorgono semplicemente perché mettiamo insieme elementi diversi.

Mescoliamo insieme sistemi complessi che non hanno mai interagito tra loro prima, aspettandoci che insieme formino un unico sistema più grande. Purtroppo, maggiore è il numero degli elementi, maggiore è il margine di errore, maggiore è l'entropia.

Nel nostro caso, l'elevata latenza non è stata il risultato di bug o decisioni sbagliate in Kubernetes, KIAM, AWS Java SDK o nel nostro microservizio. È stato il risultato della combinazione di due impostazioni predefinite indipendenti: una in KIAM, l'altra nell'SDK Java AWS. Presi separatamente, entrambi i parametri hanno senso: la policy di rinnovo dei certificati attiva nell'SDK Java AWS e il breve periodo di validità dei certificati in KAIM. Ma quando li metti insieme, i risultati diventano imprevedibili. Due soluzioni indipendenti e logiche non devono necessariamente avere senso se combinate.

PS da traduttore

Puoi trovare ulteriori informazioni sull'architettura dell'utilità KIAM per l'integrazione di AWS IAM con Kubernetes all'indirizzo questo articolo dai suoi creatori.

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento