
För ett par Är sedan diskuterades Kubernetes i den officiella GitHub-bloggen. Sedan dess har det blivit en standardteknologi för driftsÀttning av tjÀnster. Nu hanterar Kubernetes en stor del av bÄde interna och offentliga tjÀnster. Eftersom vÄra kluster har vuxit och prestandakraven har blivit strÀngare, har vi börjat mÀrka att vissa tjÀnster pÄ Kubernetes sporadiskt fÄr fördröjningar som inte kan förklaras av belastningen pÄ sjÀlva applikationen.
I praktiken upplever applikationerna nÄgot som liknar en slumpmÀssig nÀtverksfördröjning pÄ upp till 100 ms eller mer, vilket leder till tidsavbrott eller omförsök. Det förvÀntades att tjÀnsterna skulle kunna svara pÄ förfrÄgningar betydligt snabbare Àn 100 ms. Men det Àr omöjligt om sjÀlva anslutningen tar sÄ mycket tid. Separat har vi observerat mycket snabba MySQL-förfrÄgningar som borde ta millisekunder, och MySQL klarade verkligen av det pÄ millisekunder, men för den begÀrande appliceringen tog svaret 100 ms eller mer.
Det blev genast klart att problemet endast uppstod vid anslutningar till en Kubernetes-nod, Àven om anropet kom frÄn utanför Kubernetes. Det enklaste sÀttet att Äterskapa problemet Àr i ett test , som körs frÄn valfri intern vÀrd, testar Kubernetes-tjÀnsten pÄ en specifik port, och sporadiskt registrerar stor försening. I denna artikel kommer vi att titta pÄ hur vi kunde spÄra orsaken till detta problem.
Vi eliminerar överflödig komplexitet i felkedjan
Genom att Äterskapa samma exempel ville vi inskrÀnka problemet och ta bort överflödiga lager av komplexitet. Ursprungligen fanns det för mÄnga element i flödet mellan Vegeta och poddar pÄ Kubernetes. För att identifiera en djupare nÀtverksfrÄga behövde vi utesluta nÄgra av dem.

Klienten (Vegeta) skapar en TCP-anslutning till en valfri nod i klustret. Kubernetes fungerar som ett översiktsnÀtverk (ovanpÄ det befintliga datacenter-nÀtverket) som anvÀnder , vilket innebÀr att den kapslar in IP-paket frÄn översiktsnÀtverket inom IP-paket frÄn datacentret. Vid anslutning till den första noden sker en nÀtverksadressöversÀttning (NAT) med tillstÄndsövervakning för att omvandla IP-adressen och porten för Kubernetes-noden till en IP-adress och port i det överliggande nÀtverket (specifikt för podden med applikationen). För inkommande paket utförs en omvÀnd sekvens av ÄtgÀrder. Detta Àr ett komplext system med mÄnga tillstÄnd och ett stort antal element som stÀndigt uppdateras och Àndras i takt med att tjÀnsterna distribueras och flyttas.
Verktyg Fungerar libdns korrekt? => Kör i Vegeta-testet ger en fördröjning under TCP-handshake (mellan SYN och SYN-ACK). För att ta bort denna överflödiga komplexitet kan man anvÀnda hping3 för enkla "ping" med SYN-paket. Vi kontrollerar om det finns en fördröjning i svaret och sedan ÄterstÀller vi anslutningen. Vi kan filtrera data genom att endast inkludera paket med mer Àn 100 ms och fÄ ett enklare reproduktion av problemet Àn ett fullstÀndigt test pÄ nÀtverksnivÄ 7 i Vegeta. HÀr Àr "pingar" frÄn Kubernetes-noden med hjÀlp av TCP SYN/SYN-ACK pÄ "nodens port" för tjÀnsten (30927) med en intervall pÄ 10 ms, filtrerade efter de lÀngsta svaren:
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
Vi kan omedelbart göra en första observation. UtifrÄn sekvenstal och tidpunkter Àr det tydligt att detta inte Àr sporadiska flaskhalsar. Fördröjningen ackumuleras ofta, och sÄ smÄningom hanteras den.
Vidare vill vi ta reda pÄ vilka komponenter som kan vara ansvariga för att flaskhalsen uppstÄr. Kanske Àr det nÄgra av de hundratals iptables-reglerna i NAT? Eller uppstÄr det problem med IPIP-tunneling i nÀtverket? Ett sÀtt att kontrollera detta Àr att granska varje steg i systemet och utesluta det. Vad hÀnder om vi tar bort NAT och brandvÀgglogiken, och lÀmnar bara IPIP-delen:

