Prestandajusteringsscenario: Flera backend-tjänster
Den här artikeln beskriver hur ett utvecklingsteam använde mått för att hitta flaskhalsar och förbättra prestanda för ett distribuerat system. Artikeln baseras på faktisk belastningstestning som gjordes för ett exempelprogram. Programmet kommer från Azure Kubernetes Service (AKS)baslinje för mikrotjänster, tillsammans med ett Visual Studio-testprojekt som används för att generera resultaten.
Den här artikeln ingår i en serie. Läs den första delen här.
Scenario:Anropa flera backend-tjänster för att hämta information och aggregera sedan resultaten.
Det här scenariot omfattar ett program för drönarleverans. Klienter kan fråga en REST API för att få sin senaste fakturainformation. Fakturan innehåller en sammanfattning av kundens leveranser, paket och total drönaranvändning. Det här programmet använder en arkitektur för mikrotjänster som körs på AKS och den information som behövs för fakturan sprids över flera mikrotjänster.
I stället för att klienten anropar varje tjänst direkt implementerar programmet mönstret Gateway-sammansättning. Med det här mönstret gör klienten en enskild begäran till en gatewaytjänst. Gatewayen anropar i sin tur backend-tjänsterna parallellt och aggregerar sedan resultatet till en enda svarsnyttolast.

Test 1: Baslinjeprestanda
För att upprätta en baslinje startade utvecklingsteamet med ett steg-belastningstest som ökar belastningen från en simulerad användare upp till 40 användare under en total varaktighet på 8 minuter. Följande diagram, som Visual Studio, visar resultatet. Den lila linjen visar användarbelastningen och den orangefärgade linjen visar dataflöde (genomsnittliga begäranden per sekund).

Den röda linjen längst ned i diagrammet visar att inga fel returnerades till klienten, vilket är uppmuntrande. Det genomsnittliga dataflödet toppar dock ungefär halvvägs genom testet och sjunker sedan under resten, även om belastningen fortsätter att öka. Det indikerar att backend inte kan hålla reda på det. Det mönster som visas här är vanligt när ett system börjar nå resursgränser – efter att ha nått ett maximum, sjunker dataflödet faktiskt avsevärt. Resursknöjning, tillfälliga fel eller en ökning av undantagsfrekvensen kan bidra till det här mönstret.
Nu ska vi gå in på övervakningsdata för att lära oss vad som händer i systemet. Nästa diagram kommer från Application Insights. Den visar de genomsnittliga varaktigheterna för HTTP-anropen från gatewayen till backend-tjänsterna.

Det här diagrammet visar att en åtgärd GetDroneUtilization i synnerhet, , tar mycket längre tid i genomsnitt – med en storleksordning. Gatewayen gör dessa anrop parallellt, så den långsammaste åtgärden avgör hur lång tid det tar för hela begäran att slutföras.
Nästa steg är att undersöka åtgärden GetDroneUtilization och leta efter eventuella flaskhalsar. En möjlighet är resursutmattning. Kanske börjar den här specifika backend-tjänsten få slut på cpu eller minne. För ett AKS-kluster är den här informationen tillgänglig i Azure Portal via funktionen Azure Monitor för containrar. I följande diagram visas resursutnyttjande på klusternivå:

I den här skärmbilden visas både genomsnittliga och högsta värden. Det är viktigt att titta på mer än bara genomsnittet, eftersom medelvärdet kan dölja toppar i data. Här ligger den genomsnittliga processoranvändningen under 50 %, men det finns några toppar på 80 %. Det är nära kapacitet men fortfarande inom toleranser. Något annat orsakar flaskhalsen.
Nästa diagram visar den verkliga boven. Det här diagrammet visar HTTP-svarskoder från leveranstjänstens serverdatabas, som i det här fallet är Cosmos DB. Den blå linjen representerar lyckade koder (HTTP 2xx), medan den gröna linjen representerar HTTP 429-fel. En HTTP 429-returkod innebär att Cosmos DB tillfälligt begränsning av begäranden, eftersom anroparen förbrukar fler resursenheter (RU) än vad som har etablerats.

För att få ytterligare insikter använde utvecklingsteamet Application Insights för att visa telemetri från slutet till slut för ett representativt urval av begäranden. Här är en instans:

Den här vyn visar anrop som är relaterade till en enskild klientbegäran, tillsammans med tidsinformation och svarskoder. De översta anropen kommer från gatewayen till backend-tjänsterna. Anropet GetDroneUtilization till expanderas för att visa anrop till externa beroenden – i det här fallet till Cosmos DB. Anropet i rött returnerade ett HTTP 429-fel.
Observera det stora avståndet mellan HTTP 429-felet och nästa anrop. När Cosmos DB får ett HTTP 429-fel backas det automatiskt av och väntar på att försöka utföra åtgärden igen. Den här vyn visar att den här åtgärden under de 672 ms som tog mest tid gick åt till att vänta på att försöka Cosmos DB.
Här är ett annat intressant diagram för den här analysen. Den visar RU-förbrukning per fysisk partition jämfört med etablerade RU:er per fysisk partition:

