CPU-gränser och aggressiv strypning i Kubernetes

Notera. transl.: Den här ögonöppnande historien om Omio – en europeisk resesamlare – tar läsarna från grundläggande teori till de fascinerande praktiska krångligheterna i Kubernetes-konfigurationen. Förtrogenhet med sådana fall hjälper inte bara att vidga dina vyer, utan också förhindra icke-triviala problem.

CPU-gränser och aggressiv strypning i Kubernetes

Har du någonsin haft en ansökan som fastnat på plats, slutat svara på hälsokontroller och inte kunnat ta reda på varför? En möjlig förklaring är relaterad till gränser för CPU-resurskvoter. Detta är vad vi kommer att prata om i den här artikeln.

TL; DR:
Vi rekommenderar starkt att du inaktiverar CPU-gränser i Kubernetes (eller inaktiverar CFS-kvoter i Kubelet) om du använder en version av Linux-kärnan med en CFS-kvotbugg. I kärnan är tillgänglig seriös och välkänd en bugg som leder till överdriven strypning och förseningar
.

I Omio hela infrastrukturen hanteras av Kubernetes. Alla våra statliga och tillståndslösa arbetsbelastningar körs uteslutande på Kubernetes (vi använder Google Kubernetes Engine). Under de senaste sex månaderna har vi börjat observera slumpmässiga nedgångar. Applikationer fryser eller slutar svara på hälsokontroller, förlorar anslutningen till nätverket, etc. Detta beteende förbryllade oss länge, och till slut bestämde vi oss för att ta problemet på allvar.

Sammanfattning av artikeln:

  • Några ord om containrar och Kubernetes;
  • Hur CPU-förfrågningar och gränser implementeras;
  • Hur CPU-gränsen fungerar i miljöer med flera kärnor;
  • Hur man spårar CPU-strypning;
  • Problemlösning och nyanser.

Några ord om containrar och Kubernetes

Kubernetes är i grunden den moderna standarden inom infrastrukturvärlden. Dess huvudsakliga uppgift är containerorkestrering.

behållare

Tidigare var vi tvungna att skapa artefakter som Java JARs/WARs, Python Eggs eller körbara filer för att köras på servrar. Men för att få dem att fungera behövde ytterligare arbete göras: installera runtime-miljön (Java/Python), placera nödvändiga filer på rätt ställen, säkerställa kompatibilitet med en specifik version av operativsystemet, etc. Med andra ord, noggrann uppmärksamhet måste ägnas åt konfigurationshantering (vilket ofta var en källa till stridigheter mellan utvecklare och systemadministratörer).

Containers förändrade allt. Nu är artefakten en containerbild. Den kan representeras som en sorts utökad körbar fil som inte bara innehåller programmet utan också en fullfjädrad exekveringsmiljö (Java/Python/...), såväl som nödvändiga filer/paket, förinstallerade och redo att springa. Behållare kan distribueras och köras på olika servrar utan några ytterligare steg.

Dessutom fungerar containrar i sin egen sandlådemiljö. De har sin egen virtuella nätverksadapter, sitt eget filsystem med begränsad åtkomst, sin egen hierarki av processer, sina egna begränsningar för CPU och minne, etc. Allt detta implementeras tack vare ett speciellt undersystem av Linux-kärnan - namnutrymmen.

Kubernetes

Som nämnts tidigare är Kubernetes en containerorkestrator. Det fungerar så här: du ger den en pool av maskiner och säger sedan: "Hej Kubernetes, låt oss starta tio instanser av min behållare med två processorer och 2 GB minne vardera och hålla dem igång!" Kubernetes tar hand om resten. Den kommer att hitta ledig kapacitet, starta behållare och starta om dem vid behov, rulla ut uppdateringar vid versionsbyte, etc. Kubernetes låter dig i huvudsak abstrahera bort hårdvarukomponenten och gör en mängd olika system lämpliga för att distribuera och köra applikationer.

CPU-gränser och aggressiv strypning i Kubernetes
Kubernetes från lekmannens synvinkel

Vad är förfrågningar och begränsningar i Kubernetes

Okej, vi har täckt containrar och Kubernetes. Vi vet också att flera containrar kan ligga på samma maskin.

En analogi kan dras med en gemensam lägenhet. En rymlig lokal (maskiner/enheter) tas och hyrs ut till flera hyresgäster (containrar). Kubernetes fungerar som mäklare. Frågan uppstår, hur man håller hyresgäster från konflikter med varandra? Tänk om någon av dem, säg, bestämmer sig för att låna badrummet halva dagen?