Som tur Àr tillÄter Linux oss enkelt att direkt adressera det överliggande IP-lagret om maskinen befinner sig i samma nÀtverk:
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
Baserat pÄ resultaten kvarstÄr problemet fortfarande! Detta utesluter iptables och NAT. SÄ problemet ligger i TCP? LÄt oss se hur en vanlig ICMP-ping gÄr:
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
Resultaten visar att problemet inte har försvunnit. Kan det vara en IPIP-tunnel? LÄt oss förenkla testet ytterligare:

Skickas alla paket mellan dessa tvÄ vÀrdar?
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
Vi har förenklat situationen till tvÄ Kubernetes-noder som skickar vilket paket som helst till varandra, Àven ICMP-ping. De ser fortfarande fördröjningar om den mÄl vÀrden Àr "dÄlig" (vissa Àr sÀmre Àn andra).
Nu den sista frÄgan: varför uppstÄr fördröjningen endast pÄ kube-node-servrar? Och förekommer den nÀr kube-node Àr avsÀndare eller mottagare? Lyckligtvis Àr det ocksÄ ganska enkelt att ta reda pÄ genom att skicka paket frÄn en vÀrd utanför Kubernetes, men till samma "kÀnda dÄliga" mottagare. Som vi ser har problemet inte försvunnit:
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
LÄt oss sedan göra samma förfrÄgningar frÄn den tidigare avsÀndande kube-node till den externa vÀrden (vilket utesluter den ursprungliga vÀrden eftersom ping inkluderar bÄde RX och TX som komponent):
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-statistik ---
22352 paket skickade, 22350 paket mottagna, 1% paketförlust
tur-och-retur min/medel/max = 0.2/7.6/1010.6 ms
Genom att studera paketfördröjningar har vi fĂ„tt viss ytterligare information. Specifikt att avsĂ€ndaren (nedan) ser denna tidsgrĂ€ns, medan mottagaren (ovan) inte ser den â se kolumnen Delta (i sekunder):
Dessutom, om vi ser pÄ skillnaden i ordningen av TCP- och ICMP-paket (efter sekvensnummer) frÄn mottagarens sida, sÄ anlÀnder ICMP-paketen alltid i samma ordning som de skickades, men med varierande timing. Samtidigt vÀxlar TCP-paketen ibland, och en del av dem fastnar. Specifikt, om vi studerar portarna för SYN-paket, sÄ kommer de i ordning frÄn avsÀndarens sida, men inte frÄn mottagarens.
Det finns en subtil skillnad i hur moderna servrar (som i vÄrt datacenter) hanterar paket som innehÄller TCP eller ICMP. NÀr ett paket anlÀnder "hashar" nÀtverksadaptern det baserat pÄ anslutningen, vilket innebÀr att den försöker dela upp anslutningarna i köer och skicka varje kö till en separat processor. För TCP inkluderar denna hash bÄde avsÀndar- och mottagarens IP-adress och port. Med andra ord, varje anslutning hashas (potentiellt) pÄ olika sÀtt. För ICMP hashas endast IP-adresser, eftersom det inte finns nÄgra portar.
En annan ny observation: under denna period ser vi ICMP-fördröjningar i all kommunikation mellan de tvÄ vÀrdarna, medan det inte finns nÄgra för TCP. Detta indikerar att orsaken sannolikt Àr relaterad till hashningen av RX-köerna: det Àr nÀstan sÀkert att köproblem uppstÄr i bearbetningen av RX-paket, snarare Àn i sÀndningen av svar.
Detta utesluter paketutskick frÄn listan över möjliga orsaker. Nu vet vi att problemet med paketbearbetningen ligger pÄ mottagarsidan pÄ vissa kube-node-servrar.
Vi undersöker paketbearbetning i Linux-kÀrnan
För att förstÄ varför problemet uppstÄr för mottagare pÄ vissa kube-node-servrar, lÄt oss titta pÄ hur Linux-kÀrnan hanterar paket.
Tillbaka till den enklaste traditionella implementeringen, sÄ tar nÀtverkskortet emot ett paket och skickar till Linux-kÀrnan, vilket indikerar att det finns ett paket som ska behandlas. KÀrnan stoppar annat arbete, vÀxlar kontext till avbrottshanteraren, hanterar paketet och ÄtergÄr sedan till sina nuvarande uppgifter.

