Share via


Antimönster med upptagen klientdel

Om du utför asynkront arbete på ett stort antal bakgrundstrådar kan tillgången på resurser för andra uppgifter i förgrunden begränsas så att svarstiderna når en oacceptabel nivå.

Problembeskrivning

Resurskrävande uppgifter öka svarstiden för användarförfrågningar och orsaka hög latens. Ett sätt att förbättra svarstiderna är att avlasta resursintensiva aktiviteter i en separat tråd. Den här metoden gör att svarstiderna i programmet hålls nere medan bearbetningen sker i bakgrunden. Uppgifter som körs i en bakgrundstråd använder dock fortfarande resurser. Om det finns för många av dem kan tillgången på resurser begränsas i trådar som hanterar förfrågningar.

Kommentar

Termen resurs kan betyda många olika saker, som CPU-användning, användandet av minne och nätverks- eller disk-I/O.

Det här problemet uppstår vanligen när koden i ett program är monolitisk och all affärslogik är samlad på en enda nivå som delas med presentationslagret.

Här är ett exempel med ASP.NET som visar problemet. Du hittar hela exemplet här.

public class WorkInFrontEndController : ApiController
{
    [HttpPost]
    [Route("api/workinfrontend")]
    public HttpResponseMessage Post()
    {
        new Thread(() =>
        {
            //Simulate processing
            Thread.SpinWait(Int32.MaxValue / 100);
        }).Start();

        return Request.CreateResponse(HttpStatusCode.Accepted);
    }
}

public class UserProfileController : ApiController
{
    [HttpGet]
    [Route("api/userprofile/{id}")]
    public UserProfile Get(int id)
    {
        //Simulate processing
        return new UserProfile() { FirstName = "Alton", LastName = "Hudgens" };
    }
}
  • Metoden Post i kontrollanten WorkInFrontEnd implementerar en HTTP POST-åtgärd. Den här åtgärden simulerar en tidskrävande och processorintensiv aktivitet. Arbetet utförs i en separat tråd så att POST-åtgärden ska kunna slutföras snabbt.

  • Metoden Get i kontrollanten UserProfile implementerar en HTTP GET-åtgärd. Den här metoden är mycket mindre processorintensiv.

Det viktigaste här är resurskraven för metoden Post. Även om arbetet placeras i en bakgrundstråd så kan arbetet fortfarande förbruka en betydande mängd processorresurser. Dessa resurser delas med andra åtgärder som utförs av andra samtidiga användare. Om ett måttligt antal användare skickar denna förfrågan på samma gång kommer prestandan troligen att påverkas, så att alla åtgärder tar längre tid. Användare kan till exempel märka längre svarstider i metoden Get.

Åtgärda problemet

Flytta processer som förbrukar stora mängder resurser till en separat serverdel.

Med den här metoden placerar klientdelen resurskrävande uppgifter i en meddelandekö. Serverdelen hämtar uppgifterna för asynkron bearbetning. Kön fungerar också som belastningsutjämnare och buffrar förfrågningar mot serverdelen. Om kön blir för lång kan du konfigurera automatisk skalning för att skala ut serverdelen.

Här är en ny version av den tidigare koden. I den här versionen placerar metoden Post ett meddelande i Service Bus-kön.

public class WorkInBackgroundController : ApiController
{
    private static readonly QueueClient QueueClient;
    private static readonly string QueueName;
    private static readonly ServiceBusQueueHandler ServiceBusQueueHandler;

    public WorkInBackgroundController()
    {
        var serviceBusConnectionString = ...;
        QueueName = ...;
        ServiceBusQueueHandler = new ServiceBusQueueHandler(serviceBusConnectionString);
        QueueClient = ServiceBusQueueHandler.GetQueueClientAsync(QueueName).Result;
    }

    [HttpPost]
    [Route("api/workinbackground")]
    public async Task<long> Post()
    {
        return await ServiceBusQueueHandler.AddWorkLoadToQueueAsync(QueueClient, QueueName, 0);
    }
}

Serverdelen hämtar meddelanden från Service Bus-kön och utför bearbetningen.

