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.
Feljegyzé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.
Az alábbi pszeudokód bemutatja a problémát.
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()
{
string serviceBusNamespace = ...;
QueueName = ...;
ServiceBusQueueHandler = new ServiceBusQueueHandler(serviceBusNamespace);
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();
}
});
}
Megfontolások
- 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:
- Az éles rendszer folyamatmonitorozásával azonosíthatja azokat a pontokat, ahol a válaszidők lelassulnak.
- 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.
- 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.
- 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.
- 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).
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.
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.
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.
Feljegyzé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.
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.
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.