Prestandajusteringsscenario: Distribuerade affärstransaktioner

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 vi gjorde för ett exempelprogram. Programmet är från baslinjen Azure Kubernetes Service (AKS) för mikrotjänster.

Den här artikeln ingår i en serie. Läs den första delen här.

Scenario:Ett klientprogram initierar en affärstransaktion som omfattar flera steg.

Det här scenariot omfattar ett program för drönarleverans som körs på AKS. Kunder använder en webbapp för att schemalägga leveranser efter drönare. Varje transaktion kräver flera steg som utförs av separata mikrotjänster på backend-delen:

  • Leveranstjänsten hanterar leveranser.
  • Tjänsten Drone Scheduler schemalägger drönare för upphämtning.
  • Pakettjänsten hanterar paket.

Det finns två andra tjänster: En inmatningstjänst som accepterar klientbegäranden och placerar dem i en kö för bearbetning, och en arbetsflödestjänst som samordnar stegen i arbetsflödet.

Diagram som visar det distribuerade arbetsflödet

Mer information om det här scenariot finns i Utforma en arkitektur för mikrotjänster.

Test 1: Baslinje

För det första belastningstestet skapade teamet ett AKS-kluster med sex noder och distribuerade tre repliker av varje mikrotjänst. Belastningstestet var ett steg-belastningstest som börjar på två simulerade användare och ökar till 40 simulerade användare.

Inställning Värde
Klusternoder 6
Skida 3 per tjänst

I följande diagram visas resultatet av belastningstestet, som du ser i Visual Studio. Den lila linjen ritar användarbelastningen och den orangefärgade linjen ritar totalt antal begäranden.

Graph av Visual Studio belastningstestresultat

Det första du ska tänka på i det här scenariot är att klientbegäranden per sekund inte är ett användbart prestandamått. Det beror på att programmet bearbetar begäranden asynkront, så klienten får ett svar direkt. Svarskoden är alltid HTTP 202 (accepterad), vilket innebär att begäran accepterades men bearbetningen är inte slutförd.

Det vi verkligen vill veta är om backend håller takten med begärandehastigheten. Kön Service Bus kan absorbera toppar, men om backend inte kan hantera en varaktig belastning kommer bearbetningen att ligga längre och längre efter.

Här är ett mer informativt diagram. Den ritar antalet inkommande och utgående meddelanden i Service Bus kön. Inkommande meddelanden visas i ljusblått och utgående meddelanden visas i mörkblått:

Graph inkommande och utgående meddelanden

Det här diagrammet visar att frekvensen för inkommande meddelanden ökar, når en topp och sedan sjunker tillbaka till noll i slutet av belastningstestet. Men antalet utgående meddelanden når sin topp tidigt i testet och sjunker faktiskt. Det innebär att arbetsflödestjänsten, som hanterar begärandena, inte håller koll. Även efter att belastningstestet har avslutats (cirka 9:22 i diagrammet) bearbetas meddelanden fortfarande när arbetsflödestjänsten fortsätter att tömma kön.

Vad gör bearbetningen långsammare? Det första du ska leta efter är fel eller undantag som kan tyda på ett systematiskt problem. Programkartan i Azure Monitor visar diagrammet över anrop mellan komponenter och är ett snabbt sätt att upptäcka problem och sedan klicka vidare för att få mer information.

Det räcker med att programkartan visar att arbetsflödestjänsten får fel från leveranstjänsten:

Skärmbild av programkarta

Om du vill se mer information kan du välja en nod i diagrammet och klicka på en transaktionsvy från end-to-end. I det här fallet visar den att leveranstjänsten returnerar HTTP 500-fel. Felmeddelandena anger att ett undantag uppstår på grund av minnesgränser i Azure Cache for Redis.

Skärmbild av transaktionsvyn från slut till slut

Du kanske märker att dessa anrop till Redis inte visas i programkartan. Det beror på att .NET-biblioteket för Application Insights inte har inbyggt stöd för att spåra Redis som ett beroende. (En lista över vad som stöds i rutan finns i Dependency auto-collection.) Som reserv kan du använda TrackDependency-API:et för att spåra alla beroenden. Belastningstestning avslöjar ofta dessa typer av luckor i telemetrin, som kan åtgärdas.

Test 2: Ökad cachestorlek

För det andra belastningstestet ökade utvecklingsteamet cachestorleken i Azure Cache for Redis. (Se How to Scale Azure Cache for Redis.) Den här ändringen löste minnesfel, och nu visar programkartan noll fel:

Skärmbild av programkartan som visar att en ökning av cachestorleken löste undantagen om att minnet var slut.

Bearbetningen av meddelanden är dock fortfarande mycket eftersläpning. Vid belastningstestet är den inkommande meddelandefrekvensen större än 5 × utgående hastighet:

Graph inkommande och utgående meddelanden som visar inkommande meddelandefrekvens är mer än 5 gånger det utgående priset.