Denna kontextvÀxling sker lÄngsamt: kanske var fördröjningen osynlig pÄ 10-mbps nÀtverkskort under 90-talet, men pÄ moderna 10G-kort med en maximal kapacitet av 15 miljoner paket per sekund kan varje kÀrna i en liten ÄttakÀrnig server avbrytas miljoner gÄnger per sekund.
För att undvika att stÀndigt hantera avbrott, lades det för mÄnga Är sedan till i Linux : ett nÀtverks-API som anvÀnds av alla moderna drivrutiner för att öka prestandan vid höga hastigheter. Vid lÄga hastigheter tar kÀrnan fortfarande emot avbrott frÄn nÀtverkskortet pÄ det gamla sÀttet. SÄ snart ett tillrÀckligt antal paket som överstiger tröskeln anlÀnder, stÀnger kÀrnan av avbrotten och börjar istÀllet frÄga nÀtverksadapter och hÀmta paket i portioner. Behandlingen utförs i softirq, det vill sÀga i efter systemanrop och hÄrdvaruavbrott, nÀr kÀrnan (till skillnad frÄn anvÀndarutrymmet) redan har startats.

Detta Àr mycket snabbare, men leder till ett annat problem. Om det kommer för mÄnga paket gÄr all tid Ät till att behandla paket frÄn nÀtverkskortet, och processer i anvÀndarutrymmet hinner inte faktiskt tömma dessa köer (lÀsa frÄn TCP-anslutningar osv.). I slutÀndan fylls köerna, och vi börjar kassera paket. För att försöka hitta en balans sÀtter kÀrnan en budget för det maximala antalet paket som fÄr behandlas i softirq-kontakt. SÄ snart denna budget överskrids vÀcks en separat trÄd ksoftirqd (du kommer att se en av dem i ps för varje kÀrna), som hanterar dessa softirq utanför den vanliga syscall-/avbrottsvÀgen. Denna trÄd schemalÀggs med hjÀlp av den vanliga process-schemalÀggaren som försöker fördela resurser rÀttvist.

