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:
- Deserialisering av JSON – typiskt för många API:er.
- Simulerat databas-anrop – en fördröjning på 10 millisekunder för att efterlikna en databasfråga.
- CPU-bound beräkning – en klassisk Fibonacci-beräkning för att belasta processorn.
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":
- Latency: Hur lång tid tar det att få svar?
- Traffic: Hur många anrop per sekund klarar API:et?
- Errors: Hur många fel (t.ex. HTTP 500) uppstår?
- Saturation: Hur mycket CPU och minne används?
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
- P50: Medianen – 50% av anropen är snabbare än detta värde.
- P90: 90% av anropen är snabbare än detta värde.
- P99: 99% av anropen är snabbare än detta värde.
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
- CAdvisor samlar in data om alla containers.
- Prometheus hämtar och lagrar tidsseriedata från CAdvisor.
- Grafana används för att visualisera data i dashboards, t.ex. för CPU- och minnesanvändning samt latency och fel.
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 |