Det är här förfrågningar och begränsningar kommer in i bilden. CPU FÖRFRÅGAN behövs endast för planeringsändamål. Detta är ungefär en "önskelista" för behållaren, och den används för att välja den mest lämpliga noden. Samtidigt processorn Begränsa kan jämföras med ett hyresavtal - så snart vi väljer en enhet för containern, den kan inte gå över fastställda gränser. Och det är här problemet uppstår...

Hur förfrågningar och begränsningar implementeras i Kubernetes

Kubernetes använder en strypmekanism (hoppa över klockcykler) inbyggd i kärnan för att implementera CPU-gränser. Om en applikation överskrider gränsen aktiveras strypning (dvs. den tar emot färre CPU-cykler). Förfrågningar och begränsningar för minne är organiserade på olika sätt, så de är lättare att upptäcka. För att göra detta, kontrollera bara den senaste omstartstatusen för podden: om den är "OOMKilled". CPU-strypning är inte så enkelt, eftersom K8s bara gör mätvärden tillgänglig genom användning, inte av cgroups.

CPU-förfrågan

CPU-gränser och aggressiv strypning i Kubernetes
Hur CPU-begäran implementeras

För enkelhetens skull, låt oss titta på processen med en maskin med en 4-kärnig CPU som ett exempel.

