Un paio di anni fa Kubernetes
In sostanza, le applicazioni sperimentano una latenza di rete casuale fino a 100 ms o più, con conseguenti timeout o nuovi tentativi. Ci si aspettava che i servizi fossero in grado di rispondere alle richieste molto più velocemente di 100 ms. Ma questo è impossibile se la connessione stessa richiede così tanto tempo. Separatamente, abbiamo osservato query MySQL molto veloci che dovrebbero richiedere millisecondi e MySQL è stata completata in millisecondi, ma dal punto di vista dell'applicazione richiedente la risposta ha richiesto 100 ms o più.
È stato subito chiaro che il problema si verificava solo durante la connessione a un nodo Kubernetes, anche se la chiamata proveniva dall'esterno di Kubernetes. Il modo più semplice per riprodurre il problema è eseguire un test
Eliminare la complessità non necessaria nella catena che porta al fallimento
Riproducendo lo stesso esempio, volevamo restringere il focus del problema e rimuovere inutili livelli di complessità. Inizialmente c'erano troppi elementi nel flusso tra Vegeta e i pod Kubernetes. Per identificare un problema di rete più profondo, è necessario escluderne alcuni.
Il client (Vegeta) crea una connessione TCP con qualsiasi nodo del cluster. Kubernetes funziona come una rete sovrapposta (sopra la rete del data center esistente) che utilizza
Utilità tcpdump
nel test Vegeta si nota un ritardo durante l'handshake TCP (tra SYN e SYN-ACK). Per rimuovere questa complessità non necessaria, è possibile utilizzare hping3
per semplici “ping” con pacchetti SYN. Controlliamo se c'è un ritardo nel pacchetto di risposta, quindi ripristiniamo la connessione. Possiamo filtrare i dati per includere solo i pacchetti superiori a 100 ms e ottenere un modo più semplice per riprodurre il problema rispetto al test completo del livello 7 della rete di Vegeta. Ecco i "ping" del nodo Kubernetes che utilizzano TCP SYN/SYN-ACK sulla "porta del nodo" del servizio (30927) a intervalli di 10 ms, filtrati in base alle risposte più lente:
theojulienne@shell ~ $ sudo hping3 172.16.47.27 -S -p 30927 -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1485 win=29200 rtt=127.1 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1486 win=29200 rtt=117.0 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1487 win=29200 rtt=106.2 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1488 win=29200 rtt=104.1 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5024 win=29200 rtt=109.2 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5231 win=29200 rtt=109.2 ms
Può immediatamente fare la prima osservazione. A giudicare dai numeri di sequenza e dai tempi, è chiaro che non si tratta di congestioni una tantum. Il ritardo spesso si accumula e alla fine viene elaborato.
Successivamente, vogliamo scoprire quali componenti potrebbero essere coinvolti nel verificarsi della congestione. Forse queste sono alcune delle centinaia di regole iptables in NAT? Oppure ci sono problemi con il tunneling IPIP sulla rete? Un modo per verificarlo è testare ogni passaggio del sistema eliminandolo. Cosa succede se rimuovi NAT e logica firewall, lasciando solo la parte IPIP:
Fortunatamente, Linux semplifica l'accesso diretto al livello di overlay IP se la macchina si trova sulla stessa rete:
theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7346 win=0 rtt=127.3 ms
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7347 win=0 rtt=117.3 ms
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7348 win=0 rtt=107.2 ms
A giudicare dai risultati, il problema rimane ancora! Ciò esclude iptables e NAT. Quindi il problema è TCP? Vediamo come va un normale ping ICMP:
theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
len=28 ip=10.125.20.64 ttl=64 id=42594 icmp_seq=104 rtt=110.0 ms
len=28 ip=10.125.20.64 ttl=64 id=49448 icmp_seq=4022 rtt=141.3 ms
len=28 ip=10.125.20.64 ttl=64 id=49449 icmp_seq=4023 rtt=131.3 ms
len=28 ip=10.125.20.64 ttl=64 id=49450 icmp_seq=4024 rtt=121.2 ms
len=28 ip=10.125.20.64 ttl=64 id=49451 icmp_seq=4025 rtt=111.2 ms
len=28 ip=10.125.20.64 ttl=64 id=49452 icmp_seq=4026 rtt=101.1 ms
len=28 ip=10.125.20.64 ttl=64 id=50023 icmp_seq=4343 rtt=126.8 ms
len=28 ip=10.125.20.64 ttl=64 id=50024 icmp_seq=4344 rtt=116.8 ms
len=28 ip=10.125.20.64 ttl=64 id=50025 icmp_seq=4345 rtt=106.8 ms
len=28 ip=10.125.20.64 ttl=64 id=59727 icmp_seq=9836 rtt=106.1 ms
I risultati mostrano che il problema non è scomparso. Forse questo è un tunnel IPIP? Semplifichiamo ulteriormente il test:
Tutti i pacchetti vengono inviati tra questi due host?
theojulienne@kube-node-client ~ $ sudo hping3 172.16.47.27 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
len=46 ip=172.16.47.27 ttl=61 id=41127 icmp_seq=12564 rtt=140.9 ms
len=46 ip=172.16.47.27 ttl=61 id=41128 icmp_seq=12565 rtt=130.9 ms
len=46 ip=172.16.47.27 ttl=61 id=41129 icmp_seq=12566 rtt=120.8 ms
len=46 ip=172.16.47.27 ttl=61 id=41130 icmp_seq=12567 rtt=110.8 ms
len=46 ip=172.16.47.27 ttl=61 id=41131 icmp_seq=12568 rtt=100.7 ms
len=46 ip=172.16.47.27 ttl=61 id=9062 icmp_seq=31443 rtt=134.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9063 icmp_seq=31444 rtt=124.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9064 icmp_seq=31445 rtt=114.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9065 icmp_seq=31446 rtt=104.2 ms
Abbiamo semplificato la situazione in modo che due nodi Kubernetes si inviino reciprocamente qualsiasi pacchetto, anche un ping ICMP. Vedono ancora la latenza se l'host di destinazione è "cattivo" (alcuni peggiori di altri).
Ora l'ultima domanda: perché il ritardo si verifica solo sui server kube-node? E succede quando kube-node è il mittente o il destinatario? Fortunatamente, anche questo è abbastanza semplice da capire inviando un pacchetto da un host esterno a Kubernetes, ma con lo stesso destinatario “noto come cattivo”. Come puoi vedere, il problema non è scomparso:
theojulienne@shell ~ $ sudo hping3 172.16.47.27 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=312 win=0 rtt=108.5 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=5903 win=0 rtt=119.4 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=6227 win=0 rtt=139.9 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=7929 win=0 rtt=131.2 ms
Eseguiremo quindi le stesse richieste dal nodo kube di origine precedente all'host esterno (che esclude l'host di origine poiché il ping include sia un componente RX che TX):
theojulienne@kube-node-client ~ $ sudo hping3 172.16.33.44 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}.'
^C
--- 172.16.33.44 hping statistic ---
22352 packets transmitted, 22350 packets received, 1% packet loss
round-trip min/avg/max = 0.2/7.6/1010.6 ms
Esaminando le acquisizioni dei pacchetti di latenza, abbiamo ottenuto alcune informazioni aggiuntive. Nello specifico, il mittente (in basso) vede questo timeout, ma il destinatario (in alto) no: vedi la colonna Delta (in secondi):
Inoltre, se si osserva la differenza nell'ordine dei pacchetti TCP e ICMP (per numero di sequenza) sul lato destinatario, i pacchetti ICMP arrivano sempre nella stessa sequenza in cui sono stati inviati, ma con tempi diversi. Allo stesso tempo, i pacchetti TCP a volte si intercalano e alcuni di essi rimangono bloccati. In particolare, se si esaminano le porte dei pacchetti SYN, queste sono in ordine dal lato del mittente, ma non dal lato del destinatario.
C'è una sottile differenza nel come
Un'altra nuova osservazione: durante questo periodo vediamo ritardi ICMP su tutte le comunicazioni tra due host, ma TCP no. Questo ci dice che probabilmente la causa è legata all’hashing della coda RX: la congestione è quasi sicuramente nell’elaborazione dei pacchetti RX, non nell’invio delle risposte.
Ciò elimina l'invio di pacchetti dall'elenco delle possibili cause. Ora sappiamo che il problema dell'elaborazione dei pacchetti è sul lato di ricezione su alcuni server kube-node.
Comprendere l'elaborazione dei pacchetti nel kernel Linux
Per capire perché il problema si verifica nel ricevitore su alcuni server kube-node, diamo un'occhiata a come il kernel Linux elabora i pacchetti.
Ritornando all'implementazione tradizionale più semplice, la scheda di rete riceve il pacchetto e lo invia
Questo cambio di contesto è lento: la latenza potrebbe non essere stata evidente sulle schede di rete da 10 Mbps negli anni '90, ma sulle moderne schede 10G con un throughput massimo di 15 milioni di pacchetti al secondo, ciascun core di un piccolo server a otto core può essere interrotto a milioni di volte al secondo.
Per non dover gestire costantemente le interruzioni, molti anni fa è stato aggiunto Linux
Questo è molto più veloce, ma causa un problema diverso. Se ci sono troppi pacchetti, tutto il tempo viene dedicato all'elaborazione dei pacchetti dalla scheda di rete e i processi dello spazio utente non hanno il tempo di svuotare effettivamente queste code (lettura dalle connessioni TCP, ecc.). Alla fine le code si riempiono e iniziamo a scaricare i pacchetti. Nel tentativo di trovare un equilibrio, il kernel imposta un budget per il numero massimo di pacchetti elaborati nel contesto softirq. Una volta superato questo budget, si risveglia un thread separato ksoftirqd
(ne vedrai uno in ps
per core) che gestisce questi softirq al di fuori del normale percorso syscall/interrupt. Questo thread viene pianificato utilizzando lo scheduler del processo standard, che tenta di allocare le risorse in modo equo.
Dopo aver studiato come il kernel elabora i pacchetti, puoi vedere che esiste una certa probabilità di congestione. Se le chiamate softirq vengono ricevute meno frequentemente, i pacchetti dovranno attendere un po' di tempo per essere elaborati nella coda RX sulla scheda di rete. Ciò potrebbe essere dovuto ad alcune attività che bloccano il core del processore o a qualcos'altro che impedisce al core di eseguire softirq.
Restringere l'elaborazione al nucleo o al metodo
I ritardi di Softirq sono solo una supposizione per ora. Ma ha senso e sappiamo che stiamo vedendo qualcosa di molto simile. Quindi il prossimo passo sarà confermare questa teoria. E se è confermato, trova il motivo dei ritardi.
Torniamo ai nostri pacchetti lenti:
len=46 ip=172.16.53.32 ttl=61 id=29573 icmp_seq=1953 rtt=99.3 ms
len=46 ip=172.16.53.32 ttl=61 id=29574 icmp_seq=1954 rtt=89.3 ms
len=46 ip=172.16.53.32 ttl=61 id=29575 icmp_seq=1955 rtt=79.2 ms
len=46 ip=172.16.53.32 ttl=61 id=29576 icmp_seq=1956 rtt=69.1 ms
len=46 ip=172.16.53.32 ttl=61 id=29577 icmp_seq=1957 rtt=59.1 ms
len=46 ip=172.16.53.32 ttl=61 id=29790 icmp_seq=2070 rtt=75.7 ms
len=46 ip=172.16.53.32 ttl=61 id=29791 icmp_seq=2071 rtt=65.6 ms
len=46 ip=172.16.53.32 ttl=61 id=29792 icmp_seq=2072 rtt=55.5 ms
Come discusso in precedenza, questi pacchetti ICMP vengono sottoposti ad hashing in una singola coda NIC RX ed elaborati da un singolo core della CPU. Se vogliamo capire come funziona Linux, è utile sapere dove (su quale core della CPU) e come (softirq, ksoftirqd) vengono elaborati questi pacchetti per poter tracciare il processo.
Adesso è il momento di utilizzare strumenti che permettano di monitorare il kernel Linux in tempo reale. Qui abbiamo usato
Il piano qui è semplice: sappiamo che il kernel elabora questi ping ICMP, quindi inseriremo un hook nella funzione kernel hping3
sopra.
codice icmp_echo
trasferimenti struct sk_buff *skb
: Questo è un pacchetto con una "richiesta echo". Possiamo rintracciarlo ed estrarre la sequenza echo.sequence
(che confronta con icmp_seq
da hping3 выше
) e inviarlo allo spazio utente. È anche conveniente acquisire il nome/id del processo corrente. Di seguito sono riportati i risultati che vediamo direttamente mentre il kernel elabora i pacchetti:
TGID NOME PROCESSO PID ICMP_SEQ 0 0 swapper/11 770 0 0 swapper/11 771 0 0 swapper/11 772 0 0 swapper/11 773 0 0 swapper/11 774 20041 20086 prometheus 775 0 0 swapper/11 776 0 0 swapper/11 777 0 0 11 swapper/778 4512 4542 779 rapporti-raggi-s XNUMX
Va notato qui che nel contesto softirq
i processi che hanno effettuato chiamate di sistema appariranno come "processi" quando in realtà è il kernel che elabora in modo sicuro i pacchetti nel contesto del kernel.
Con questo strumento possiamo associare processi specifici a pacchetti specifici che mostrano un ritardo di hping3
. Rendiamolo semplice grep
su questa acquisizione per determinati valori icmp_seq
. I pacchetti che corrispondono ai valori icmp_seq di cui sopra sono stati annotati insieme al loro RTT osservato sopra (tra parentesi ci sono i valori RTT attesi per i pacchetti che abbiamo filtrato a causa di valori RTT inferiori a 50 ms):
TGID NOME PROCESSO PID ICMP_SEQ ** RTT -- 10137 10436 cadvisor 1951 10137 10436 cadvisor 1952 76 76 ksoftirqd/11 1953 ** 99ms 76 76 ksoftirqd/11 1954 ** 89ms 76 76 ksoftir qd/11 1955 ** 79ms 76 76 ksoftirqd/ 11 1956 ** 69ms 76 76 ksoftirqd/11 1957 ** 59ms 76 76 ksoftirqd/11 1958 ** (49ms) 76 76 ksoftirqd/11 1959 ** (39ms) 76 76 ksoftirqd/11 1960 ** (29ms) 76 76 ksoft irqd/ 11 1961 ** (19ms) 76 76 ksoftirqd/11 1962 ** (9ms) -- 10137 10436 cadvisor 2068 10137 10436 cadvisor 2069 76 76 ksoftirqd/11 2070 ** 75ms 76 76 ksoftirqd/11 2071**65ms 76 76 ksoftirqd/ 11 2072 ** 55ms 76 76 ksoftirqd/11 2073 ** (45ms) 76 76 ksoftirqd/11 2074 ** (35ms) 76 76 ksoftirqd/11 2075 ** (25ms) 76 76 ksoftirqd/11 2076 ** (15ms ) 76 76 ksoftirqd/11 2077 ** (5ms)
I risultati ci dicono diverse cose. Innanzitutto, tutti questi pacchetti vengono elaborati dal contesto ksoftirqd/11
. Ciò significa che per questa particolare coppia di macchine, i pacchetti ICMP sono stati sottoposti ad hashing sul core 11 del destinatario. Vediamo anche che ogni volta che si verifica un inceppamento, ci sono pacchetti che vengono elaborati nel contesto della chiamata di sistema cadvisor
. poi ksoftirqd
assume il compito ed elabora la coda accumulata: esattamente il numero di pacchetti che si è accumulato successivamente cadvisor
.
Il fatto che immediatamente prima funzioni sempre cadvisor
, implica il suo coinvolgimento nel problema. Ironicamente, lo scopo
Come per altri aspetti dei contenitori, questi sono tutti strumenti altamente avanzati e possono verificarsi problemi di prestazioni in alcune circostanze impreviste.
Cosa fa Cadvisor per rallentare la coda dei pacchetti?
Ora abbiamo una comprensione abbastanza buona di come si verifica l'arresto anomalo, quale processo lo causa e su quale CPU. Vediamo che a causa del blocco rigido, il kernel Linux non ha tempo per la pianificazione ksoftirqd
. E vediamo che i pacchetti vengono elaborati nel contesto cadvisor
. È logico presumerlo cadvisor
lancia una chiamata di sistema lenta, dopo la quale vengono elaborati tutti i pacchetti accumulati in quel momento:
Questa è una teoria, ma come verificarla? Ciò che possiamo fare è tracciare il core della CPU durante questo processo, trovare il punto in cui il numero di pacchetti supera il budget e viene chiamato ksoftirqd, quindi guardare un po' più indietro per vedere cosa stava esattamente funzionando sul core della CPU poco prima di quel punto . È come sottoporre a raggi X la CPU ogni pochi millisecondi. Apparirà qualcosa del genere:
Convenientemente, tutto questo può essere fatto con gli strumenti esistenti. Per esempio, ksoftirqd
:
# record 999 times a second, or every 1ms with some offset so not to align exactly with timers
sudo perf record -C 11 -g -F 999
# take that recording and make a simpler stack trace.
sudo perf script 2>/dev/null | ./FlameGraph/stackcollapse-perf-ordered.pl | grep ksoftir -B 100
Ecco i risultati:
(сотни следов, которые выглядят похожими)
cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_iter cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages ksoftirqd/11;ret_from_fork;kthread;kthread;smpboot_thread_fn;smpboot_thread_fn;run_ksoftirqd;__do_softirq;net_rx_action;ixgbe_poll;ixgbe_clean_rx_irq;napi_gro_receive;netif_receive_skb_internal;inet_gro_receive;bond_handle_frame;__netif_receive_skb_core;ip_rcv_finish;ip_rcv;ip_forward_finish;ip_forward;ip_finish_output;nf_iterate;ip_output;ip_finish_output2;__dev_queue_xmit;dev_hard_start_xmit;ipip_tunnel_xmit;ip_tunnel_xmit;iptunnel_xmit;ip_local_out;dst_output;__ip_local_out;nf_hook_slow;nf_iterate;nf_conntrack_in;generic_packet;ipt_do_table;set_match_v4;ip_set_test;hash_net4_kadt;ixgbe_xmit_frame_ring;swiotlb_dma_mapping_error;hash_net4_test ksoftirqd/11;ret_from_fork;kthread;kthread;smpboot_thread_fn;smpboot_thread_fn;run_ksoftirqd;__do_softirq;net_rx_action;gro_cell_poll;napi_gro_receive;netif_receive_skb_internal;inet_gro_receive;__netif_receive_skb_core;ip_rcv_finish;ip_rcv;ip_forward_finish;ip_forward;ip_finish_output;nf_iterate;ip_output;ip_finish_output2;__dev_queue_xmit;dev_hard_start_xmit;dev_queue_xmit_nit;packet_rcv;tpacket_rcv;sch_direct_xmit;validate_xmit_skb_list;validate_xmit_skb;netif_skb_features;ixgbe_xmit_frame_ring;swiotlb_dma_mapping_error;__dev_queue_xmit;dev_hard_start_xmit;__bpf_prog_run;__bpf_prog_run
Ci sono molte cose qui, ma la cosa principale è che troviamo il modello "cadvisor prima di ksoftirqd" che abbiamo visto in precedenza nel tracciante ICMP. Cosa significa?
Ogni riga è una traccia della CPU in un determinato momento. Ogni chiamata nello stack su una riga è separata da un punto e virgola. Nel mezzo delle righe vediamo la chiamata di sistema chiamata: read(): .... ;do_syscall_64;sys_read; ...
. Quindi Cadvisor dedica molto tempo alla chiamata di sistema read()
legati alle funzioni mem_cgroup_*
(inizio dello stack di chiamate/fine della riga).
È scomodo vedere nel tracciamento di una chiamata cosa viene letto esattamente, quindi corriamo strace
e vediamo cosa fa Cadvisor e troviamo le chiamate di sistema più lunghe di 100 ms:
theojulienne@kube-node-bad ~ $ sudo strace -p 10137 -T -ff 2>&1 | egrep '<0.[1-9]'
[pid 10436] <... futex resumed> ) = 0 <0.156784>
[pid 10432] <... futex resumed> ) = 0 <0.258285>
[pid 10137] <... futex resumed> ) = 0 <0.678382>
[pid 10384] <... futex resumed> ) = 0 <0.762328>
[pid 10436] <... read resumed> "cache 154234880nrss 507904nrss_h"..., 4096) = 658 <0.179438>
[pid 10384] <... futex resumed> ) = 0 <0.104614>
[pid 10436] <... futex resumed> ) = 0 <0.175936>
[pid 10436] <... read resumed> "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 577 <0.228091>
[pid 10427] <... read resumed> "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 577 <0.207334>
[pid 10411] <... epoll_ctl resumed> ) = 0 <0.118113>
[pid 10382] <... pselect6 resumed> ) = 0 (Timeout) <0.117717>
[pid 10436] <... read resumed> "cache 154234880nrss 507904nrss_h"..., 4096) = 660 <0.159891>
[pid 10417] <... futex resumed> ) = 0 <0.917495>
[pid 10436] <... futex resumed> ) = 0 <0.208172>
[pid 10417] <... futex resumed> ) = 0 <0.190763>
[pid 10417] <... read resumed> "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 576 <0.154442>
Come ci si potrebbe aspettare, qui vediamo chiamate lente read()
. Dal contenuto delle operazioni di lettura e dal contesto mem_cgroup
è chiaro che queste sfide read()
fare riferimento al fascicolo memory.stat
, che mostra l'utilizzo della memoria e i limiti di cgroup (tecnologia di isolamento delle risorse di Docker). Lo strumento Cadvisor interroga questo file per ottenere informazioni sull'utilizzo delle risorse per i contenitori. Controlliamo se è il kernel o Cadvisor a fare qualcosa di inaspettato:
theojulienne@kube-node-bad ~ $ time cat /sys/fs/cgroup/memory/memory.stat >/dev/null
real 0m0.153s
user 0m0.000s
sys 0m0.152s
theojulienne@kube-node-bad ~ $
Ora possiamo riprodurre il bug e capire che il kernel Linux è affetto da una patologia.
Perché l'operazione di lettura è così lenta?
In questa fase è molto più semplice trovare messaggi di altri utenti su problemi simili. Come si è scoperto, nel tracker Cadvisor questo bug è stato segnalato come
Il problema è che i cgroup tengono conto dell'utilizzo della memoria all'interno dello spazio dei nomi (contenitore). Quando tutti i processi in questo cgroup escono, Docker rilascia il cgroup di memoria. Tuttavia, la "memoria" non è solo la memoria del processo. Sebbene la memoria del processo in sé non venga più utilizzata, sembra che il kernel assegni ancora i contenuti memorizzati nella cache, come dentry e inode (metadati di directory e file), che sono memorizzati nella cache nel cgroup di memoria. Dalla descrizione del problema:
cgroup zombie: cgroup che non hanno processi e sono stati cancellati, ma hanno ancora memoria allocata (nel mio caso, dalla dentry cache, ma può anche essere allocata dalla page cache o tmpfs).
Il controllo da parte del kernel di tutte le pagine nella cache quando si libera un cgroup può essere molto lento, quindi viene scelto il processo pigro: attendere finché queste pagine non vengono richieste nuovamente, quindi cancellare finalmente il cgroup quando la memoria è effettivamente necessaria. Fino a questo momento, cgroup viene ancora preso in considerazione nella raccolta delle statistiche.
Dal punto di vista delle prestazioni, hanno sacrificato la memoria a favore delle prestazioni: accelerando la pulizia iniziale lasciando dietro di sé un po' di memoria nella cache. Questo va bene. Quando il kernel utilizza l'ultima memoria cache, il cgroup viene eventualmente cancellato, quindi non può essere definito un "leak". Sfortunatamente, l'implementazione specifica del meccanismo di ricerca memory.stat
in questa versione del kernel (4.9), combinata con l'enorme quantità di memoria sui nostri server, significa che ci vuole molto più tempo per ripristinare gli ultimi dati memorizzati nella cache e cancellare gli zombie di cgroup.
Si scopre che alcuni dei nostri nodi avevano così tanti cgroup zombie che la lettura e la latenza superavano un secondo.
La soluzione alternativa al problema di Cadvisor è liberare immediatamente le cache di dentry/inode in tutto il sistema, eliminando immediatamente la latenza di lettura e di rete sull'host, poiché svuotare la cache attiva le pagine cgroup zombie memorizzate nella cache e libera anche quelle. Questa non è una soluzione, ma conferma la causa del problema.
Si è scoperto che nelle versioni del kernel più recenti (4.19+) le prestazioni delle chiamate erano migliorate memory.stat
, quindi il passaggio a questo kernel ha risolto il problema. Allo stesso tempo, disponevamo di strumenti per rilevare i nodi problematici nei cluster Kubernetes, svuotarli con garbo e riavviarli. Abbiamo setacciato tutti i cluster, trovato nodi con una latenza sufficientemente elevata e li abbiamo riavviati. Questo ci ha dato il tempo di aggiornare il sistema operativo sui server rimanenti.
Riassumendo
Poiché questo bug ha interrotto l'elaborazione della coda della scheda NIC RX per centinaia di millisecondi, ha causato contemporaneamente sia un'elevata latenza su connessioni brevi che una latenza a metà connessione, ad esempio tra richieste MySQL e pacchetti di risposta.
Comprendere e mantenere le prestazioni dei sistemi più fondamentali, come Kubernetes, è fondamentale per l'affidabilità e la velocità di tutti i servizi basati su di essi. Ogni sistema che esegui beneficia dei miglioramenti delle prestazioni di Kubernetes.
Fonte: habr.com