Följande diagram mäter dataflödet när det gäller slutförande av meddelanden– det vill säga den hastighet med vilken arbetsflödestjänsten markerar Service Bus meddelanden som slutförda. Varje punkt i diagrammet representerar 5 sekunders data, vilket visar ett maximalt dataflöde på ~16 per sekund.

Graph av meddelandegenomflöde

Det här diagrammet genererades genom att köra en fråga i Log Analytics-arbetsytan med hjälp av Kusto-frågespråket:

let start=datetime("2020-07-31T22:30:00.000Z");
let end=datetime("2020-07-31T22:45:00.000Z");
dependencies
| where cloud_RoleName == 'fabrikam-workflow'
| where timestamp > start and timestamp < end
| where type == 'Azure Service Bus'
| where target has 'https://dev-i-iuosnlbwkzkau.servicebus.windows.net'
| where client_Type == "PC"
| where name == "Complete"
| summarize succeeded=sumif(itemCount, success == true), failed=sumif(itemCount, success == false) by bin(timestamp, 5s)
| render timechart

Test 3: Skala ut backend-tjänsterna

Det verkar som att backend-delen är flaskhalsen. Ett enkelt nästa steg är att skala ut affärstjänsterna (Paket, Leverans och Drone Scheduler) och se om dataflödet förbättras. För nästa belastningstest skalade teamet ut tjänsterna från tre repliker till sex repliker.

Inställning Värde
Klusternoder 6
Inmatningstjänst 3 repliker
Arbetsflödestjänst 3 repliker
Tjänster för paket, leverans, drone scheduler 6 repliker vardera

Det här belastningstestet visar tyvärr bara en liten förbättring. Utgående meddelanden håller fortfarande inte koll på inkommande meddelanden:

Graph inkommande och utgående meddelanden som visar att utgående meddelanden fortfarande inte håller koll på inkommande meddelanden.

Dataflödet är mer konsekvent, men det maximala uppnådda är ungefär detsamma som i föregående test:

Graph av meddelandegenomflödet som visar att det högsta uppnådda värdet är ungefär detsamma som i föregående test.

Om vi tittar på Azure Monitor för containrarverkar det dessutom som att problemet inte orsakas av resursutmattning i klustret. För det första visar måtten på nodnivå att processoranvändningen är under 40 % även vid den 95:e percentilen och minnesanvändningen är cirka 20 %.

Graph av AKS-nodanvändning

I en Kubernetes-miljö är det möjligt att enskilda poddar är resursbegränsade även om noderna inte är det. Men poddnivåvyn visar att alla poddar är felfria.

Graph av AKS-poddanvändning

Från det här testet verkar det som om det inte hjälper att bara lägga till fler poddar i backend-delen. Nästa steg är att titta närmare på arbetsflödestjänsten för att förstå vad som händer när den bearbetar meddelanden. Program Insights visar att den genomsnittliga varaktigheten för arbetsflödestjänstens Process åtgärd är 246 ms.

Skärmbild av Insights

Vi kan också köra en fråga för att hämta mått för enskilda åtgärder inom varje transaktion:

Mål percentile_duration_50 percentile_duration_95
https://dev-i-iuosnlbwkzkau.servicebus.windows.net/ | dev-i-iuosnlbwkzkau 86.66950203 283.4255578
leverans 37 57
package 12 17
dronescheduler 21 41

Den första raden i den här tabellen representerar Service Bus kön. De andra raderna är anropen till backend-tjänsterna. Här är Log Analytics-frågan för den här tabellen som referens:

let start=datetime("2020-07-31T22:30:00.000Z");
let end=datetime("2020-07-31T22:45:00.000Z");
let dataset=dependencies
| where timestamp > start and timestamp < end
| where (cloud_RoleName == 'fabrikam-workflow')
| where name == 'Complete' or target in ('package', 'delivery', 'dronescheduler');
dataset
| summarize percentiles(duration, 50, 95) by target

Skärmbild av Log Analytics-frågeresultat

Dessa svarstider ser rimlig ut. Men här är den viktigaste insikten: Om den totala åtgärdstiden är ~250 ms, sätter det en strikt övre gräns för hur snabbt meddelanden kan bearbetas seriellt. Nyckeln till att förbättra dataflödet är därför större parallellitet.

Det bör vara möjligt i det här scenariot, av två skäl:

  • Det här är nätverksanrop, så den mesta av tiden går åt till att vänta på I/O-slutförande
  • Meddelandena är oberoende och behöver inte bearbetas i ordning.

Test 4: Öka parallellitet

I det här testet fokuserade teamet på att öka parallellitet. För att göra det justerar de två inställningar på Service Bus som används av arbetsflödestjänsten:

Inställning Beskrivning Standardvärde Nytt värde
MaxConcurrentCalls Det maximala antalet meddelanden som ska bearbetas samtidigt. 1 20
PrefetchCount Hur många meddelanden klienten hämtar i förväg till sin lokala cache. 0 3000

