Nota di traduzione.: Questo articolo, scritto da Galo Navarro, Principal Software Engineer nella società europea Adevinta, è un affascinante e istruttivo «inchiesta» nel campo dell'infrastruttura. Il titolo originale è stato leggermente modificato nella traduzione per motivi che l'autore spiega all'inizio.

Nota dell'autore: Sembra che questa pubblicazione molto più attenzione di quanto ci si aspettasse. Ricevo ancora commenti arrabbiati sul fatto che il titolo dell'articolo sia fuorviante e che alcuni lettori siano delusi. Capisco le ragioni di ciò che sta accadendo, quindi, nonostante il rischio di rovinare tutta l'intrigante storia, voglio subito rivelare di cosa tratta questo articolo. Nel passaggio dei team a Kubernetes, ho notato una cosa curiosa: ogni volta che si presenta un problema (ad esempio, un aumento della latenza dopo la migrazione), la prima cosa su cui si punta il dito è Kubernetes, ma poi si scopre che l'orchestratore, in effetti, non c'entra nulla. Questo articolo racconta di uno di questi casi. Il suo titolo ripete l'esclamazione di uno dei nostri sviluppatori (poi vi renderete conto che Kubernetes non è affatto il responsabile). In esso non troverete rivelazioni inaspettate su Kubernetes, ma potete contare su un paio di buone lezioni riguardanti sistemi complessi.
Qualche settimana fa, il mio team si è occupato della migrazione di un microservizio sulla piattaforma principale, che include CI/CD, un ambiente di lavoro basato su Kubernetes, metriche e altre utilità. Il trasferimento è stato di prova: prevedevamo di utilizzarlo come base e migrare ulteriormente circa 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 in Kubernetes e reindirizzato parte del traffico, ci aspettava una sorpresa allarmante. La latenza (latency) delle richieste in Kubernetes era 10 volte superiore rispetto a EC2. In generale, era necessario trovare una soluzione a questo problema oppure rinunciare alla migrazione del microservizio (e, eventualmente, all'intero progetto).
Perché la latenza in Kubernetes è così superiore rispetto a EC2?
Per identificare il collo di bottiglia, abbiamo raccolto metriche lungo l'intero percorso della richiesta. La nostra architettura è semplice: il gateway API (Zuul) inoltra le richieste alle istanze del microservizio in EC2 o Kubernetes. In Kubernetes utilizziamo il Controller Ingress NGINX, mentre i backend sono normali oggetti di tipo con applicazioni JVM sulla piattaforma Spring.
EC2
+---------------+
| +---------+ |
| | | |
+-------> BACKEND | |
| | | | |
| | +---------+ |
| +---------------+
+------+ |
Pubblico | | |
-------> ZUUL +--+
traffico | | | Kubernetes
+------+ | +-----------------------------+
| | +-------+ +---------+ |
| | | | xx | | |
+-------> NGINX +------> BACKEND | |
| | | xx | | |
| +-------+ +---------+ |
+-----------------------------+Sembra che il problema sia dovuto al ritardo nei primi stadi di funzionamento del backend (ho contrassegnato la sezione problematica nel grafico come «xx»). In EC2 la risposta dell'applicazione richiedeva circa 20 ms. In Kubernetes, il ritardo arrivava a 100—200 ms.
Abbiamo rapidamente escluso i potenziali sospetti legati al cambiamento dell'ambiente di esecuzione. La versione della JVM è rimasta la stessa. Anche i problemi di containerizzazione non erano in discussione: l'applicazione già funzionava con successo nei container in EC2. Il carico? Ma abbiamo osservato alti ritardi anche con una richiesta al secondo. Anche le pause per la raccolta dei rifiuti erano trascurabili.
Uno dei nostri amministratori di Kubernetes ha chiesto se l'applicazione avesse dipendenze esterne, poiché in passato le richieste DNS avevano causato problemi simili.
Ipotesi 1: risoluzione dei nomi DNS
Ad ogni richiesta, la nostra applicazione contatta un'istanza di AWS Elasticsearch da una a tre volte in un dominio simile a elastic.spain.adevinta.com. All'interno dei container abbiamo , quindi possiamo controllare se effettivamente la ricerca del dominio richiede molto tempo.
Richieste 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 msecRichieste simili da una delle istanze EC2, dove 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 msecDati che la ricerca richiede circa 30 ms, è diventato chiaro che la risoluzione DNS durante l'accesso a Elasticsearch contribuisce effettivamente ad aumentare la latenza.
Tuttavia, questo era strano per due motivi:
- Abbiamo già molte applicazioni in Kubernetes che interagiscono con le risorse AWS, ma non soffrono di latenze elevate. Qualunque sia la ragione, riguarda specificamente questo caso.
- Sappiamo che la JVM esegue la memorizzazione nella cache DNS in memoria. Nei nostri container, il valore TTL è specificato in
$JAVA_HOME/jre/lib/security/java.securityed è impostato su 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 rinunciare temporaneamente alle richieste DNS e vedere se il problema scomparisse. Inizialmente abbiamo deciso di riconfigurare l'applicazione in modo che si collegasse a Elasticsearch direttamente tramite indirizzo IP, piuttosto che attraverso il nome di dominio. Ciò avrebbe richiesto modifiche al codice e un nuovo deployment, quindi abbiamo semplicemente associato il dominio al suo indirizzo IP in /etc/hosts:
34.55.5.111 elastic.spain.adevinta.comOra il container riceveva l'IP quasi istantaneamente. Questo ha portato a un miglioramento, ma ci siamo avvicinati solo leggermente al livello di latenza previsto. Anche se la risoluzione DNS richiedeva molto tempo, la vera causa ci sfuggiva ancora.
Diagnostica attraverso la rete
Abbiamo deciso di analizzare il traffico dal container utilizzando tcpdump, per monitorare cosa stesse succedendo sulla rete:
[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap Poi abbiamo inviato alcune richieste e scaricato la loro cattura (kubectl cp my-service:/capture.pcap capture.pcap) per ulteriori analisi in .
Non c'era nulla di sospetto nelle richieste DNS (tranne per un piccolo dettaglio di cui parlerò più avanti). Tuttavia, c'erano alcune stranezze nel modo in cui il nostro servizio gestiva ogni richiesta. Di seguito è riportato uno screenshot della cattura che mostra l'accettazione della richiesta prima che inizi la risposta:

I numeri dei pacchetti sono riportati nella prima colonna. Per chiarezza, ho evidenziato in colore diversi i vari flussi TCP.
Il flusso verde, che inizia dal pacchetto 328, mostra come il client (172.17.22.150) abbia stabilito una connessione TCP con il container (172.17.36.147). Dopo il primo handshake (328-330), il pacchetto 331 ha portato HTTP GET /v1/.. — richiesta in entrata 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 di Elasticsearch (il handshake TCP è assente poiché viene utilizzata una connessione già esistente). Ciò ha richiesto 18 ms.
Per ora va tutto bene, e i tempi corrispondono approssimativamente ai ritardi previsti (20-30 ms nelle misurazioni dal client).
Tuttavia, la sezione blu impiega 86 ms. Cosa sta succedendo lì? Con il pacchetto 333, il nostro servizio ha inviato una richiesta HTTP GET a /latest/meta-data/iam/security-credentials, immediatamente 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'intero tracciamento. La risoluzione DNS è effettivamente un po' più lenta nei nostri contenitori (la spiegazione di questo fenomeno è piuttosto interessante, ma la conserverò per un articolo a parte). È emerso che la causa dei lunghi ritardi sono le chiamate al servizio AWS Instance Metadata per ogni richiesta.
Ipotesi 2: chiamate superflue ad AWS
Entrambi gli endpoint appartengono a . Il nostro microservizio utilizza questo servizio durante l'interazione con Elasticsearch. Entrambi i richiami sono parte del processo di autorizzazione di base. L'endpoint a cui si accede con la prima richiesta 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_roleLa seconda richiesta accede al secondo endpoint per ottenere i 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 e deve periodicamente ottenere nuovi certificati (fino alla loro Expiration). Il modello è semplice: AWS esegue frequenti rotazioni delle chiavi temporanee per motivi di sicurezza, ma i client possono cachearle per alcuni minuti, compensando la diminuzione delle prestazioni dovuta all'ottenimento di nuovi certificati.
AWS Java SDK dovrebbe occuparsi di gestire questo processo, ma per qualche motivo non lo fa.
Cercando tra le segnalazioni su GitHub, ci siamo imbattuti nel problema . Questo ci ha aiutato a identificare la direzione in cui continuare a "scavare".
AWS SDK aggiorna i certificati al verificarsi di una delle seguenti condizioni:
- La scadenza dei certificati (
Expiration) si avvicina alEXPIRATION_THRESHOLD, fissato nel codice a 15 minuti. - È trascorso più tempo dalla mia ultima tentativo di aggiornare i certificati rispetto a
REFRESH_THRESHOLD, fissato a 60 minuti.
Per visualizzare la data di scadenza effettiva dei certificati che riceviamo, abbiamo eseguito i comandi cURL indicati sopra da un contenitore e da un'istanza EC2. La durata del certificato ottenuto dal contenitore si è rivelata molto più breve: esattamente 15 minuti.
Ora è tutto chiaro: per la prima richiesta, il nostro servizio riceveva certificati temporanei. Poiché la loro validità non superava i 15 minuti, alla richiesta successiva l'AWS SDK decideva di aggiornarli. E questo accadeva con ogni richiesta.
Perché la validità dei certificati è diventata più breve?
Il servizio AWS Instance Metadata è progettato per funzionare con istanze EC2, e non con Kubernetes. D'altra parte, non volevamo cambiare l'interfaccia delle applicazioni. Per questo motivo abbiamo utilizzato – uno strumento che grazie ad agenti su ogni nodo Kubernetes consente agli utenti (ingegneri che distribuiscono applicazioni nel 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 propria cache, dopo averle ottenute da AWS. Dal punto di vista dell'applicazione, non cambia nulla.
KIAM fornisce certificati temporanei ai pod. Ciò ha senso considerando che la durata media di vita di un pod è inferiore a quella di un'istanza EC2. Per impostazione predefinita, la validità dei certificati .
Di conseguenza, se sovrapponiamo entrambi i valori predefiniti, si presenta un problema. Ogni certificato fornito all'applicazione scade dopo 15 minuti. Inoltre, l'AWS Java SDK aggiorna forzatamente qualsiasi certificato che scada entro 15 minuti.
Di conseguenza, il certificato temporaneo viene aggiornato forzatamente ad ogni richiesta, il che comporta alcune chiamate all'API AWS e porta a un significativo aumento della latenza. Nell'AWS Java SDK abbiamo scoperto , in cui si menziona un problema simile.
La soluzione si è rivelata semplice. Abbiamo semplicemente riconfigurato KIAM per richiedere certificati con una durata di validità più lunga. Una volta fatto ciò, le richieste hanno iniziato a passare senza l'intervento del servizio AWS Metadata, e la latenza è scesa anche a livelli inferiori rispetto a EC2.
Conclusioni
Dalla nostra esperienza con le migrazioni, possiamo dire che una delle fonti più comuni di problemi non riguarda errori in Kubernetes o in altri elementi della piattaforma. Non è nemmeno legata a difetti fondamentali nei microservizi che stiamo migrando. I problemi sorgono spesso semplicemente perché mettiamo insieme elementi diversi.
Mescoliamo sistemi complessi che prima non hanno mai interagito tra di loro, aspettandoci che insieme formino un unico sistema più grande. Purtroppo, più elementi ci sono, maggiori sono le possibilità di errore, maggiore è l'entropia.
Nel nostro caso, l'alta latenza non è stata causata da errori o scelte sbagliate in Kubernetes, KIAM, AWS Java SDK o nel nostro microservizio. È stata il risultato della combinazione di due parametri indipendenti, impostati di default: uno in KIAM, l'altro nell'AWS Java SDK. Presi singolarmente, entrambi i parametri hanno senso: sia la politica attiva di aggiornamento dei certificati nell'AWS Java SDK, sia la breve durata di validità dei certificati in KIAM. Ma se messi insieme, i risultati diventano imprevedibili. Due decisioni indipendenti e logiche non devono necessariamente avere senso se unite.
P.S. dal traduttore
Puoi scoprire di più sull'architettura dello strumento KIAM per integrare AWS IAM con Kubernetes in da parte dei suoi creatori.
E sul nostro blog leggi anche:
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