K8s använder en kontrollgruppsmekanism (cgroups) för att styra allokeringen av resurser (minne och processor). En hierarkisk modell är tillgänglig för det: barnet ärver gränserna för föräldragruppen. Distributionsdetaljerna lagras i ett virtuellt filsystem (/sys/fs/cgroup). I fallet med en processor är detta /sys/fs/cgroup/cpu,cpuacct/*.

K8s använder fil cpu.share att tilldela processorresurser. I vårt fall får rot-cgruppen 4096 andelar av CPU-resurser - 100% av den tillgängliga processorkraften (1 kärna = 1024; detta är ett fast värde). Rotgruppen fördelar resurser proportionellt beroende på andelar av ättlingar registrerade i cpu.share, och de i sin tur gör detsamma med sina ättlingar osv. På en typisk Kubernetes-nod har rot-cgruppen tre barn: system.slice, user.slice и kubepods. De två första undergrupperna används för att fördela resurser mellan kritiska systembelastningar och användarprogram utanför K8:er. Sista - kubepods — skapad av Kubernetes för att fördela resurser mellan pods.

Diagrammet ovan visar att den första och andra undergruppen fick vardera 1024 aktier, med kuberpod-undergruppen tilldelad 4096 aktier Hur är detta möjligt: ​​trots allt har rotgruppen endast tillgång till 4096 aktier, och summan av hennes ättlingars andelar överstiger betydligt detta antal (6144)? Poängen är att värdet är logiskt logiskt, så Linux-schemaläggaren (CFS) använder det för att proportionellt allokera CPU-resurser. I vårt fall får de två första grupperna 680 reala aktier (16,6% av 4096), och kubepod får de återstående 2736 aktier Vid driftstopp kommer de två första grupperna inte att använda de tilldelade resurserna.

Lyckligtvis har schemaläggaren en mekanism för att undvika att slösa oanvända CPU-resurser. Den överför "ledig" kapacitet till en global pool, från vilken den distribueras till grupper som behöver ytterligare processorkraft (överföringen sker i omgångar för att undvika avrundningsförluster). En liknande metod tillämpas på alla ättlingar till ättlingar.

Denna mekanism säkerställer en rättvis fördelning av processorkraft och säkerställer att ingen process "stjäl" resurser från andra.

CPU-gräns

Trots att konfigurationerna av gränser och förfrågningar i K8s ser likadana ut, är deras implementering radikalt annorlunda: detta mest vilseledande och den minst dokumenterade delen.

K8s engagerar sig CFS kvotmekanism att implementera gränser. Deras inställningar anges i filer cfs_period_us и cfs_quota_us i cgroup-katalogen (filen finns också där cpu.share).

Till skillnad från cpu.share, baseras kvoten på tidsperiodoch inte på tillgänglig processorkraft. cfs_period_us anger varaktigheten av perioden (epoken) - den är alltid 100000 100 μs (8 ms). Det finns ett alternativ att ändra detta värde i KXNUMXs, men det är bara tillgängligt i alfa för tillfället. Schemaläggaren använder epok för att starta om använda kvoter. Andra filen cfs_quota_us, anger tillgänglig tid (kvot) i varje epok. Observera att det också anges i mikrosekunder. Kvoten får överskrida epoklängden; med andra ord kan det vara större än 100 ms.

Låt oss titta på två scenarier på 16-kärniga maskiner (den vanligaste typen av dator vi har på Omio):

CPU-gränser och aggressiv strypning i Kubernetes
Scenario 1: 2 trådar och en gräns på 200 ms. Ingen strypning

CPU-gränser och aggressiv strypning i Kubernetes
Scenario 2: 10 trådar och 200 ms gräns. Strypningen börjar efter 20 ms, åtkomst till processorresurser återupptas efter ytterligare 80 ms

Låt oss säga att du ställer in CPU-gränsen till 2 kärnor; Kubernetes kommer att översätta detta värde till 200 ms. Detta innebär att behållaren kan använda maximalt 200ms CPU-tid utan strypning.

Och det är här det roliga börjar. Som nämnts ovan är den tillgängliga kvoten 200 ms. Om du arbetar parallellt tio trådar på en 12-kärnig maskin (se illustration för scenario 2), medan alla andra pods är lediga, kommer kvoten att vara slut på bara 20 ms (eftersom 10 * 20 ms = 200 ms), och alla trådar i denna pod kommer att hänga » (strypa) för de kommande 80 ms. De redan nämnda schemaläggaren bugg, på grund av vilket överdriven strypning inträffar och behållaren inte ens kan uppfylla den befintliga kvoten.

Hur utvärderar man strypning i baljor?

Logga bara in på podden och kör cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods — Det totala antalet schemaläggningsperioder.
  • nr_throttled — antal strypningsperioder i sammansättningen nr_periods;
  • throttled_time — kumulativ strypningstid i nanosekunder.

CPU-gränser och aggressiv strypning i Kubernetes

Vad är det som händer egentligen?

Som ett resultat får vi hög strypning i alla applikationer. Ibland är han med en och en halv gång starkare än beräknat!

Detta leder till olika fel - beredskapskontrollfel, behållare fryser, nätverksanslutningsavbrott, timeouts inom servicesamtal. Detta resulterar i slutändan i ökad latens och högre felfrekvens.

Beslut och konsekvenser

Allt är enkelt här. Vi övergav CPU-gränserna och började uppdatera OS-kärnan i kluster till den senaste versionen, där buggen fixades. Antalet fel (HTTP 5xx) i våra tjänster minskade omedelbart markant:

HTTP 5xx-fel

CPU-gränser och aggressiv strypning i Kubernetes
HTTP 5xx-fel för en kritisk tjänst

Svarstid p95

CPU-gränser och aggressiv strypning i Kubernetes
Kritisk fördröjning för tjänstbegäran, 95:e percentilen

Operations kostnader

CPU-gränser och aggressiv strypning i Kubernetes
Antal tillbringade instanstimmar

Vad är fångsten?

Som det står i början av artikeln:

En analogi kan dras med en gemensam lägenhet... Kubernetes fungerar som mäklare. Men hur ska man hålla hyresgästerna från konflikter med varandra? Tänk om någon av dem, säg, bestämmer sig för att låna badrummet halva dagen?

Här är haken. En slarvig behållare kan äta upp alla tillgängliga CPU-resurser på en maskin. Om du har en smart applikationsstack (till exempel JVM, Go, Node VM är korrekt konfigurerade), är detta inte ett problem: du kan arbeta under sådana förhållanden under lång tid. Men om applikationer är dåligt optimerade eller inte optimerade alls (FROM java:latest), kan situationen komma utom kontroll. På Omio har vi automatiserat bas Dockerfiler med adekvata standardinställningar för huvudspråkstacken, så det här problemet fanns inte.

Vi rekommenderar att du övervakar mätvärdena ANVÄNDNING (användning, mättnad och fel), API-fördröjningar och felfrekvenser. Se till att resultatet motsvarar förväntningarna.

referenser

Det här är vår historia. Följande material hjälpte till att förstå vad som hände:

Kubernetes felrapporter:

Har du stött på liknande problem i din praktik eller har du erfarenhet av strypning i containeriserade produktionsmiljöer? Dela din berättelse i kommentarerna!

PS från översättaren

Läs även på vår blogg:

Källa: will.com

Lägg en kommentar