Genom att studera hur kÀrnan behandlar paket kan man notera att det finns en viss sannolikhet för att det uppstÄr köer. Om softirq-anropen kommer mer sÀllan mÄste paketen vÀnta en stund pÄ behandling i RX-kön pÄ nÀtverkskortet. Detta kan bero pÄ nÄgon uppgift som blockerar kÀrnprocessorn, eller nÄgot annat som hindrar kÀrnan frÄn att köra softirq.
Vi begrÀnsar behandlingen till kÀrnan eller metoden
Softirq-fördröjningar Àr fortfarande bara en hypotes. Men det Àr ett rimligt antagande, och vi vet att vi upplever nÄgot vÀldigt liknande. SÄ nÀsta steg Àr att bekrÀfta denna teori. Om den bekrÀftas, mÄste vi hitta orsaken till fördröjningarna.
LÄt oss ÄtergÄ till vÄra lÄngsamma paket:
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
Som diskuterats tidigare, dessa ICMP-paket hashas till en kö för NIC RX och behandlas av en enda CPU-kÀrna. Om vi vill förstÄ hur Linux fungerar, Àr det bra att veta var (vilken CPU-kÀrna) och hur (softirq, ksoftirqd) dessa paket behandlas, för att kunna spÄra processen.
Det Àr nu dags att anvÀnda verktyg som gör det möjligt att spÄra Linux-kÀrnans drift i realtid. HÀr anvÀnde vi . Denna uppsÀttning verktyg lÄter oss skriva smÄ program i C som fÄngar upp godtyckliga funktioner i kÀrnan och buffrar hÀndelser i ett Python-anvÀndarutrymmeprogram, som kan bearbeta dem och ge oss resultat. Hookar för godtyckliga funktioner i kÀrnan Àr komplicerat, men verktyget Àr designat för maximal sÀkerhet och Àr avsett för att spÄra sÄdana produktionsproblem som Àr svÄra att reproducera i en test- eller utvecklingsmiljö.
Planen hÀr Àr enkel: Vi vet att kÀrnan hanterar dessa ICMP-pingar, sÄ vi sÀtter en hook pÄ kÀrnfunktionen , som tar emot den inkommande ICMP-paketet "echo request" och initierar sÀndningen av ICMP-svaret "echo response". Vi kan identifiera paketet genom att se pÄ ökning av numret icmp_seq, som visar hping3 högre.
Kod ser komplicerat ut, men det Àr inte sÄ skrÀmmande som det verkar. Funktionen icmp_echo rapporterar struct sk_buff *skb: Àr paketet med "echo request"-förfrÄgan. Vi kan spÄra det, ta ut sekvensen echo.sequence (som matchar med icmp_seq frÄn hping3 metoden.), och skicka den till anvÀndarutrymmet. Det Àr ocksÄ praktiskt att fÄnga det aktuella processnamnet/id. Nedan visas resultaten som vi ser direkt under paketbehandlingen av kÀrnan:
TGID PID PROCESS NAME 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 swapper/11 778 4512 4542 spokes-report-s 779
Det bör pÄpekas att i detta sammanhang softirq processer som har gjort systemanrop kommer att visas som "processer", Àven om kÀrnan faktiskt hanterar paket i kÀrnans kontext.
Med detta verktyg kan vi etablera en koppling mellan specifika processer och specifika paket som visar förseningar i hping3. Vi gör en enkel grep analys av denna fÄngst för vissa vÀrden icmp_seq. Paketen som överensstÀmmer med ovanstÄende vÀrden av icmp_seq har mÀrkts tillsammans med deras RTT, som vi observerade ovan (de förvÀntade vÀrdena för RTT för paketen som vi filtrerade pÄ grund av RTT-vÀrden mindre Àn 50 ms anges i parentes):
TGID PID PROCESS NAME 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 ksoftirqd/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 ksoftirqd/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)
Resultaten talar om flera saker. För det första hanteras alla dessa paket av kontexten ksoftirqd/11. Detta innebÀr att för denna specifika maskinpar hashades ICMP-paketen till kÀrna 11 pÄ mottagarsidan. Vi ser ocksÄ att det vid varje stopp finns paket som hanteras i kontexten av systemanropet cadvisor. Sedan ksoftirqd tar sig an uppgiften och behandlar den ackumulerade kön: den mÀngd paket som har samlats efter cadvisor.
Det faktum att det alltid finns verksamhet precis innan detta cadvisor, tyder pÄ dess involvering i problemet. Ironiskt nog Àr syftet med  att "analysera resursanvÀndning och prestandakarakteristika för startade containrar", snarare Àn att orsaka detta prestandaproblem.
Likt med andra aspekter av containerarbete, Àr detta ett ytterst avancerat verktyg som mycket vÀl kan ge upphov till prestandaproblem under vissa oförutsedda omstÀndigheter.
Vad Àr det cadvisor gör som bromsar paketkön?
Nu har vi en ganska bra förstÄelse för hur felet intrÀffar, vilken process som orsakar det och pÄ vilken CPU. Vi ser att pÄ grund av hÄrd blockering hinner Linux-kÀrnan inte schemalÀgga i tid. ksoftirqdOch vi ser att paketen bearbetas i kontexten. cadvisor. Det Àr logiskt att anta att cadvisor driver en lÄngsam syscall, efter vilket alla paket som har samlats under tiden bearbetas:

