Antipattern zaneprázdněného front-endu

Provádění asynchronní práce na velkém počtu vláken na pozadí může výrazně omezit jiné souběžné úlohy prostředků v popředí a zhoršit tak dobu odezvy na nepřijatelnou úroveň.

Popis problému

Úlohy náročné na prostředky můžou zvyšovat doby odezvy pro požadavky uživatelů a způsobovat tak vysokou latenci. Jedním ze způsobů, jak doby odezvy zlepšit, je přesměrovat úlohy náročné na prostředky do samostatného vlákna. Tento přístup umožní aplikaci, aby byla schopná reagovat, zatímco zpracování probíhá na pozadí. Úlohy, které běží na vlákně na pozadí, ale i nadále spotřebovávají prostředky. Pokud je jich příliš mnoho, můžou omezit vlákna zpracovávající požadavky.

Poznámka:

Termín prostředek může zahrnovat celou řadu věcí, například využití procesoru, obsazení paměti nebo vstupně-výstupní operace sítě nebo disku.

K tomuto problému obvykle dochází, pokud je aplikace vytvořena jako monolitická část kódu, ve které se veškerá obchodní logika kombinuje do jedné vrstvy sdílené s prezentační vrstvou.

Následující příklad používá rozhraní ASP.NET a demonstruje daný problém. Kompletní ukázku najdete tady.

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" };
    }
}
  • Metoda Post v kontroleru WorkInFrontEnd implementuje operaci HTTP POST. Tato operace simuluje dlouho běžící úlohu náročnou na procesor. Práce se provádí na samostatném vlákně, aby se umožnilo rychlé dokončení operace POST.

  • Metoda Get v kontroleru UserProfile implementuje operaci HTTP GET. Tato metoda je mnohem méně náročná na procesor.

Prvořadým zájmem jsou požadavky na prostředky metody Post. I když se práce umístí do vlákna na pozadí, může dále spotřebovávat značné prostředky procesoru. Tyto prostředky se sdílí s dalšími operacemi prováděnými jinými souběžnými uživateli. Pokud tento požadavek odešle současně i menší množství uživatelů, celkový výkon se pravděpodobně sníží a všechny operace se zpomalí. Uživatelé mohou například zaznamenat výraznou latenci v metodě Get.

Jak problém vyřešit

Přesuňte procesy, které spotřebovávají značné prostředky, do samostatného back-endu.

Front-end s tímto přístupem převede úlohy náročné na prostředky do fronty zpráv. Back-end úlohy převezme pro asynchronní zpracování. Fronta také vyrovnává zatížení, protože požadavky pro back-end ukládá do vyrovnávací paměti. Pokud bude fronta příliš dlouhá, můžete nakonfigurovat automatické škálování, aby se back-end škáloval na více instancí.

Tady je upravená verze předchozího kódu. V této verzi metoda Post převede zprávu do fronty služby Service Bus.

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);
    }
}

Back-end přetáhne zprávy z fronty služby Service Bus a zpracuje je.

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();
            }
        });
}

Důležité informace

  • Díky tomuto postupu bude aplikace o něco složitější. Je potřeba zajistit bezpečné zařazování do fronty a vyřazování z fronty, aby v případě chyby nedošlo ke ztrátě požadavků.
  • Aplikace je závislá na další službě pro fronty zpráv.
  • Procesní prostředí musí být dostatečně škálovatelné, aby mohlo zpracovat očekávané zatížení a splňovat cíle požadované propustnosti.
  • Zatímco by tento přístup měl vylepšit celkovou odezvu, může dokončení úloh přesunutých na back-end trvat delší dobu.

Jak zjistit problém

Mezi příznaky zaneprázdněného front-endu patří vysoká latence při provádění úloh náročných na prostředky. Koncoví uživatelé pravděpodobně hlásí delší dobu odezvy nebo chyby způsobené časovým limitem služeb. Tato selhání můžou také vracet chyby HTTP 500 (interní server) nebo chyby HTTP 503 (Služba není k dispozici). Zkontrolujte protokoly událostí webového serveru, které budou pravděpodobně obsahovat podrobnější informace o příčinách a okolnostech chyb.

Následující postup vám pomůže tento problém identifikovat:

  1. Proveďte monitorování procesů produkčního systému. Můžete tak identifikovat body, kdy se doby odezvy zpomalí.
  2. Prozkoumejte telemetrická data zachycená v těchto bodech, abyste zjistili kombinaci prováděných operací a používaných prostředků.
  3. Hledejte korelace mezi dlouhými dobami odezvy a objemy a kombinacemi operací, které se v těchto obdobích prováděly.
  4. Proveďte zátěžový test každé podezřelé operace, abyste určili operace, které spotřebovávají prostředky a omezují další operace.
  5. Zkontrolujte u takových operací zdrojový kód, abyste mohli zjistit, proč by mohly způsobovat nadměrnou spotřebu prostředků.

Ukázková diagnostika

V následujících částech se tento postup použije u ukázkové aplikace popsané výše.

Identifikace bodů zpomalení

Instrumentuje každou metodu, aby sledovala dobu trvání a prostředky spotřebovávané jednotlivými požadavky. Potom aplikaci sledujte v produkčním prostředí. Získáte tak celkový přehled o tom, jakým způsobem mezi sebou požadavky soupeří. Pomalé požadavky náročné na prostředky budou během období zátěže pravděpodobně ovlivňovat jiné operace. Toto chování můžete sledovat prostřednictvím monitorování systému, kde si také můžete všimnout poklesu výkonu.