Mer information om de här inställningarna finns i Best Practices for performance improvements using Service Bus Messaging. När du körde testet med de här inställningarna skapade du följande diagram:

Graph inkommande och utgående meddelanden som visar hur många utgående meddelanden som faktiskt överstiger det totala antalet inkommande meddelanden.

Kom ihåg att inkommande meddelanden visas i ljusblått och utgående meddelanden visas i mörkblått.

Vid en första anblick är det här en mycket graf. Under en stund spårar den utgående meddelandefrekvensen exakt den inkommande frekvensen. Men vid ungefär 2:03-markeringen planar frekvensen för inkommande meddelanden ut, medan antalet utgående meddelanden fortsätter att öka, vilket faktiskt överstiger det totala antalet inkommande meddelanden. Det verkar omöjligt.

Ledtråden till detta hittar du i vyn Beroenden i Application Insights. Det här diagrammet sammanfattar alla anrop som arbetsflödestjänsten har gjort för att Service Bus:

Graph beroendeanrop

Observera att posten för DeadLetter . Anropen anger att meddelanden förs till Service Bus kö för dead-letter.

För att förstå vad som händer måste du förstå Peek-Lock-semantiken i Service Bus. När en klient använder Peek-Lock Service Bus atomiskt ett meddelande. När låset hålls kvar kommer meddelandet garanterat inte att levereras till andra mottagare. Om låset upphör att gälla blir meddelandet tillgängligt för andra mottagare. Efter ett maximalt antal leveransförsök (vilket kan konfigureras) Service Bus du meddelandena i en kö för oställbara meddelanden,där de kan undersökas senare.

Kom ihåg att arbetsflödestjänsten förinstallerar stora batchar med meddelanden – 3 000 meddelanden i taget). Det innebär att den totala tiden för att bearbeta varje meddelande är längre, vilket resulterar i uppnådd time out för meddelanden, att gå tillbaka till kön och så småningom gå till kön för dead letter.

Du kan också se det här beteendet i undantagen, MessageLostLockException där flera undantag registreras:

Skärmbild av programundantag Insights visar flera MessageLostLockException-undantag.

Test 5: Öka låsvaraktigheten

För det här belastningstestet var varaktigheten för meddelandelås inställd på 5 minuter för att förhindra timeouter för lås. Diagrammet över inkommande och utgående meddelanden visar nu att systemet håller takten med frekvensen för inkommande meddelanden:

Graph inkommande och utgående meddelanden som visar att systemet håller takten med frekvensen för inkommande meddelanden.

Under den totala varaktigheten för 8-minuters belastningstestet slutförde programmet 25 000 åtgärder, med ett högsta dataflöde på 72 åtgärder per sekund, vilket representerar en ökning på 400 % av det maximala dataflödet.

Graph av meddelandegenomflödet som visar en ökning på 400 % av det maximala dataflödet.

Men att köra samma test med en längre varaktighet visade att programmet inte kunde hantera den här frekvensen:

Graph inkommande och utgående meddelanden som visar att programmet inte kunde hantera den här frekvensen.

Containermåtten visar att den maximala processoranvändningen var nära 100 %. Nu verkar programmet vara CPU-bundet. Att skala klustret kan förbättra prestandan nu, till skillnad från det tidigare försöket att skala ut.

Graph av AKS-nodanvändningen som visar att den maximala processoranvändningen var nära 100 %.

Test 6: Skala ut backend-tjänsterna (igen)

För det sista belastningstestet i serien skalade teamet ut Kubernetes-klustret och poddarna på följande sätt:

Inställning Värde
Klusternoder 12
Inmatningstjänst 3 repliker
Arbetsflödestjänst 6 repliker
Tjänster för paket, leverans, drone scheduler 9 repliker vardera

Det här testet resulterade i ett högre varaktigt dataflöde, utan betydande fördröjningar i bearbetningen av meddelanden. Dessutom låg nodens CPU-användning under 80 %.

Graph av meddelandegenomflödet som visar ett högre varaktigt dataflöde, utan betydande fördröjningar i bearbetningen av meddelanden.

Sammanfattning

I det här scenariot identifierades följande flaskhalsar:

  • Undantag utanför minnet i Azure Cache for Redis.
  • Brist på parallellitet i meddelandebearbetning.
  • Otillräcklig varaktighet för meddelandelås, vilket leder till att timeouter och meddelanden som placeras i kön för dead letter (död bokstav) låses.
  • Processorbelastning.

För att diagnostisera dessa problem förlitade sig utvecklingsteamet på följande mått:

  • Frekvensen för inkommande och utgående Service Bus meddelanden.
  • Programkarta i Application Insights.
  • Fel och undantag.
  • Anpassade Log Analytics-frågor.
  • Processor- och minnesanvändning i Azure Monitor för containrar.

Nästa steg

Mer information om utformningen av det här scenariot finns i Designa en arkitektur för mikrotjänster.