Foglalt előtér kizárási minta

A nagy mennyiségű háttérbeli szálon végzett aszinkron feladatok elvonhatják az erőforrásokat más egyidejű előtérbeli feladatok elől, ami elfogadhatatlan mértékben megnöveli a válaszidőket.

A probléma leírása

A nagy erőforrásigényű feladatok növelhetik a felhasználói kérések válaszidejét, és magas késést eredményezhetnek. A válaszidők javításának egyik módja az, ha a nagy erőforrásigényű feladatokat kiszervezzük egy önálló szálra. Ezzel a megoldással az alkalmazás továbbra is válaszkész maradhat, miközben a feldolgozás a háttérben zajlik. Azonban a háttérbeli szálon futó feladatok továbbra is használnak erőforrásokat. Ha túl sok az ilyen feladat, azok elvonhatják az erőforrásokat azon szálak elől, amelyek a kéréseket kezelik.

Megjegyzés:

Az erőforrás kifejezés sok mindent foglalhat magában, köztük a processzor- és memóriakihasználtságot, illetve a hálózati vagy lemez I/O-t.

Ez a probléma általában akkor fordul elő, ha egy alkalmazás egyetlen nagy kódtömbként jött létre, ahol az összes üzleti logika egy rétegben található, és osztozik a megjelenítési réteggel.

A következő ASP.NET-alapú példa bemutatja a problémát. A teljes kódmintát itt találja.

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" };
    }
}
  • A WorkInFrontEnd vezérlőben lévő Post metódus egy HTTP POST műveletet implementál. Ez a művelet egy hosszan futó, nagy processzorigényű feladatot szimulál. A feladat egy önálló szálon fut, hogy a POST művelet gyorsan befejeződhessen.

  • A UserProfile vezérlőben lévő Get metódus egy HTTP GET műveletet valósít meg. Ennek a metódusnak sokkal kisebb a processzorigénye.

Az elsődleges szempont a Post metódus erőforrásigénye. Bár a metódus egy háttérbeli szálra helyezi a feladatot, a feladat így is jelentős mértékű processzor-erőforrásokat használhat. Ezeket az erőforrásokat egyidejűleg más műveletek is használják, amelyeket más felhasználók végeznek. Ha közepes számú felhasználó egyszerre küldi el ezt a kérést, az valószínűleg rontja az összteljesítményt, és lelassítja az összes műveletet. A Get metódus használatakor a felhasználók többek között jelentős késleltetéseket tapasztalhatnak.

A probléma megoldása

Helyezze át a jelentős erőforrás-használatú folyamatokat egy külön háttérre.

Így az előtér az erőforrás-igényes feladatokat egy üzenetsorba állítja. A háttér felveszi a feladatokat aszinkron feldolgozásra. Az üzenetsor terheléselosztóként is működik, amely puffereli a kéréseket a háttér számára. Ha az üzenetsor túl hosszúra nő, az automatikus skálázás konfigurálható a háttér horizontális felskálázásra.

Az alábbiakban az előző kód átdolgozott verziója látható. Ebben a verzióban a Post metódus a Service Bus-üzenetsorban helyez el egy üzenetet.

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

A háttér lekéri az üzeneteket a Service Bus-üzenetsorból, és elvégzi a feldolgozást.

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

Considerations

  • Ez a módszer összetettebbé teszi az alkalmazást. Gondoskodni kell a biztonságos sorba állításról és sorból való eltávolításról, hogy ne vesszenek el a kérések hiba esetén.
  • Az alkalmazás függőséget vesz fel egy további szolgáltatásra az üzenetsorhoz.
  • A feldolgozási környezetnek megfelelően skálázhatónak kell lennie, hogy képes legyen kezelni a várt számításifeladat-mennyiséget, és teljesíteni tudja az átviteli sebességgel kapcsolatos követelményeket.
  • Ennek a megoldásnak összességében növelnie kellene a válaszkészséget, de előfordulhat, hogy a háttérbe áthelyezett feladatok elvégzése hosszabb időt vesz igénybe.

A probléma észlelése

