
Para disa vjet, Kubernetes në blogun zyrtar të GitHub. Që atëherë, ai është bërë teknologjia standarde për shpërndarjen e shërbimeve. Tani Kubernetes menaxhon një pjesë të madhe të shërbimeve interne dhe publike. Me rritjen e klasterëve tanë dhe kërkesave më të forta për performancë, kemi filluar të ndeshemi me vonesa sporadike në disa shërbime në Kubernetes, të cilat nuk mund të shpjegohen nga ngarkesa e vetë aplikacionit.
Në thelb, aplikacionet ndeshen me vonesa të rastit në rrjet deri në 100 ms ose më shumë, gjë që shkakton timeout ose përpjekje të përsëritura. Pritej që shërbimet të ishin në gjendje të përgjigjeshin ndaj kërkesave shumë më shpejt se 100 ms. Por kjo është e pamundur nëse lidhja vetë merr kaq shumë kohë. Ndërkohë, kemi vënë re kërkesa shumë të shpejta në MySQL, të cilat do duhej të zinin milisekonda, dhe MySQL në të vërtetë e përfundonte për milisekonda, por nga këndvështrimi i aplikacionit kërkues, përgjigja merrte 100 ms ose më shumë.
Së shpejti u kuptua se problemi ndodhte vetëm gjatë lidhjes me nodin Kubernetes, edhe nëse thirrja vinte jashtë Kubernetes. Më lehtë është të riprodhosh problemin në një test , i cili ekzekutohet nga ndonjë host të brendshëm, teston shërbimin Kubernetes në një port të caktuar, dhe me sporadikë regjistron një vonesë të madhe. Në këtë artikull do të shohim se si arritëm ta gjurmojmë shkakun e këtij problemi.
Eliminimi i komplikeve të panevojshme në zinxhirin drejt dështimit
Duke riprodhuar shembullin e njëjtë, doja të ngushtoja fokusin e problemit dhe të eliminonim shtresat e panevojshme të komplikuar. Fillimisht, kishte shumë elementë midis Vegeta dhe pod'ave në Kubernetes. Për të përcaktuar një problem më të thellë në rrjet, është e nevojshme të përjashtohen disa nga ato.

