API Performance Tests del 1: Go vs ASP.NET & More

Blog Post by Stefan Holmberg

Introduktion: Prestandatester av API:er i Olika Programmeringsspråk

Att välja rätt programmeringsspråk och ramverk för att bygga API:er är en fråga som ofta väcker diskussioner bland utvecklare. Vilket språk är snabbast? Vilket är mest resurssnålt? Och hur påverkar valet av språk och implementation prestandan under hög belastning? I denna artikel går vi igenom en serie prestandatester där samma API byggs i olika språk – med fokus på Go och ASP.NET (C#) – och körs i en modern molnmiljö med Kubernetes, Docker och övervakningsverktyg som Prometheus och Grafana. Vi undersöker mätvärden, flaskhalsar och lärdomar kring asynkron programmering, trådhantering och resursanvändning.

Testupplägg: Samma API i Flera Språk

API:ets Funktionalitet

För att skapa en rättvis jämförelse byggs ett och samma API i flera olika språk. API:et är enkelt: det har en endpoint (/players) som returnerar JSON-data. Under huven görs tre saker:

Detta ger en blandning av I/O-bound och CPU-bound arbetsuppgifter, vilket är representativt för många verkliga API:er.

Miljö och Infrastruktur

För att säkerställa jämförbara resultat körs API:erna i ett Kubernetes-kluster i molnet. Klustret är litet och består av två virtuella maskiner. Varje API körs i en egen Docker-container, och resurser som CPU och minne begränsas explicit i deployment-filerna. Övervakning sker med CAdvisor, Prometheus och Grafana, och lasttestning görs med verktyget "hey" (ett Go-baserat lasttestningsprogram).

Mätvärden: De Fyra "Golden Signals"

Vid prestandamätning av API:er fokuserar man ofta på fyra nyckelvärden, de så kallade "golden signals":

Dessa mätvärden visualiseras i Grafana-dashboards och analyseras för att identifiera flaskhalsar och styrkor i respektive implementation.

Lasttestning och Mätmetodik

Verktyg och Testparametrar

Lasttestningen sker med "hey", där man kan ange antal anrop, antal samtidiga användare och anropsfrekvens. Ett typiskt test kan vara 1500 anrop med 50 samtidiga användare, där varje användare gör ett anrop per sekund. Resultaten analyseras med hjälp av percentiler (P50, P90, P99) för att förstå fördelningen av svarstider.

Percentiler och Vad de Säger

Detta ger en mer nyanserad bild än bara medelvärde, särskilt när det gäller att förstå "long tail"-problem och spikar i svarstider.

Infrastruktur: Kubernetes, Docker och Övervakning

Kubernetes-klustret

API:erna körs i ett Kubernetes-kluster, där varje implementation får en egen deployment och service. Ingress-kontrollers (NGINX) används för att routa trafik till rätt API. Resursbegränsningar (CPU och minne) sätts i deployment-filerna för att möjliggöra rättvis mätning av resursanvändning.

Övervakning med CAdvisor, Prometheus och Grafana

Konfiguration och Automatisering

All konfiguration – från Dockerfiler till Kubernetes YAML-filer och Helm-skript – delas och kan återanvändas av andra som vill göra liknande tester. Det finns även tips om vanliga fallgropar, t.ex. eftersläpningar i rapporteringen mellan övervakningsverktygen.

Kodstruktur och Skillnader

Både Go och ASP.NET-implementationerna gör i grunden samma sak: deserialiserar JSON, simulerar ett databas-anrop och gör en CPU-bound beräkning. Men det finns viktiga skillnader i hur de hanterar asynkrona operationer och trådhantering.

ASP.NET (C#)

I C# används traditionellt Thread.Sleep för att simulera fördröjningar, men detta är problematiskt i en webbmiljö. Varje tråd som sover är otillgänglig för andra anrop, vilket snabbt leder till att trådpoolen tar slut under hög belastning. Lösningen är att använda asynkrona metoder med await Task.Delay, vilket frigör tråden under väntetiden och gör att fler samtidiga anrop kan hanteras.

Go

Go är designat från grunden för parallellism och samtidighet. Alla anrop till standardbiblioteket är redan asynkrona och icke-blockerande. Det finns ingen motsvarighet till await – det är inbyggt i språket och run-timen. Detta gör att Go-applikationer kan hantera mycket högre belastning utan att trådpoolen tar slut.

Exempel på Kodproblem och Optimering I det första testet användes Thread.Sleep i C#-koden, vilket ledde till att API:et snabbt blev överbelastat och började returnera fel (HTTP 500). Efter att ha bytt till await Task.Delay förbättrades prestandan avsevärt, men Go-implementationen var fortfarande betydligt mer resurssnål och stabil under hög belastning.

Resultat: Prestanda och Resursanvändning

Go: Låg Resursanvändning och Hög Stabilitet

Go-API:et klarade 1500 anrop per sekund utan problem. CPU-användningen låg på ca 3,5% och minnesanvändningen på under 32% av den tilldelade limiten. Alla anrop returnerade HTTP 200, och svarstiderna var jämna och låga. Go:s inbyggda parallellism och låga runtime-overhead gör det mycket väl lämpat för högbelastade API:er.

ASP.NET (C#): Högre Footprint och Känsligare för Belastning

ASP.NET-API:et hade initialt stora problem med fel och överbelastning på grund av synkron trådblockering. Efter optimering med asynkrona anrop förbättrades resultatet, men CPU- och minnesanvändningen var fortfarande betydligt högre än för Go. Svarstiderna var mer varierande, och det uppstod fortfarande en del fel vid hög belastning.

Skillnader i Garbage Collection

Både Go och C# har garbage collectors, vilket ibland kan ge spikar i svarstider (latency). I Go är dessa spikar mindre påtagliga, medan C#-applikationen kan få större variationer i latency, särskilt under hög belastning. Rust, som saknar garbage collector, förväntas ha ännu jämnare latency (något som ska testas i kommande delar av serien).

Lärdomar om Asynkron Programmering och Trådhantering

Varför är Asynkron Kod Viktigt?

I moderna webbapplikationer är det avgörande att inte blockera trådar i onödan. Synkrona fördröjningar (t.ex. Thread.Sleep) gör att trådpoolen snabbt tar slut, vilket leder till fel och dålig prestanda. Asynkrona metoder (t.ex. await Task.Delay i C#) frigör tråden under väntetiden och möjliggör högre samtidighet.

Go:s Fördelar

Go är designat för samtidighet och parallellism. Alla operationer i standardbiblioteket är redan asynkrona, och det finns ingen risk att "glömma" att använda await. Detta gör Go mycket robust under hög belastning och minskar risken för flaskhalsar relaterade till trådhantering.

C# och Andra C-baserade Språk

C#, Java och andra språk med rötter i C är designade för en tid då datorer hade en CPU och en kärna. För att hantera samtidighet har man lagt till asynkrona mönster och trådpooler, men det kräver att utvecklaren är medveten om och använder dessa korrekt. Annars riskerar man att snabbt slå i taket för vad applikationen klarar av.

Resursbegränsningar och Mätning

Varför Sätta Limits? Genom att sätta explicita begränsningar på CPU och minne i Kubernetes kan man mäta hur mycket resurser varje implementation faktiskt kräver. Detta är viktigt för att kunna jämföra olika språk och ramverk på ett rättvist sätt.

Hur Tolkade Vi Graferna?

I Grafana visualiserades både CPU- och minnesanvändning som procent av den tilldelade limiten. Detta gör det enkelt att se vilken implementation som är mest resurssnål och hur nära man är att slå i taket för tilldelade resurser.

Slutsatser och Rekommendationer

Go: Perfekt för Högbelastade API:er

Go visade sig vara extremt resurssnålt och stabilt under hög belastning. Det är ett utmärkt val för API:er som förväntas hantera många samtidiga användare och höga anropsfrekvenser. Den inbyggda parallellismen och låga runtime-overheaden gör att Go-applikationer kan köras effektivt även på små servrar.

ASP.NET (C#): Bra, men Kräver Optimering

ASP.NET är ett moget och kraftfullt ramverk, men kräver att man skriver asynkron kod för att klara hög belastning. Även med optimeringar är resursförbrukningen högre än för Go, och det finns en större risk för flaskhalsar vid mycket hög samtidighet. För många applikationer är detta dock inget problem, särskilt om belastningen är måttlig.

Välj Språk efter Behov

Det finns ingen "one size fits all". Om din applikation förväntas hantera extremt hög last och du vill minimera infrastrukturkostnader kan Go vara det bästa valet. Om du redan har en stor kodbas i C# och belastningen är rimlig fungerar ASP.NET utmärkt, förutsatt att du skriver asynkron kod.

Mät, Mät och Mät

Oavsett språk är det avgörande att mäta och övervaka din applikation under realistisk last. Använd verktyg som Prometheus och Grafana för att få insikt i resursanvändning, svarstider och fel. Testa olika implementationer och optimera utifrån faktiska mätvärden, inte magkänsla.

Nästa Steg: Fler Språk och Djupare Analyser

Denna artikel är första delen i en serie där fler språk (Java, Python, Rust m.fl.) kommer att testas på samma sätt. Vi kommer även att undersöka hur skillnaderna ser ut vid mer "normala" laster (t.ex. 10–30 anrop per sekund) och vilka optimeringar som kan göras i respektive språk och ramverk.

Följ med i nästa del för djupare analyser, fler jämförelser och praktiska tips för att bygga snabba, skalbara och resurssnåla API:er – oavsett vilket språk du väljer!

Rekommenderade kurser inom detta ämne

.

Kubernetes och Docker för utvecklare
© Copyright Systementor AB. All Rights Reserved
Version: 1.0.20240904.1-74e3242