För att förstå det här diagrammet måste du förstå hur Cosmos DB hanterar partitioner. Samlingar i Cosmos DB ha en partitionsnyckel. Varje möjligt nyckelvärde definierar en logisk partition av data i samlingen. Cosmos DB distribuerar dessa logiska partitioner över en eller flera fysiska partitioner. Hanteringen av fysiska partitioner hanteras automatiskt av Cosmos DB. När du lagrar mer data kan Cosmos DB flytta logiska partitioner till nya fysiska partitioner för att sprida belastningen över de fysiska partitionerna.
För det här belastningstestet etablerades Cosmos DB samling med 900 RU:er. Diagrammet visar 100 RU per fysisk partition, vilket innebär totalt nio fysiska partitioner. Även Cosmos DB automatiskt hanterar horisontell partitionering av fysiska partitioner, kan vetskapen om antalet partitioner ge insikter om prestanda. Utvecklingsteamet kommer att använda den här informationen senare, allt eftersom de fortsätter att optimera. När den blå linjen passerar den lila vågräta linjen har RU-förbrukningen överskridit de etablerade RU:erna. Det är den punkt där Cosmos DB börjar begränsa anrop.
Test 2: Öka resursenheterna
För det andra belastningstestet skalade teamet ut Cosmos DB från 900 RU till 2 500 RU. Dataflödet ökade från 19 begäranden per sekund till 23 begäranden per sekund och den genomsnittliga svarstiden minskade från 669 ms till 569 ms.
| Metric | Test 1 | Test 2 |
|---|---|---|
| Dataflöde (req/sek) | 19 | 23 |
| Genomsnittlig svarstid (ms) | 669 | 569 |
| Lyckade begäranden | 9,8 K | 11 K |
Det här är inga stora vinster, men om vi tittar på grafen över tid visas en mer fullständig bild:

Föregående test visade en inledande topp följt av en kraftig minskning, men det här testet visar ett mer konsekvent dataflöde. Det maximala dataflödet är dock inte betydligt högre.
Alla begäranden om Cosmos DB returnerade en 2xx-status och HTTP 429-felen gick bort:

Diagrammet över RU-förbrukning jämfört med etablerade RU:er visar att det finns gott om utrymme. Det finns cirka 275 RU:er per fysisk partition och belastningstestet nådde sin topp vid cirka 100 förbrukade RU:er per sekund.

Ett annat intressant mått är antalet anrop till Cosmos DB per lyckad åtgärd:
| Metric | Test 1 | Test 2 |
|---|---|---|
| Anrop per åtgärd | 11 | 9 |
Om inga fel uppstår ska antalet anrop matcha den faktiska frågeplanen. I det här fallet omfattar åtgärden en fråga mellan partitioner som träffar alla nio fysiska partitioner. Det högre värdet i det första belastningstestet visar antalet anrop som returnerade ett 429-fel.
Det här måttet beräknades genom att köra en anpassad Log Analytics-fråga:
let start=datetime("2020-06-18T20:59:00.000Z");
let end=datetime("2020-07-24T21:10:00.000Z");
let operationNameToEval="GET DroneDeliveries/GetDroneUtilization";
let dependencyType="Azure DocumentDB";
let dataset=requests
| where timestamp > start and timestamp < end
| where success == true
| where name == operationNameToEval;
dataset
| project reqOk=itemCount
| summarize
SuccessRequests=sum(reqOk),
TotalNumberOfDepCalls=(toscalar(dependencies
| where timestamp > start and timestamp < end
| where type == dependencyType
| summarize sum(itemCount)))
| project
OperationName=operationNameToEval,
DependencyName=dependencyType,
SuccessRequests,
AverageNumberOfDepCallsPerOperation=(TotalNumberOfDepCalls/SuccessRequests)
Sammanfattningsvis visar det andra belastningstestet förbättringar. Åtgärden tar GetDroneUtilization dock fortfarande ungefär en storlek längre tid än den näst långsammaste åtgärden. Genom att titta på transaktioner från början till slut kan du förklara varför:

Som tidigare nämnts omfattar GetDroneUtilization åtgärden en fråga mellan partitioner för att Cosmos DB. Det innebär Cosmos DB klienten måste förfjäna frågan till varje fysisk partition och samla in resultaten. Som transaktionsvyn från end-to-end visar utförs dessa frågor seriellt. Åtgärden tar så lång tid som summan av alla frågor – och det här problemet blir bara sämre när storleken på data växer och fler fysiska partitioner läggs till.
Test 3: Parallella frågor
Baserat på föregående resultat är ett uppenbart sätt att minska svarstiden att utfärda frågorna parallellt. Klient-SDK Cosmos DB klienten har en inställning som styr den högsta graden av parallellitet.
| Värde | Beskrivning |
|---|---|
| 0 | Ingen parallellitet (standard) |
| > 0 | Maximalt antal parallella anrop |
| -1 | Klient-SDK väljer en optimal grad av parallellitet |
För det tredje belastningstestet ändrades den här inställningen från 0 till -1. I följande tabell sammanfattas resultatet:
| Metric | Test 1 | Test 2 | Test 3 |
|---|---|---|---|
| Dataflöde (req/sek) | 19 | 23 | 42 |
| Genomsnittlig svarstid (ms) | 669 | 569 | 215 |
| Lyckade begäranden | 9,8 K | 11 K | 20 K |
| Begränsade begäranden | 2,72 K | 0 | 0 |
Från belastningstestdiagrammet är dataflödet inte bara mycket högre (den orange linjen), utan dataflödet håller också jämna steg med belastningen (den lila linjen).