Detta Ă€r teorin, men hur kan vi bekrĂ€fta den? Vad vi kan göra Ă€r att följa CPU-kĂ€rnans arbete genom hela denna process, hitta punkten dĂ€r budgetöverskridandet för antalet paket intrĂ€ffar och ksoftirqd anropas, och sedan titta lite tidigare â vad som arbetade pĂ„ CPU:n precis innan detta ögonblick. Det Ă€r som en röntgen av CPU:n var nĂ„gra millisekund.

Det Àr praktiskt att allt detta kan göras med befintliga verktyg. Till exempel, kontrollerar med angiven frekvens den specificerade CPU-kÀrnan och kan generera en anropsgraf för det fungerande systemet, inklusive bÄde anvÀndarutrymme och Linux-kÀrnan. Vi kan ta denna inspelning och bearbeta den med hjÀlp av en liten fork av programmet frÄn Brendan Gregg, som bevarar ordningen pÄ stackspÄren. Vi kan spara enstreckiga stackspÄr var 1 ms och sedan extrahera och spara ett prov för 100 millisekunder innan det kommer in i spÄret. 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
HÀr Àr resultaten:
(hundratals spÄr som ser likadana ut)
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
Det finns mycket hÀr, men det viktigaste Àr att vi hittar mönstret "cadvisor före ksoftirqd" som vi tidigare sett i ICMP-tracern. Vad betyder detta?
Varje rad Àr en CPU-tracering vid en viss tidpunkt. Varje nedÄtgÄende anrop i stacken pÄ raden separeras med ett semikolon. I mitten av raderna ser vi det anrop som görs: read(): .... ;do_syscall_64;sys_read; .... SÄledes spenderar cadvisor mycket tid pÄ systemanropet read(), relaterat till funktionerna mem_cgroup_* (den övre delen av anropsstacken/slutet av raden).
I anropstraceringen Àr det svÄrt att se vad som faktiskt lÀses, sÄ vi kommer att köra för att kontrollera paketöverföringen Ät bÄda hÄll och se vad cadvisor gör och hitta systemanrop som tar mer Àn 100 ms:
theojulienne@kube-node-bad ~ $ sudo strace -p 10137 -T -ff 2>&1 | egrep '<0.[1-9]'
[pid 10436] ) = 0
[pid 10432] ) = 0
[pid 10137] ) = 0
[pid 10384] ) = 0
[pid 10436] "cache 154234880nrss 507904nrss_h"..., 4096) = 658
[pid 10384] ) = 0
[pid 10436] ) = 0
[pid 10436] "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 577
[pid 10427] "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 577
[pid 10411] ) = 0
[pid 10382] ) = 0 (Timeout)
[pid 10436] "cache 154234880nrss 507904nrss_h"..., 4096) = 660
[pid 10417] ) = 0
[pid 10436] ) = 0
[pid 10417] ) = 0
[pid 10417] "cache 0nrss 0nrss_huge 0nmapped_"..., 4096) = 576
Som förvÀntat ser vi lÄngsamma anrop hÀr read(). InnehÄllet i lÀsoperationer och kontext mem_cgroup det Àr tydligt att dessa anrop read() hör till filen memory.stat, som visar minnesanvÀndning och begrÀnsningar för cgroup (resursisoleringsteknologi i Docker). Verktyget cadvisor frÄgar denna fil för att fÄ information om resursanvÀndning för containrar. LÄt oss kontrollera om det Àr kÀrnan eller cadvisor som gör nÄgot ovÀntat:
theojulienne@kube-node-bad ~ $ time cat /sys/fs/cgroup/memory/memory.stat >/dev/null
verklig 0m0.153s
anvÀndare 0m0.000s
sys 0m0.152s
theojulienne@kube-node-bad ~ $
Nu kan vi Äterskapa buggen och förstÄr att Linux-kÀrnan stöter pÄ en patologi.
Varför Àr lÀsoperationen sÄ lÄngsam?
Vid det hÀr laget Àr det mycket enklare att hitta meddelanden frÄn andra anvÀndare om liknande problem. Som det visade sig, rapporterades denna bugg i cadvisor-tracker som en , det var bara ingen som upptÀckte att fördröjningen Àven slumpmÀssigt pÄverkar nÀtverksstacken. Det mÀrktes verkligen att cadvisor förbrukade mer CPU-tid Àn förvÀntat, men detta togs inte pÄ allvar, eftersom vÄra servrar har mycket CPU-resurser, sÄ problemet studerades inte noggrant.
Problemet Ă€r att kontrollgrupper (cgroups) tar hĂ€nsyn till minnesanvĂ€ndning inom namnutrymmet (containern). NĂ€r alla processer i denna cgroup avslutas frigör Docker minneskontrollgruppen. Men 'minne' Ă€r inte bara processminne. Ăven om processens minne inte lĂ€ngre anvĂ€nds, visar det sig att kĂ€rnan fortfarande tilldelar cacheinnehĂ„ll, sĂ„som dentries och inodes (metadata för kataloger och filer), som cachas i memory cgroup. FrĂ„n problembeskrivningen:
cgroups-zombier: kontrollgrupper utan processer som har tagits bort, men för vilka minne fortfarande allokeras (i mitt fall, frÄn dentry-cache, men det kan Àven allokeras frÄn sidcache eller tmpfs).
KÀrnans kontroll av alla sidor i cachen vid frigörandet av cgroup kan vara mycket lÄngsam, sÄ en lat process valdes: vÀnta tills dessa sidor begÀrs igen, och först nÀr minnet verkligen behövs, till slut rensa cgroup. Fram till dess beaktas cgroup fortfarande vid insamling av statistik.
Ur prestandas synpunkt har de offrat minnet för prestanda: snabbare initial rensning genom att kvarlÀmna lite cacheminne. Det Àr okej. NÀr kÀrnan anvÀnder den sista delen av det cacheminnet, rensas cgroup till slut, sÄ det kan inte kallas en "lÀcka". TyvÀrr gör den specifika implementeringen av sökmekanismen memory.stat i denna version av kÀrnan (4.9), i kombination med den stora mÀngden minne pÄ vÄra servrar, att det tar mycket lÀngre tid att ÄterstÀlla de senaste cachade data och rensa cgroup-zombier.
Det visade sig att pÄ vissa av vÄra noder fanns sÄ mÄnga cgroup-zombier att lÀsning och fördröjning översteg en sekund.
Ett sÀtt att kringgÄ problemet med cadvisor Àr att omedelbart frigöra cacharna för dentries/inodes över hela systemet, vilket omedelbart eliminerar lÀsfördröjningen, sÄvÀl som nÀtverksfördröjningen pÄ vÀrden, eftersom borttagning av cache inkluderar de cachade sidorna av cgroup-zombier, som ocksÄ frigörs. Detta Àr ingen lösning, men bekrÀftar orsaken till problemet.
Det visade sig att i de nyare versionerna av kÀrnan (4.19+) har prestandan för anrop förbÀttrats memory.stat, sÄ att uppgradering till denna kÀrna löste problemet. Samtidigt hade vi verktyg för att upptÀcka problematiska noder i Kubernetes-kluster, elegant stÀnga av dem och starta om dem. Vi genomsökte alla kluster, hittade noder med tillrÀckligt hög fördröjning och startade om dem. Detta gav oss tid att uppdatera OS pÄ resten av servrarna.
Sammanfattningsvis
Eftersom denna bugg stoppade hanteringen av NIC RX-köer i hundratals millisekunder, orsakade den samtidigt bÄde stor fördröjning i korta anslutningar och fördröjning i mitten av anslutningen, till exempel mellan MySQL-frÄgor och svarspaket.
FörstÄelse och stöd för prestanda hos de mest fundamentala systemen, sÄsom Kubernetes, Àr avgörande för tillförlitligheten och hastigheten hos alla tjÀnster som Àr baserade pÄ dem. Alla körda system drar nytta av Kubernetes prestandaförbÀttringar.
KĂ€lla: habr.com