Az elfoglalt előtér tünetei közé tartozik a magas válaszidő a nagy erőforrásigényű feladatok végrehajtásakor. A végfelhasználók valószínűleg hosszabb válaszidőket vagy a szolgáltatások időtúllépése által okozott hibákat jelentik. Ezek a hibák HTTP 500 (belső kiszolgáló) vagy HTTP 503 (szolgáltatás nem érhető el) hibákat is eredményezhetnek. Vizsgálja át a webkiszolgáló eseménynaplóit, amelyek valószínűleg részletesebb információkat tartalmaznak a hibák okairól és körülményeiről.

A következő lépések végrehajtásával azonosíthatja a problémát:

  1. Az éles rendszer folyamatmonitorozásával azonosíthatja azokat a pontokat, ahol a válaszidők lelassulnak.
  2. Az ezeken a pontokon gyűjtött telemetriaadatok vizsgálatával megállapíthatja, hogy mely műveletek mennek végbe és mely erőforrások vannak használatban.
  3. Megtalálhatja az összefüggéseket a gyenge válaszidők és az adott időpontokban futó műveletek mennyisége és kombinációi között.
  4. Végezzen terhelési teszteket a gyanús műveletekkel, így megállapíthatja, hogy mely műveletek használják az erőforrásokat és veszik el azokat más műveletek elől.
  5. Tekintse át az adott műveletek forráskódját, amiből kiderülhet, hogy a műveletek miért járnak túlzott erőforráshasználattal.

Diagnosztikai példa

Az alábbi szakaszokban ezeket a lépéseket hajtjuk végre a fentebb leírt mintaalkalmazáson.

A lassulási pontok azonosítása

Tagolja az egyes metódusokat, hogy nyomon követhesse az egyes kérések futási idejét és erőforrás-használatát. Ezután monitorozza az alkalmazást éles környezetben. Ezzel átfogó képet kaphat arról, hogy a kérések hogyan versengenek egymással. A nagy nyomással járó időszakokban a lassan futó, nagy erőforrásigényű kérések valószínűleg hatással lesznek a többi műveletre. Ezt a viselkedést úgy figyelheti meg, ha a rendszer monitorozásakor észreveszi a teljesítménycsökkenést.

Az alábbi képen egy monitorozási irányítópult látható. (Használtuk AppDynamics a tesztjeinkhez.) Kezdetben a rendszer könnyű terheléssel rendelkezik. Ezután a felhasználók elkezdik lekérni a UserProfile GET metódust. A teljesítmény viszonylag jó marad egészen addig, amíg más felhasználók el nem kezdenek kéréseket küldeni a WorkInFrontEnd POST metódus számára. Ekkor a válaszidők jelentős mértékben megnövekednek (első nyíl). A válaszidők csak akkor kezdenek el csökkenni, amikor a WorkInFrontEnd vezérlő számára küldött kérések mennyisége lecsökken (második nyíl).

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

A telemetriaadatok vizsgálata és az összefüggések felderítése

A következő képen néhány olyan mérőszám látható, amelyek ugyanezen időszak alatt az erőforráshasználat monitorozása céljából lettek összegyűjtve. Először kevés felhasználó fér hozzá a rendszerhez. Ahogy további felhasználók csatlakoznak, a processzorkihasználtság rendkívül magasra emelkedik (100%). A processzor kihasználtságával együtt kezdetben a hálózati I/O is megnövekszik. A processzorhasználat tetőzésével azonban a hálózati I/O visszaesik. Ennek az az oka, hogy a rendszer csak viszonylag kevés kérést tud egyszerre kezelni, miután a processzor elérte a maximális kapacitását. Ahogy a felhasználók bontják a kapcsolatot, a processzor terhelése fokozatosan csökken.

AppDynamics metrics showing the CPU and network utilization

Ekkor úgy tűnik, hogy a WorkInFrontEnd vezérlő Post metódusát kell közelebbről megvizsgálni. Az elmélet megerősítéséhez további lépések szükségesek ellenőrzött környezetben.

Terhelési tesztelés végrehajtása

A következő lépés tesztek végrehajtása ellenőrzött környezetben. Például hajtson végre több olyan terhelési tesztet, amelyek először tartalmazzák, majd kihagyják az egyes kéréseket, és ez alapján mérje fel a hatásukat.

