Go är inte medveten om de gränser som gäller för dess behållare, vilket leder till vissa problem som är svåra att spåra. Det här är en berättelse om hur jag råkade stöta på ett av dem.
I den här artikeln kommer jag att ta upp ett specifikt problem som uppstod i en av våra produktionsmiljöer, go om den bakomliggande orsaken och en möjlig lösning. Rubriken ger en ledtråd om det underliggande problemet, men vi kommer att gå igenom i detalj vad som hände.
Jag fick en felrapport om att det uppstår ett 502 Bad Gateway-fel när en av våra API-tjänstslutpunkter, i en specifik produktionsmiljö, anropas. Anropen för samma API-slutpunkt lyckas i alla andra produktionsmiljöer, men inte i den här.
Började med att kontrollera de vanliga sakerna. Felloggar, ingenting. Försökte reproducera det, problemet finns fortfarande kvar, men det finns inga felloggar eller andra loggar alls. Jag märkte att Kubernetes pod-containern startas om varje gång jag anropar API-tjänstens slutpunkt. Den första tanken som dök upp var att tjänsten fick panik och containern startade om, så det måste finnas loggar i den föregående podinstansen. Kollade där, ingenting. Sedan lade jag till fler informationsloggar för att se exakt var problemet uppstod. Det pekade på att problemet uppstod när json.Marshal Funktionsanropet kördes, men det fanns inget konstigt i koden. Det var dags att gräva lite mer.
Jag skiftade mitt fokus till Kubernetes-poden och hur den beter sig när problemet uppstår. Medan den nya poden startar märker jag att den gamla poden har OOMDödad status. OOMKilled? Men det är slut på minne-status när containern inuti poden överskrider den definierade RAM-gränsen. Vad i helvete försöker detta API-slutpunktssvar returnera?? För att vara rättvis byggde vi denna slutpunkt för att hämta bulkdata för vår mobilapp så att den kan fungera i offline-läge. Ändå förväntade jag mig inte att det skulle finnas så mycket data eftersom det här är resursgränserna för API-tjänstcontainern:
# part of the deployment file - name: api image: gcr.io/xyz/api:989d83a73a9d1605ac601e3ef0fdef5711dcffe4 imagePullPolicy: Always ports: - containerPort: 5500 resources: limits: cpu: 200m memory: 400Mi requests: cpu: 100m memory: 200MiRAM-gränsen för API-tjänstens container är 400 MB. Jag fördubblade RAM-gränserna för att se hur långt problemet går. Distributionen konfigurerades om och en ny pod med de nya containergränserna driftsattes. Jag utlöste API-slutpunkten igen och den returnerade svaret utan problem. Storleken på svaret är 66 MB.
Hmm, API-tjänstcontainern med ett RAM-minnesbegränsning på 400 MB borde ha kunnat hantera svaret på 66 MB utan problem. Hur kunde den då ge felmeddelandet ” go ” på grund av minnesbrist? Efter att ha läst igenom en del dokumentation och artiklar om hur Go fungerar inuti (se referenserna i slutet) började jag få vissa misstankar om Go:s sopuppsamlare.
Två saker är viktiga för att förklara hur problemet manifesterar sig.
Först och främst: Go:s sopuppsamlare. Jag ska inte go gå in på alla detaljer om hur Go:s sopuppsamlare fungerar. Jag har länkat till några bra artiklar i slutet om du vill läsa mer om detaljerna. En viktig detalj för det aktuella problemet är att Go:s sopuppsamlare utökar heap-minnet när det behövs. Vid varje körning av Go:s sopuppsamlare avgör den om mer minne behöver tilldelas, och tilldelar det sedan.
Nu är det dags att åter go era till artikelns rubrik. När programmet Go (som en enskild process) körs i en container i en Kubernetes-pod känner det som standard inte till de resursbegränsningar som har ställts in för den container där det körs. Låt oss till exempel anta att vår container körs på en maskin med flera kärnor och 8 GB RAM-minne. Om vi antar att heapet ständigt växer kommer Go -körningen så småningom att försöka allokera allt tillgängligt minne av dessa 8 GB, även om andra containrar är distribuerade på samma maskin och kör andra processer.
Genom att kombinera dessa fakta får vi fram hur problemet manifesterar sig. Medan vi utför json.Marshal När funktionen ” Go” körs upptäcker dess GC att det behövs mer heap-minne och allokerar det. Detta görs upprepade gånger under flera GC-cykler. När processen i containern överskrider minnesgränsen (400 MB) avslutar containerns systemkärna processen med ett ”out-of-memory”-fel (OOM).
Låt oss kontrollera GC-spårningsloggarna för att få lite data om vad som hände. Du kan kontrollera dessa loggar genom att ställa in miljövariabeln GODEBUG=gctrace=1Det finns några sätt att göra det på, jag har lagt till det i min Dockerfile.
#The last part of the multi-step build # Build minimal image FROM alpine COPY --from=0 /go/bin/api /go/bin/api ENV GODEBUG gctrace=1 # Expose port EXPOSE 5500 # Run ENTRYPOINT [ "/go/bin/api" ]Om man tittar på loggarna nedan finns det ytterligare en sak att notera. Det finns ytterligare en miljövariabel, GOGC, som avgör hur mycket den allokerade heapen kommer att växa innan nästa GC-körning. GOGC är som standard inställt på 100, vilket innebär att den kommer att växa med 100 %.
{"level":"info","msg":"GetBulkDmsFiles: done","time":"2024-02-25T16:06:08Z"}
{"level":"info","msg":"marshal response","time":"2024-02-25T16:06:08Z"}
gc 14 @247.533s 0%: 0.073+195+0.040 ms clock, 0.14+0/10/97+0.080 ms cpu, 138->138->117 MB, 138 MB goal, 0 MB stacks, 0 MB globals, 2 P
gc 15 @248.448s 0%: 0.061+280+0.041 ms clock, 0.12+0/89/2.9+0.082 ms cpu, 309->309->260 MB, 234 MB goal, 0 MB stacks, 0 MB globals, 2 PAv loggfilerna framgår det att heapet växer vid varje GC-körning. Observera att den sista posten uppgår till 260 MB, vilket är den mängd data som programmet ” Go ” hann skriva innan det avbröts. Vid nästa GC-körning skulle heapet växa till över 500 MB, vilket överstiger gränsen på 400 MB som angetts i distributionsfilen för API-tjänstbehållaren.
Allas favoritsvar, det beror på. Vissa föredrar att lämna det som det är, för att ha en container som snabbt misslyckas, för att inte mixtra med hur GC:n fungerar och för att hantera själva funktionaliteten. Denna API-slutpunkt som returnerar bulkdata kunde ha skrivits om för att strömma tillbaka data, snarare än att skicka ett enda stort svar.
Om det dock stör dig att ditt Go -program och Kubernetes inte riktigt är synkroniserade finns det ett sätt att justera det så att det fungerar smidigare. Det GOMEMLIMIT Miljövariabeln, som infördes i Go 1.19, kan användas som en mjuk minnesgräns för GC. Ett OOM-fel kan fortfarande inträffa, men GC kommer att arbeta mer aggressivt när gränsen nås och se till att utnyttja hela den aktiva heapen istället för att fortsätta utöka heapen bortom gränsen.
I API-tjänstens YAML-fil för distribution har jag lagt till denna miljökonfiguration:
env: - name: GOMEMLIMIT valueFrom: resourceFieldRef: resource: limits.memoryDe GOMEMLIMIT är inställd på det som definieras som en minnesgräns i distributionens YAML-fil. På så sätt, när jag ändrar minnesgränserna, GOMEMLIMIT kommer att ändras i enlighet därmed.
Det vore bättre att kunna definiera något i stil med "90 % av minnesgränserna", men det finns fortfarande inget bra sätt att göra detta, annat än att kanske använda requests.memory. Detta utfärda är fortfarande öppen. Den automemlimit biblioteket gör det (och mer, med cgroups), men det måste inkluderas i koden.
När jag anropar samma slutpunkt returnerar den svaret utan problem. Här är GC-loggarna och hur minnet ser ut före och efter anropet.
{"level":"info","msg":"GetBulkDmsFiles: done","time":"2024-02-25T16:31:01Z"}
{"level":"info","msg":"marshal response","time":"2024-02-25T16:31:01Z"}
gc 18 @92.420s 0%: 0.053+190+0.097 ms clock, 0.10+0/98/95+0.19 ms cpu, 115->115->93 MB, 125 MB goal, 0 MB stacks, 0 MB globals, 2 P
gc 19 @93.019s 0%: 0.066+102+0.035 ms clock, 0.13+0/4.0/89+0.071 ms cpu, 189->189->165 MB, 186 MB goal, 0 MB stacks, 0 MB globals, 2 P
{"level":"info","msg":"marshal response done","time":"2024-02-25T16:31:02Z"}
gc 20 @93.821s 0%: 0.066+193+0.004 ms clock, 0.13+0/2.6/3.6+0.009 ms cpu, 359->359->197 MB, 330 MB goal, 0 MB stacks, 0 MB globals, 2 P
{"level":"info","msg":"response writer write done","time":"2024-02-25T16:31:10Z"}
Den presenterade lösningen är ingen mirakelkur. Det finns scenarier där det är att föredra att snabbt misslyckas med standardinställningarna framför att pressa ut all vätska ur högen innan processen avslutas med OOM-felet.
Med det sagt kan den presenterade lösningen bidra till bättre resursutnyttjande, särskilt när flera tjänster distribueras på samma maskin.
Det finns redan bra artiklar om det här ämnet. Jag fick reda på dem ganska sent, det hade sparat mig mycket tid (om jag inte hade skrivit den här haha). Du borde läsa den för att bli ännu mer bekant med ämnet. Jag har inkluderat dem i referenserna nedan.
Hör gärna av dig om det finns liknande och mer eleganta lösningar för att finjustera dina Kubernetes-resurser och Go -program – jag skulle gärna vilja höra om dem!
GOMEMLIMIT som har mycket fler detaljer – https://weaviate.io/blog/gomemlimit-a-game-changer-for-high-memory-applications