Následující obrázek znázorňuje řídicí panel monitorování. (Použili jsmeAppDynamics pro naše testy.) Na začátku má systém lehké zatížení. Potom začnou uživatelé vyžadovat metodu UserProfile GET. Výkon se udržuje na přiměřeně dobré úrovni, dokud nezačnou ostatní uživatelé vydávat požadavky na metodu WorkInFrontEnd POST. V tuto chvíli se doba odezvy dramaticky zvýší (první šipka). Doba odezvy se zlepší, teprve až se objem požadavků na kontroler WorkInFrontEnd sníží (druhá šipka).

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

Prozkoumání telemetrických dat a hledání korelací

Další obrázek zobrazuje některé z metrik shromážděných za účelem monitorování využití prostředků během toho stejného intervalu. Zpočátku do systému přistupuje pouze několik uživatelů. Když se začne připojovat více uživatelů, využití procesoru se výrazně zvýší (100 %). Všimněte si také, že síťová frekvence V/V zpočátku roste společně se zvyšujícím se využitím procesoru. Jakmile ale dosáhne využití procesoru svého maxima, síťová frekvence V/V začne klesat. Je to způsobeno tím, že jakmile procesor dosáhne své kapacity, systém může zpracovat jen relativně malý počet požadavků. Když se uživatelé začnou odpojovat, zatížení procesoru začne klesat.

AppDynamics metrics showing the CPU and network utilization

V tuto chvíli to vypadá, že za hlubší analýzu by stála metoda Post v kontroleru WorkInFrontEnd. Abychom si tuto hypotézu potvrdili, je potřeba provést další úlohy v řízeném prostředí.

Provedení zátěžového testování

Dalším krokem je provedení testů v řízeném prostředí. Spusťte například řadu zátěžových testů, které zahrnují a pak vynechávají jednotlivé požadavky, aby byl vidět jejich vliv.

Graf níže ukazuje výsledky zátěžového testu provedeného na stejném nasazení cloudové služby jako v předchozích testech. Test použil konstantní zatížení 500 uživatelů provádějících operaci Get v kontroleru UserProfile společně s krokovým zatížením uživatelů provádějících operaci Post v kontroleru WorkInFrontEnd.

Initial load test results for the WorkInFrontEnd controller

Krokové zatížení je na začátku 0, požadavky UserProfile tedy provádí jediní aktivní uživatelé. Systém je schopný reagovat přibližně na 500 požadavků za sekundu. Po 60 sekundách začne do kontroleru WorkInFrontEnd posílat své požadavky POST dalších 100 uživatelů. Pracovní zatížení odesílané do kontroleru UserProfile se prakticky okamžitě sníží na přibližně 150 požadavků za sekundu. Důvodem je způsob, jakým funguje spouštěč zátěžových testů. Než pošle další požadavek, čeká na odpověď. Čím déle tedy trvá získání odpovědi, tím nižší je frekvence požadavků.

Když do kontroleru WorkInFrontEnd začne posílat požadavky POST více uživatelů, míra odpovědí kontroleru UserProfile bude dál klesat. Všimněte si ale, že objem požadavků zpracovávaných kontrolerem WorkInFrontEnd zůstává relativně konstantní. Sytost systému začíná být zřejmá, když se celková míra obou požadavků začne blížit ke konstantnímu, ale nízkému limitu.

Kontrola zdrojového kódu

Posledním krokem je kontrola zdrojového kódu. Vývojový tým si byl vědom toho, že metoda Post může trvat poměrně dlouhou dobu, a proto původní implementace použila samostatné vlákno. Vyřešil se tím bezprostřední problém, protože metoda Post se nezablokovala čekáním na dokončení dlouho běžící úlohy.

Tato metoda však dále spotřebovává procesor, paměť a jiné prostředky. Pokud povolíte, aby tento proces běžel asynchronně, může dojít ke zhoršení výkonu, protože uživatelé mohou současně a neřízeným způsobem aktivovat velký počet těchto operací. Existuje omezení počtu vláken, která může server spustit. Pokud se aplikace po překročení tohoto omezení pokusí spustit nové vlákno, bude muset zřejmě získat výjimku.

Poznámka:

Neznamená to, že byste se měli vyhýbat asynchronním operacím. Provádění asynchronního await u volání sítě je doporučeným postupem. (Viz Synchronní antipattern vstupně-výstupní operace.) Problém spočívá v tom, že práce náročná na procesor se vytvořila na jiném vlákně.

Implementace řešení a ověření výsledku

Následující obrázek ukazuje monitorování výkonu po implementaci řešení. Zatížení bylo podobné dříve uvedenému zatížení, ale doby odezvy kontroleru UserProfile jsou teď mnohem rychlejší. Objem požadavků se za stejnou dobu trvání zvýšil z 2759 na 23565.

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

Všimněte si, že kontroler WorkInBackground také zpracoval mnohem větší objem požadavků. Tento případ ale nemůžete použít k přímému porovnání, protože práce provedená v tomto kontroleru se hodně liší od původního kódu. Nová verze jednoduše zařadí požadavek do fronty a neprovádí časově náročné výpočty. Nejdůležitější je, že tato metoda už nezatěžuje celý systém.

Využití procesoru a sítě také vykazují zlepšení výkonu. Využití procesu nikdy nedosahovalo 100 % a objem zpracovaných síťových požadavků byl vyšší než dříve a nesnížil se, dokud zatížení nekleslo.

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

Následující graf znázorňuje výsledky zátěžového testu. Celkový objem obsloužených požadavků je ve srovnání s předchozími testy mnohem větší.

Load-test results for the BackgroundImageProcessing controller