Az alábbi grafikonon látható terhelésiteszt-eredmények egy ugyanolyan felhőszolgáltatás üzemelő példányán lettek elvégezve, mint a korábbi tesztek. A tesztben 500 felhasználó hajtotta végre a Get műveletet a UserProfile vezérlőben, miközben lépéses terhelés is történt, ahol a felhasználók a Post műveletet végezték a WorkInFrontEnd vezérlőben.

Initial load test results for the WorkInFrontEnd controller

Kezdetben a lépéses terhelés 0, így az egyedüli aktív felhasználók a UserProfile kéréseket hajtják végre. A rendszer körülbelül másodpercenként 500 kérésre képes válaszolni. 60 másodperc után további 100 felhasználó kezd el POST kéréseket küldeni a WorkInFrontEnd vezérlőnek. A UserProfile vezérlőnek elküldött számításifeladat-mennyiség szinte azonnal másodpercenként 150 kérésre csökken. Ez a terhelési teszt működési mechanizmusa miatt van. A teszt a kérések elküldése előtt megvárja az előző kérdésre kapott választ, ezért minél hosszabb ideig tart a válasz érkezése, annál alacsonyabb lesz a kérések aránya.

Ahogy több felhasználó küld POST kéréseket a WorkInFrontEnd vezérlőnek, úgy csökken tovább a UserProfile vezérlő válaszadási aránya. Vegye figyelembe azonban, hogy a vezérlő által WorkInFrontEnd kezelt kérelmek mennyisége viszonylag állandó marad. Láthatóvá válik a rendszer túltelítődése, ahogy a két kérés összesített sebessége egy egyenletesen alacsony korlát felé tart.

A forráskód áttekintése

Az utolsó lépés a forráskód áttekintése. A fejlesztőcsapat tisztában van azzal, hogy a Post metódus jelentős időt vehet igénybe, ezért az eredeti implementációban egy külön szál lett erre a célra használva. Ezzel a közvetlen probléma megoldódott, mivel a Post metódus nem blokkolt le arra várva, hogy egy hosszan futó feladat befejeződjön.

Azonban ez a metódus továbbra is használja a processzort, a memóriát és az egyéb erőforrásokat. A folyamat aszinkron módon való futásának engedélyezése valójában csökkentheti a teljesítményt, mivel a felhasználók nagy mennyiségű ilyen műveletet aktiválhatnak egyszerre, felügyelet nélkül. A kiszolgálók csak véges számú szálat tudnak egyszerre futtatni. Ennek elérése után az alkalmazások valószínűleg kivételt kapnak, ha megpróbálnak elindítani egy új szálat.

Megjegyzés:

Ez nem jelenti azt, hogy az aszinkron műveleteket kerülni kellene. Az aszinkron várakoztatás végrehajtása a hálózati hívásoknál ajánlott eljárás. (Lásd: Szinkron I/O antipattern.) A probléma itt az, hogy a processzorigényes munkát egy másik szálon hozták.

A megoldás megvalósítása és az eredmény ellenőrzése

A következő képen a teljesítmény monitorozása látható a megoldás implementálása után. A terhelés hasonló, mint korábban, de a UserProfile vezérlő válaszideje már sokkal rövidebb. Az adott időtartam alatt fogadott kérések száma 2759-ről 23 565-re nőtt.

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

Emellett a WorkInBackground vezérlő sokkal nagyobb mennyiségű kérést kezelt. Ebben az esetben azonban nem lehet közvetlen párhuzamot vonni, mivel e vezérlő feladatvégzése teljesen más, mint az eredeti kód. Az új verzió egyszerűen sorba állít egy kérést, ahelyett, hogy elvégezne egy időigényes számítási feladatot. A fő szempont az, hogy ez a metódus már nem csökkenti az egész rendszer teljesítményét nagy terhelés esetén.

A teljesítményjavulás a processzor és a hálózat kihasználtságában is megmutatkozik. A processzor kihasználtsága egyszer sem érte el a 100%-ot, a kezelt hálózati kérések száma a korábbinál sokkal nagyobb volt, és nem csökkent a számítási feladat befejezéséig.

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

A következő grafikon egy terhelési teszt eredményeit mutatja. A kiszolgált kérések teljes mennyisége nagymértékben nőtt a korábbi tesztekhez képest.

Load-test results for the BackgroundImageProcessing controller