public async Task RunAsync(CancellationToken cancellationToken)
{
    this._queueClient.OnMessageAsync(
        // This lambda is invoked for each message received.
        async (receivedMessage) =>
        {
            try
            {
                // Simulate processing of message
                Thread.SpinWait(Int32.MaxValue / 1000);

                await receivedMessage.CompleteAsync();
            }
            catch
            {
                receivedMessage.Abandon();
            }
        });
}

Att tänka på

  • Med den här metoden ökar komplexiteten i programmet något. Du måste hantera att placera och hämta meddelanden i kön på ett säkert sätt så att inga förfrågningar förloras i händelse av fel.
  • Programmet får ett beroende på en ytterligare tjänst för meddelandekön.
  • Bearbetningsmiljön måste vara tillräckligt skalbar för att hantera den förväntade arbetsbelastningen och uppfylla nödvändiga dataflödesmål.
  • Även om den här metoden bör förbättra programmets svarstider i stort kan det ta längre tid att utföra de uppgifter som flyttas till serverdelen.

Identifiera problemet

Symptomen på en upptagen klientdel kan vara långa svarstider för resurskrävande aktiviteter. Slutanvändarna kommer sannolikt att rapportera utökade svarstider eller fel som orsakas av tidsgränsen för tjänsterna. Dessa fel kan också returnera HTTP 500-fel (intern server) eller HTTP 503-fel (tjänsten är inte tillgänglig). Undersök händelseloggarna för webbservern, som troligen innehåller mer detaljerad information felens orsaker och omständigheter.

Du kan göra följande för att identifiera problemet:

  1. Utför processövervakning i produktionssystemet och identifiera punkter med långa svarstider.
  2. Granska telemetridata från de här punkterna och avgör vilken blandning av åtgärder som utförs och vilka resurser som används.
  3. Identifiera korrelationer mellan långa svarstider och vilka volymer och kombinationer av åtgärder som utfördes vid de tidpunkterna.
  4. Belastningstesta varje misstänkt åtgärd och identifiera vilka åtgärder som förbrukar resurser och begränsar resursanvändningen för andra åtgärder.
  5. Granska källkoden för dessa åtgärder och avgör varför de förbrukar onormalt mycket resurser.

Exempeldiagnos

I följande avsnitt används stegen på exempelprogrammet som beskrivs ovan.

Identifiera långsamma punkter

Instrumentera varje metod och spåra vilken tid och vilka resurser som används av varje förfrågan. Övervaka sedan programmet i produktion. Detta kan ge en översikt över hur olika förfrågningar konkurrerar med varandra. Under belastning påverkar troligen långsamma och resurskrävande förfrågningar andra åtgärder, och det här beteendet kan observeras genom att du övervakar systemet och kontrollerar när prestanda försämras.

I följande bild visas en instrumentpanel för övervakning. (Vi använde AppDynamics för våra tester.) Till en början har systemet lätt belastning. Sedan börjar användarna begära UserProfile GET-metoden. Prestanda är förhållandevis bra tills andra användare börjar utfärda förfrågningar till WorkInFrontEnd POST-metoden. Här ökar svarstiderna kraftigt (första pilen). Svarstiderna förbättras först när antalet förfrågningar till kontrollanten WorkInFrontEnd minskar (andra pilen).

AppDynamics Business Transactions pane showing the effects of the response times of all requests when the WorkInFrontEnd controller is used

Granska telemetridata och hitta korrelationer

I nästa bild visas några av de mätvärden som samlats in för att övervaka resursutnyttjandet under samma intervall. Först använder bara några få användare systemet. Allt eftersom fler användare ansluter så blir processoranvändningen mycket hög (100 %). Observera också att nätverkets I/O-hastighet från början går upp när processoranvändningen ökar. Men när processanvändningen når sin topp minskar faktiskt nätverkets I/O-hastighet. Det här beror på att systemet bara kan hantera ett relativt litet antal förfrågningar när processorns fulla kapacitet används. När användarna kopplar från börjar processorbelastningen att minska.

AppDynamics metrics showing the CPU and network utilization

Nu verkar det som att metoden Post i kontrollanten WorkInFrontEnd borde undersökas närmare. Hypotesen måste undersökas i en kontrollerad miljö.