Klienti (Vegeta) krijon një lidhje TCP me ndonjë nod në klaster. Kubernetes funksionon si një rrjet mbështjellës (sipër rrjetit ekzistues të qendrës së të dhënave) që përdor , domethënë inkapsulon paketat IP të rrjetit mbështjellës brenda paketave IP të qendrës së të dhënave. Kur lidhemi me nodin e parë, bëhet një transformim i adresave rrjetore (NAT) me gjurmimin e gjendjes për të transformuar adresën IP dhe portin e nodit Kubernetes në adresën IP dhe portin në rrjetin mbështjellës (në veçanti, pod'in me aplikacionin). Për paketat e ardhura, realizohet sekuenca e kundërt e veprimeve. Kjo është një sistem kompleks me shumë gjendje dhe elemente që përditësohen dhe ndryshojnë vazhdimisht me shpërndarjen dhe lëvizjen e shërbimeve.
Utilitari tcpdump në testin e Vegeta shkakton një vonesë gjatë dorëzimit të TCP (midis SYN dhe SYN-ACK). Për të eliminuar këtë kompleksitet të panevojshëm, mund të përdorim hping3 për "ping" të thjeshtë me paketa SYN. Kontrollojmë nëse ka vonesë në paketën përgjigje, pastaj rikujtojmë lidhjen. Mund të filtrojmë të dhënat, duke përfshirë vetëm paketat më të ngadalta se 100 ms, dhe të kemi një variant më të thjeshtë për të riprodhuar problemin sesa testi i plotë i nivelit të rrjetit 7 në Vegeta. Këtu janë "ping" të nodit Kubernetes duke përdorur TCP SYN/SYN-ACK në "portin e nodit" të shërbimit (30927) me interval 10 ms, të filtruar për përgjigjet më të ngadalta:
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
Mund të bëhet një vëzhgim të parë. Në bazë të numrave të rendit dhe koha të duket se këto nuk janë bllokime të rastësishme. Vonesat shpesh akumulohen dhe në fund përpunohen.
Pastaj duam të zbulojmë se cilat komponentë mund të kenë kontribuar në bllokim. A mund të jetë ndonjë nga qindra rregullave të iptables në NAT? Apo ndonjë problem me tunelizimin IPIP në rrjet? Një nga mënyrat për ta provuar këtë është të kontrollojmë çdo hap të sistemit, duke e përjashtuar atë. Çfarë do të ndodhte nëse heqim NAT dhe logjikën e firewall, duke lënë vetëm pjesën IPIP:

Fatmirësisht, Linux lejon të qaset lehtësisht drejtpërdrejt në shtresën mbështjellëse IP, nëse makineria është në të njëjtin rrjet:
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
Duke parë rezultatet, problemi ende qëndron! Kjo përjashton iptables dhe NAT. Pra, a është problemi në TCP? Le të shohim si funksionon pingu i zakonshëm 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
Rezultatet tregojnë se problemi nuk ka zhdukur. Ndoshta është një tunel IPIP? Le të thjeshtojmë testin më tej:

Të gjitha paketat dërgohen midis këtyre dy hosteve?
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
Ne kemi thjeshtuar situatën në dy nodet Kubernetes që dërgojnë paketat njëra-tjetrës, madje edhe pings ICMP. Ata vazhdojnë të shohin vonesa, nëse hosti i synuar është 'i keq' (disa më keq se të tjerët).
Tani pyetja e fundit: pse vonesa ndodh vetëm në serverët kube-node? A ndodh kur kube-node është dërguesi apo marrësi? Fatmirësisht, kjo gjithashtu është e lehtë për t'u zbuluar duke dërguar një paketë nga një host jashtë Kubernetes-it, por me të njëjtin 'më të keqin' të njohur. Siç shohim, problemi nuk është zhdukur:
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
Pastaj do të kryejmë të njëjtat kërkesa nga kube-node burimor i mëparshëm në hostin e jashtëm (kjo përjashton hostin burimor, pasi ping përfshin si komponent RX ashtu edhe 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 statistik ---
22352 paketa të transmetuara, 22350 paketa të pranuara, 1% humbje paketi
koha e kthimit minimal/mesatare/maksimale = 0.2/7.6/1010.6 ms
Duke shqyrtuar kapjet e paketave me vonesa, kemi marrë disa informata shtesë. Në veçanti, dërguesi (më poshtë) e sheh këtë kohë të skadimit, por marrësi (më lart) nuk e sheh — shihni kolonën Delta (në sekonda):
Përveç kësaj, nëse shikoni ndryshimin në rendin e paketimeve TCP dhe ICMP (sipër numrave të rendit) në anën e marrësit, paketat ICMP gjithmonë arrijnë në të njëjtin rend siç janë dërguar, por me një kohë të ndryshme. Në të njëjtën kohë, paketat TCP ndonjëherë alternojnë dhe disa prej tyre mbeten të bllokuar. Në veçanti, nëse shqyrtojmë portet e paketave SYN, ato në anën e dërguesit vijnë në radhë, ndërsa në anën e marrësit jo.
Ka një dallim të hollë në mënyrën se si e serverëve modernë (si në qendrën tonë të të dhënave) trajtojnë paketat që përmbajnë TCP ose ICMP. Kur një paketë arrin, adaptori rrjetit 'hashon atë sipas lidhjes', dmth. përpiqet të ndajë lidhjet sipas radhëve dhe dërgon çdo radhë në një bërthamë të veçantë procesori. Për TCP, ky hash përfshin si adresën IP burimore ashtu edhe atë përfundimtare dhe portin. Me fjalë të tjera, çdo lidhje hashon (potencialisht) ndryshe. Për ICMP, vetëm adresat IP hashohen, pasi nuk ka porte.
Një vëzhgim tjetër i ri: gjatë kësaj periudhe shohim vonesa ICMP në të gjitha komunikimet midis dy hosteve, ndërsa për TCP nuk ka. Kjo na tregon se arsyeja, shumë mirë, është e lidhur me hashimin e radhëve RX: pothuajse me siguri ngërçi ndodh në trajtimin e paketave RX dhe jo në dërgimin e përgjigjeve.
Kjo përjashton mundësitë e dërgimit të paketave. Tani e dimë se problemi i trajtimit të paketave ndodhet në anën e pranimit në disa serverë kube-node.
Të kuptojmë trajtimin e paketave në bërthamën Linux
Për të kuptuar se pse problemi ndodh tek marrësi në disa serverë kube-node, le të shikojmë se si bërthama Linux trajton paketat.
Duke u kthyer në implementimin më të thjeshtë tradicional, karta e rrjetit merr një paketë dhe dërgon bërthamës Linux, që është një paketë që duhet të përpunojë. Bërthama ndalon punën tjetër, kalon kontekstin në manipuluesin e ndërprerjeve, përpunon paketën dhe pastaj kthehet në detyrat aktuale.

Ky kalim konteksti ndodh ngadalë: ndoshta vonesa ishte e padukshme në kartat e rrjetit 10-megabiti në vitet '90, por në kartat moderne 10G me një kapacitet maksimal prej 15 milion paketash në sekondë, çdo bërthamë e një serveri të vogël tetë-bërthamësh mund të ndalet miliona herë në sekondë.
Për të mos u marrë vazhdimisht me procesin e trajtimit të ndërprerjeve, shumë vite më parë në Linux u shtua : API rrjetit që përdorin të gjithë drejtuesit modernë për të përmirësuar performancën në shpejtësi të larta. Në shpejtësi të ulëta, bërthama akoma merr ndërprerje nga karta rrjeti në mënyrën e vjetër. Sa më shumë paketa të vijë, e cila tejkalon pragun, bërthama ç aktivizon ndaloj ndërprerjet dhe në vend të kësaj fillon të interrogojë adaptorin rrjet dhe merr paketat në grupe. Procesimi ndodh në softirq, dmth në pas thirrjeve sistemike dhe ndërprerjeve harduerike, kur bërthama (ndryshe nga hapësira përdoruese) është tashmë e aktivizuar.

Kjo është shumë më e shpejtë, por krijon një problem tjetër. Nëse ka shumë paketa, gjithmonë kalon koha për të procesuar paketat nga karta rrjet, dhe proceset e hapësirës përdoruese nuk arrijnë të zbrazin vërtet këto radhë (leximi nga lidhjet TCP etj.). Në fund, radhët mbushen dhe fillojmë të hedhin paketat. Duke u përpjekur të gjejmë një ekuilibër, bërthama ka vendosur një buxhet për numrin maksimal të paketave që përpunohen në kontekstin e softirq. Sap buxheti tejkalohet, zgjohet një proces i veçantë ksoftirqd (do të shihni një prej tyre në ps për secilën bërthamë), i cili përpunon këto softirq jashtë rrugës së zakonshme syscall/interrupt. Ky proces planifikohet me ndihmën e planifikuesit standard të proceseve, i cili përpiqet të shpërndajë burimet në mënyrë të drejtë.

Duke studiuar se si bërthama i përpunon paketat, mund të vëreni se ekziston një mundësi e caktuar për shfaqjen e bllokimeve. Nëse thirrjet e softirq vijnë më rrallë, paketat do të duhet të presin disa kohë për t’u përpunuar në radhën RX në kartën rrjet. Ndoshta, kjo ndodh për shkak të ndonjë detyre që bllokon bërthamën e procesorit, ose diçka tjetër pengon bërthamën të aktivizojë softirq.
Duke e ngushtuar përpunimin te bërthama ose metoda
Pritjet e softirq janë ende vetëm një hipotezë. Por ka kuptim, dhe ne e dimë se po vërejmë diçka shumë të ngjashme. Prandaj, hapi tjetër është të konfirmojmë këtë teori. Dhe nëse ajo konfirmohet, të gjejmë shkakun e pritjeve.
Le të kthehemi te paketat tona të ngadalta:
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
Siç u diskutua më parë, këto paketa ICMP janë të vendosura në një radhë NIC RX dhe përpunohen nga një bërthamë CPU. Nëse duam të kuptojmë funksionimin e Linux-it, është e dobishme të dimë se ku (në cilën bërthamë CPU) dhe si (softirq, ksoftirqd) përpunohen këto paketa për të ndjekur procesin.
Tani është koha të përdorim mjetet që lejojnë ndjekjen në kohë reale të funksionimit të bërthamës Linux. Këtu ne përdorëm . Këto mjete lejojnë të shkruhen programe të vogla në C, që kapin funksione të rastit në bërthamë dhe ndihmojnë ngjarjet në programin e hapësirës përdoruese Python, i cili mund t’i përpunojë dhe t’ju kthejë rezultatin. Kufizimet për funksione të rastit në bërthamë janë një çështje e komplikuar, por utiliteti është projektuar për siguri maksimale dhe është i dedikuar për ndjekjen e këtyre problemeve të prodhimit, që është vështirë të riprodhohen në një mjedis testimi ose zhvillimi.
Plani këtu është i thjeshtë: ne e dimë se bërthama i përpunon këto pings ICMP, prandaj, do të vendosim një kufizim në funksionin e bërthamës , i cili merr paketën e ardhshme ICMP "echo request" dhe iniciaton dërgimin e përgjigjes ICMP "echo response". Ne mund të identifikojmë paketën nga rritja e numrit icmp_seq, i cili tregon hping3 më lart.
Kodi duket i komplikuar, por nuk është aq i tmerrshëm sa duket. Funksioni icmp_echo transmeton struct sk_buff *skb: është paketa e kërkesës "echo request". Ne mund ta ndjekim atë, të tërheqim sekuencën echo.sequence (e cila korrespondon me icmp_seq nga hping3 më sipër), dhe ta dërgojmë atë në hapësirën përdoruese. Po ashtu është e dobishme të kapim emrin aktual të procesit/id e tij. Më poshtë janë rezultatet që ne shohim gjatë përpunimit të paketave nga bërthama:
TGID PID EMRI I PROCESIT 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
Këtu duhet të vëreni se në kontekstin e softirq proceset që bënë thirrjet sistemike do të shfaqen si "procese", ndonëse në të vërtetë bërthama e përpunon sigurt paketat në kontekstin e bërthamës.
Me këtë mjet mund të lidhim procese të caktuara me paketat specifike, të cilat tregojnë vonesë në hping3. Bëjmë një thjesht grep në këtë kapje për vlera të caktuara icmp_seq. Paketat përkatëse të vlerave të mësipërme icmp_seq janë shënuar së bashku me RTT që kemi vëzhguar më lart (vlerat e pritura të RTT për paketat që i kemi filtruar për shkak të vlerave RTT më pak se 50 ms janë shënuar në parantezë):
TGID PID EMRI I PROCESIT 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)
Rezultatet na tregojnë disa gjëra. E para, të gjitha këto paketa përpunohen nga konteksti ksoftirqd/11. Kjo do të thotë se për këtë çift të caktuar makinash paketat ICMP ishin të hash-uara në bërthamën 11 në anën që pranon. Shohim gjithashtu se gjatë çdo bllokimi ka paketa që përpunohen në kontekstin e thirrjes së sistemit cadvisor. Më pas ksoftirqd merr përsipër detyrën dhe përpunon radhën e mbledhur: pikërisht numrin e paketave që u mblodhën pas cadvisor.
Fakti që menjëherë përpara kësaj gjithmonë funksionon cadvisor, nënkupton përfshirjen e tij në problem. Për ironi, qëllimi është «të analizojë përdorimin e resursëve dhe karakteristikat e performancës së konteinerëve të nisur», e jo të shkaktojë këtë problem me performancën.
Si me aspektet e tjera të funksionit të kontejnerëve, ky është një mjet shumë i avancuar, nga i cili mund të pritet që të ketë probleme me performancën në disa rrethana të paparashikuara.
Çfarë bën cadvisor që ngadalëson radhën e paketave?
Tani kemi një kuptim të mirë se si ndodh dështimi, cili proces e shkakton dhe në cilin CPU. Shohim se për shkak të bllokimit të fortë, bërthama Linux nuk arrin të planifikojë në kohë ksoftirqd. Dhe shohim se paketat përpunohen në kontekstin cadvisor. Logjikisht është për të supozuar se cadvisor starton një syscall të ngadalshëm, pas të cilit përpunohen të gjitha paketat e mbledhura në atë kohë:

Kjo është një teori, por si ta verifikojmë? Çfarë mund të bëjmë, është të ndjekim punën e bërthamës CPU gjatë gjithë këtij procesi, të gjejmë pikën ku ndodh tejkalimi i buxhetit mbi numrin e paketave dhe ftohet ksoftirqd, dhe më pas të shohim pak më parë — çfarë po punonte në bërthamën CPU menjëherë përpara këtij momenti. Është si një rëntgen i CPU çdo disa milisekonda. Do të duket përafërsisht kështu:

E përshtatshme, është se të gjitha këto mund të bëhen me mjetet ekzistuese. Për shembull, me një frekuencë të caktuar kontrollon një bërthamë CPU të caktuar dhe mund të gjenerojë një grafik thirrjesh të sistemit të punës, duke përfshirë si hapësirën e përdoruesit ashtu edhe bërthamën Linux. Mund të marrë këtë regjistrim dhe ta përpunojmë me një fork të vogël të programit të Brendan Gregg, i cili ruan rendin e gjurmimit të stack. Mund të ruajmë gjurmimet e stack-ut në një rresht çdo 1 ms, dhe më pas të veçojmë dhe ruajmë një mostër për 100 milisekonda përpara se të hyjmë në gjurmimin 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
Ja rezultatet:
(qindra gjurmë që duken të ngjashme)
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
Ka shumë gjëra këtu, por më e rëndësishmja është se ne gjejmë modelin „cadvisor para ksoftirqd“, të cilin e kemi parë më parë në tracer-in e ICMP. Çfarë do të thotë kjo?
Çdo rresht është një gjurmim i CPU-së në një moment të caktuar. Çdo thirrje në rrjedhën e thellë në rresht ndahet me një pikë të ndarjes. Në mes të rreshtave shohim sistemin e thirrjes: read(): .... ;do_syscall_64;sys_read; .... Kështu, cadvisor kalon shumë kohë në thirrjen sistemike read(), që lidhet me funksionet mem_cgroup_* (pjesa e sipërme e stack-ut të thirrjeve/fundi i rreshtit).
Në gjurmimin e thirrjeve nuk është e lehtë të shohim se çfarë po lexon, prandaj do të ekzekutojmë strace dhe do të shohim se çfarë bën cadvisor dhe do të gjejmë thirrjet sistemike që zgjasin më shumë se 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
Siç mund të supozohet, këtu shohim thirrje të ngadalta read(). Nga përmbajtja e operacioneve të leximit dhe kontekstit mem_cgroup në duket se këto thirrje read() lidhen me dosjen memory.stat, e cila tregon përdorimin e memories dhe kufizimet e cgroup (teknologji e izolimit të burimeve në Docker). Instrumenti cadvisor e pyet këtë dosje për të marrë të dhëna rreth përdorimit të burimeve për kontejnerët. Le të verifikojmë nëse është bërthama ose cadvisor bëri ndonjë gjë të papritur:
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 ~ $
Tani mund të riprodhojmë gabimin dhe kuptojmë se bërthama Linux përballet me një patologji.
Çfarë e bën kaq të ngadaltë operacionin e leximit?
Në këtë pikë, është shumë më e lehtë të gjejmë mesazhe nga përdorues të tjerë mbi probleme të ngjashme. Siç doli, në tracer-in e cadvisor, ky gabim u raportua si , thjesht askush nuk e vuri re se vonesa po pasqyrohet rastësisht gjithashtu në stekët e rrjetit. Realisht u vërejt se cadvisor konsumon më shumë kohë procesori se sa pritej, por këto nuk iu kushtua gjithëvëmendje, pasi serverët tanë kanë shumë burime procesori, kështu që problemi nuk u shqyrtua me kujdes.
Problemi është se grupet e kontrollit (cgroups) llogaritin përdorimin e memories brenda hapësirës së emrit (kontejneri). Kur të gjitha proceset në këtë cgroup përfundojnë, Docker liçon grupin e kontrollit të memories. Megjithatë, „memoria“ nuk është thjesht memoria e procesit. Ndërsa vetë memoria e proceseve nuk po përdoret më, rezulton se bërthama gjithashtu cakton përmbajtje të ndihmës, si dentries dhe inodes (metadata e dosjeve dhe katalogëve), të cilat ruhen në cgroup-in e memories. Nga përshkrimi i problemës:
cgroups-zombi: grupet e kontrollit, në të cilat nuk ka procese dhe ato janë të fshira, por për të cilat akoma është rezervuar memoria (në rastin tim, nga cache dentry, por gjithashtu mund të rezervohen nga cache-t e faqeve ose tmpfs).
Kontrolli nga bërthama i të gjitha faqeve në cache kur lirohet cgroup mund të jetë shumë i ngadalshëm, prandaj është zgjedhur një proces inert: të presë derisa këto faqe të kërkohen përsëri dhe atëherë, kur memoria vërtet nevojitet, së fundi të pastrohet cgroup. Deri në këtë moment cgroup akoma llogaritet në mbledhjen e statistikave.
Nga pikëpamja e performancës, ata sakrifikuan memorin për të arritur performancë: pëshpejtimi i pastrimit fillestar duke lënë pak memorie të ruajtur. Kjo është normale. Kur bërthama përdor pjesën e fundit të memories së ruajtur, cgroup përfundimisht pastrohet, kështu që kjo nuk mund të quhet "zgjidhje e humbur". Fatkeqësisht, zbatimi specifik i mekanizmit të kërkimit memory.stat në këtë version të bërthamës (4.9), së bashku me një volum të madh memories në serverët tanë, çon në faktin se rikuperimi i të dhënave të fundit të ruajtura dhe pastrimi i cgroup-zombie kërkon shumë më tepër kohë.
Doli se në disa nga nyjat tona kishte kaq shumë cgroup-zombie, saqë leximi dhe vonesa kalonin një sekondë.
Një mënyrë për të anashkaluar problemin cadvisor është të lirohet menjëherë keshat e dentrive/inodeve në të gjithë sistemin, gjë që menjëherë eliminon vonesat në lexim dhe gjithashtu vonesën e rrjetit në host, pasi fshirja e caches përfshin faqet e ruajtur cgroup-zombie, të cilat gjithashtu çlirtohen. Kjo nuk është një zgjidhje, por konfirmon shkakun e problemit.
U zbulua se në versionet më të reja të bërthamës (4.19+) është përmirësuar performanca e thirrjes memory.stat, kështu që kalimi në këtë bërthamë zgjidhte problemin. Në të njëjtën kohë, kishim mjete për të identifikuar nyjat problematike në klasterët Kubernetes, për t'i ndarë ato me elegancë dhe për t'i rinisur. Ne kaluam nëpër të gjithë klasterët, gjetëm nyjat me vonesa mjaft të larta dhe i rinisëm. Kjo na dha kohë për të përditësuar sistemin operativ në serverët e tjerë.
Në përfundim
Pasi kjo defekt ndalonte përpunimin e radhëve NIC RX për qindra milisekonda, ai shkaktonte një vonesë të madhe në lidhjet e shkurtra dhe gjithashtu një vonesë në mes të lidhjeve, siç janë midis kërkesave MySQL dhe paketave të përgjigjeve.
Kuptimi dhe mbështetja e performancës së sistemeve më themelore, si Kubernetes, është thelbësore për besueshmërinë dhe shpejtësinë e të gjitha shërbimeve mbi të. Të gjitha sistemet që funksionojnë përfitojnë nga përmirësimet në performancën e Kubernetes.
Burimi: habr.com