Vi kan kontrollera att Cosmos DB-klienten gör frågor parallellt genom att titta på transaktionsvyn från början till slut:

Intressant är att en sidoeffekt av att öka dataflödet är att antalet förbrukade RU:er per sekund också ökar. Även Cosmos DB begränsade inga begäranden under det här testet låg förbrukningen nära den etablerade RU-gränsen:

Det här diagrammet kan vara en signal för att ytterligare skala ut databasen. Men det visar sig att vi kan optimera frågan i stället.
Steg 4: Optimera frågan
Föregående belastningstest visade bättre prestanda vad gäller svarstid och dataflöde. Den genomsnittliga svarstiden för förfrågningar minskades med 68 % och dataflödet ökade med 220 %. Frågan mellan partitioner är dock ett problem.
Problemet med frågor mellan partitioner är att du betalar för RU för varje partition. Om frågan bara körs ibland, till exempel en gång i timmen, spelar det kanske ingen roll. Men när du ser en läsintensiva arbetsbelastning som omfattar en fråga mellan partitioner bör du se om frågan kan optimeras genom att inkludera en partitionsnyckel. (Du kan behöva göra om samlingen så att den använder en annan partitionsnyckel.)
Här är frågan för det här scenariot:
SELECT * FROM c
WHERE c.ownerId = <ownerIdValue> and
c.year = <yearValue> and
c.month = <monthValue>
Den här frågan väljer poster som matchar ett visst ägar-ID och månad/år. I den ursprungliga designen är ingen av dessa egenskaper partitionsnyckeln. Det kräver att klienten förfjärrr frågan till varje fysisk partition och samlar in resultaten. För att förbättra frågeprestandan ändrade utvecklingsteamet designen så att ägar-ID är partitionsnyckeln för samlingen. På så sätt kan frågan rikta in sig på en specifik fysisk partition. (Cosmos DB hanterar detta automatiskt. Du behöver inte hantera mappningen mellan partitionsnyckelvärden och fysiska partitioner.)
Efter att ha bytt samling till den nya partitionsnyckeln, fanns det en dramatisk förbättring av RU-förbrukningen, vilket innebär direkt lägre kostnader.
| Metric | Test 1 | Test 2 | Test 3 | Test 4 |
|---|---|---|---|---|
| RU:er per åtgärd | 29 | 29 | 29 | 3.4 |
| Anrop per åtgärd | 11 | 9 | 10 | 1 |
Transaktionsvyn från slutet till slut visar att frågan som förutsagd endast läser en fysisk partition:

Belastningstestet visar förbättrat dataflöde och svarstid:
| Metric | Test 1 | Test 2 | Test 3 | Test 4 |
|---|---|---|---|---|
| Dataflöde (req/sek) | 19 | 23 | 42 | 59 |
| Genomsnittlig svarstid (ms) | 669 | 569 | 215 | 176 |
| Lyckade begäranden | 9,8 K | 11 K | 20 k | 29 k |
| Begränsade begäranden | 2,72 K | 0 | 0 | 0 |
En följd av den förbättrade prestandan är att nodens processoranvändning blir mycket hög:

Mot slutet av belastningstestet nådde den genomsnittliga cpu:n cirka 90 % och den maximala CPU:n nådde 100 %. Det här måttet anger att CPU är nästa flaskhals i systemet. Om ett högre dataflöde behövs kan nästa steg vara att skala ut leveranstjänsten till fler instanser.
Sammanfattning
I det här scenariot identifierades följande flaskhalsar:
- Cosmos DB begränsningsbegäranden på grund av otillräcklig etablering av RU:er.
- Hög fördröjning som orsakas av att flera databaspartitioner körs i serie.
- Ineffektiv fråga mellan partitioner eftersom frågan inte innehåller partitionsnyckeln.
Dessutom identifierades cpu-användning som en potentiell flaskhals i högre skala. För att diagnostisera dessa problem tittade utvecklingsteamet på:
- Svarstid och dataflöde från belastningstestet.
- Cosmos DB och RU-förbrukning.
- Transaktionsvyn från slutet till slut i Application Insight.
- Processor- och minnesanvändning i Azure Monitor för containrar.
Nästa steg
Granska prestanda antimönster