Utför belastningstester

Nästa steg är att utföra tester i en kontrollerad miljö. Till exempel kan du köra en serie belastningstester som inkluderar och sedan utelämnar varje förfrågan så att du ser vilken effekt den har.

I diagrammet nedan visas resultatet av ett belastningstest som utförts mot en identisk distribution av molntjänsten som användes i testerna ovan. I testet används en konstant belastning där 500 användare utför den åtgärden Get i kontrollanten UserProfile, tillsammans med en stegbelastning med användare som utför åtgärden Post i kontrollanten WorkInFrontEnd.

Initial load test results for the WorkInFrontEnd controller

Inledningsvis är stegbelastningen 0, så de aktiva användarna utför bara UserProfile-förfrågningar. Systemet kan svara på cirka 500 förfrågningar per sekund. Efter 60 sekunder börjar en belastning på ytterligare 100 användare skicka POST-förfrågningar till kontrollanten WorkInFrontEnd. Nästan omedelbart faller arbetsbelastningen som skickas till kontrollanten UserProfile att sjunka till ungefär 150 förfrågningar per sekund. Det här beror på hur funktioner utförs i belastningstestet. Svar inväntas innan nästa förfrågan skickas, så ju längre tid tar att få ett svar desto lägre blir förfrågningsfrekvensen.

När allt fler användare skickar POST-förfrågningar till kontrollanten WorkInFrontEnd så fortsätter svarsfrekvensen för kontrollanten UserProfile att falla. Observera dock att mängden begäranden som hanteras av kontrollanten WorkInFrontEnd förblir relativt konstant. Mättnaden i systemet blir tydlig eftersom den totala frekvensen för båda förfrågningarna går mot en konstant men låg gräns.

Granska källkoden

Det sista steget är att titta på källkoden. Utvecklingsteamet kände till att metoden Post kan ta lång tid, vilket är anledningen till att en separat tråd användes i den ursprungliga implementeringen. Det löste problemet initialt, eftersom metoden Post inte blockerades under väntan på att en tidskrävande uppgift skulle slutföras.

Arbetet som utfördes av metoden förbrukade dock fortfarande processorkraft, minne och andra resurser. Att köra den här processen asynkront kan faktiskt försämra prestanda, eftersom användare kan utlösa ett stort antal sådana åtgärder samtidigt på ett icke-kontrollerat sätt. Det finns en gräns för hur många trådar en server kan köra. Över den här gränsen utlöses förmodligen ett undantag i programmet vid försök att starta en ny tråd.

Kommentar

Detta innebär inte att du bör undvika asynkrona åtgärder. Att vänta asynkront på nätverksanrop är en rekommendation. (Se Synkront I/O-antimönster .) Problemet här är att processorintensivt arbete skapades på en annan tråd.

Implementera lösningen och verifiera resultatet

I följande bild visas en prestandaövervakning när lösningen har implementerats. Belastningen är ungefär som tidigare, men svarstiden för kontrollanten UserProfile är nu mycket snabbare. Antalet förfrågningar under samma tidsperiod har ökat från 2 759 till 23 565.

AppDynamics Business Transactions pane showing the effects of the response times of all requests when the WorkInBackground controller is used

Observera att kontrollanten WorkInBackground också hanterade ett mycket större antal förfrågningar. Du kan däremot inte jämföra de här fallen direkt, eftersom arbetet som utförs i den här kontrollanten skiljer sig mycket från den ursprungliga koden. I den nya versionen köas helt enkelt en förfrågning snarare än att en tidskrävande beräkning utförs. Det viktiga här är att metoden inte längre påverkar hela systemet negativt under belastning.

Processor- och nätverksanvändningen visar också på bättre prestanda. Processoranvändningen når aldrig 100 % och antalet hanterade nätverksförfrågningar är mycket större än tidigare, och antalet minskade inte förrän belastningen trappade av.

AppDynamics metrics showing the CPU and network utilization for the WorkInBackground controller

I följande diagram visas resultatet av ett belastningstest. Det sammanlagda antalet betjänade förfrågningar förbättrades avsevärt jämfört med tidigare tester.

Load-test results for the BackgroundImageProcessing controller