Zkroťte své saranče aneb zátěžové testy s Locust.io

Příběhy z projektů

Úvod

Je s podivem, že platforma pro zátěžové testy Locust není v IT komunitě více rozšířena. Přitom open-source projekt Locust.io existuje už 9 let a na Internetu můžete najít řadu návodů, jak s ním rychle a efektivně začít.

Oproti tomu množina případových studií o komplexnějších realizacích „zátěžáků“ je poměrně skromná. Tento článek má ambice ji rozšířit a ukázat, proč se Locust vyplatí použít i na větším projektu.

O čem to bude

Locust jako základ robustního frameworku pro zátěžové testy

Správa datových setů během exekuce testů

Integrace na BI řešení pro reporting

Dynamické řízení testovacích scénářů

Zadání

Úkol zněl jasně: je potřeba zátěžově otestovat webové stránky generované CMS systémem. Pro ověření optimálního nastavení sdílené infrastruktury byly vytipovány 3 testovací scénáře:

  1. Základní: klasická zátěž generovaná běžnými návštěvníky webových stránek
  2. Rozšířený základní: zátěž od běžných návštěvníků kombinovaná se situací, kdy CMS systém na základě zeditovaného obsahu pro stránky sestavuje finální web
  3. Editační: zátěž generovaná editory při měnění obsahu, kdy CMS systém připravuje náhled, jak bude stránka s danými úpravami vypadat

Použité technologie

Frontend aplikace je vytvořen v javascriptovém frameworku React, aplikační vrstva nad .NET platformou v Azure cloud. Vývoj je řízen přes TFS-GIT, což platilo i pro kód zátěžových testů.

Jako základní kámen řešení pro zátěžové testy byl odzkoušen a vybrán nástroj Locust díky těmto klíčovým vlastnostem:

  • Vysoký výkon umožňující simulovat 1k–10k virtuálních uživatelů z běžného PC
  • Flexibilita a komunitní podpora jazyka Python pro utilizaci existujících, respektive psaní nových, doplňkových knihoven a řešení méně standardních až specifických úloh v rámci implementace testovacích scénářů
  • Připravenost na integraci do CI/CD pipeline
  • Připravenost frameworku pro integraci na vybrané řešení pro zpracování, analýzu a vizualizaci dat zachycených při běhu testu (v tomto případě Elastic + Kibana)

Pro další výhody Locust platformy lze nahlédnout přímo do dokumentace na Locust.io, kde jsou k dispozici i příklady základních principů a použití. Navíc na Internetu najdete například tento a řadu dalších tutoriálů. Pokud se s Locustem setkáváte poprvé, doporučuji před pokračováním v článku zabrousit alespoň na uvedené odkazy.

Locust tak utvořil jádro frameworku, jehož celkovou architekturu zachycuje následující obrázek:

Jednotlivé moduly si blíže popíšeme.

Moduly frameworku

Konfigurace testů a testovacích dat

Protože jedním z cílů bylo umožnit vývojářům spouštění testů kdykoli v případě potřeby, tj. obejít se při denním používání testů bez specializovaného performance inženýra, nastavení parametru běhu testů a testovacích dat byla vytažena do konfiguračního souboru:

Jedná se o běžný Python soubor, což umožňuje svobodu při definici datových struktur parametrů testu. Např. LOAD_STAGES a SITES jsou proměnné nativních datových Python typů, přesto jsou však pro uživatele testu velmi čitelné a srozumitelné (samozřejmě i díky patřičným komentářům).

Run-time data, tj. data utilizovaná při běhu testu, je vhodné zapouzdřit do dedikovaných tříd:

Ukázka kódu výše demonstruje, jak snadno lze nad datovými objekty vystavět jednoduchou či složitější logiku pro operaci s daty. To je užitečné například v případě, že se datové věty používají opakovaně a zároveň jednu datovou větu nelze použít pod více paralelně běžícími virtuálními uživateli. Příklady použití si ukážeme dále v kapitole o implementaci testovacích scénářů.

Elastic  - ukládání dat o běhu testu

Byť webové UI Locustu umožňuje sledovat několik statistik, které postačí na základní přehled o běhu zátěžového testu, Locust nedisponuje vlastní robustní datově analytickou ani reportovací vrstvou. V dnešní době, kdy je k tomuto účelu dostupná řada specializovaných velmi efektivních nástrojů, je tento na první pohled nedostatek několikanásobně vynahrazen přímočarostí, se kterou se Locust dokáže na jakékoli BI řešení integrovat. Ať už jde v našem případě o integraci na Elastic nebo na nějaké jiné datové řešení, vždy stojíme před úkolem ukládání dvou kategorií dat:

  1. Sledované výkonnostní veličiny zatěžované aplikace, typicky doba odezvy, doba doběhnutí jobu, úroveň čerpání infrastrukturních zdrojů (vytížení CPU, zaplnění paměti, počet aktivních nodů, atd.)
  2. Parametry prostředí aktuálního běhu testu. Například aktuální počet virtuálních uživatelů, počet paralelně zpracovávaných požadavků

Pokud jste se již seznámili se základy Locustu, víte, že o každém requestu a navazujícím response se zachycuje základní sada statistik vizualizovaná na webovém UI. Jak ale údaje, na základě kterých se statistiky počítají, odesílat do nějaké externí databáze? Také jste si možná všimli, že Locust vizualizuje v grafu počet virtuálních uživatelů, což je údaj, který by se nám určitě též hodil. Návodem budiž následující kód přidávající listenery nad vybrané Locust události:

První dva additional_success_handler a additional_failure_handler v podstatě jen dodatečně přeposílají nativně zachycované statistiky v případě ne/úspěšného requestu „někam“ dál pomocí forwarder.add(message).

Implementaci objektu forwarder, který zapouzdřuje integraci na Elastic, zde nebudeme rozebírat. Vše potřebné naleznete v článku Karola Brejny Locust.io experiments — Emitting results to external DB.

Druhé dva on_test_start a on_test_stop, které se spustí při zahájení a ukončení testu, obsahují příklady custom loggerů implementovaných nad rámec těch Locust nativních: log_VUs a log_memory. Všimněte si, že loggery se startují příkazem gevent.spawn(), který způsobí, že logger běží v tzv. Greenletu. To umožňuje, že loggery běží paralelně s běžícím testem, avšak neblokují pro sebe celý CPU proces.

Greenlet architektura je zajímavá sama o sobě. Na její implementaci pro Python v podobě knihovny gevent je vystavěn i Locust. A není tajemstvím, že právě díky ní poráží Locust ve výkonnosti rozdílem třídy některé tradiční nástroje pro zátěžové testy, jako například jMeter. Klíčová myšlenka Greenletů tkví v předpokladu, že větší úlohy lze vždy rozdělit na menší sub-úlohy, které lze vykonávat „na přeskáčku“ (tzv. context switching). Úlohy pak mohou být vypořádávány současně v rámci jednoho CPU procesu na rozdíl od patternu zpracování v paralelních vláknech, kdy každá úloha spotřebovává právě jeden proces, tj. jedno CPU vlákno. Více se můžete dozvědět na domovských stránkách gevent projektu nebo třeba v hezkém tutoriálu gevent For the Working Python Developer.

Loggery log_VUs a log_memory jsou instancemi tříd VUs a Memory z modulu loggers.py:

V kódu opět vidíme objekt forwarder, který obstarává směrování logovacích záznamů do externí databáze. Z pohledu Greenlet architektury je klíčový příkaz gevent.sleep(1), který říká: aktuální greenlet (úlohu) uspi na 1s a uvolni zdroje (tzv. yielding) ke zpracování jiného greenletu (blíže viz odkazy o gevent knihovně výše). Z toho vyplývá, že logovací údaje jsou zasílány do databáze každou vteřinu.

Ve třídě Memory si povšimněme volání služby, která vrací aktuální stav zaplnění paměti na daném Azure boxu (serveru). Takto vystavený monitorovací endpoint umožňuje orchestrovat logování přímo Locustem. V případě, že takový endpoint nemáme k dispozici, nezbývá než zařídit, aby se tyto údaje posílaly do databáze, kam ukládáme data o běhu testu, přímo z interního infrastrukturního monitoringu. Má to samozřejmě tu nevýhodu, že při exekuci testu musíme často spolupracovat s pracovníky správy infrastruktury.

Možná jste zvědaví, jak konkrétně jsou data zasílaná do Elasticu vizualizována. Práce s Kibanou nad správně strukturovanými zdrojovými daty z běhu zátěžového testu, si však zaslouží samostatný článek, který bude brzy následovat.

Externí moduly

Ač je Python téměř všeho schopný, určitě najdeme řadu případů, kdy je optimální použít jiné prostředky. V našem případě šlo o PowerShell joby, které v Azure prostředí simulovaly změny obsahu stránek. Integrace na externí modul tak byla triviální - stačilo spustit vybraný PS job a zparsovat výstup (zkontrolovat, že nedošlo k chybě, a případně zjistit požadovaný údaj):

Implementace testovacích scénářů

Základní scénář

Při troše programátorské zručnosti je Locust schopen zvládnout prakticky kterýkoli komunikační protokol. Nicméně pro klasickou komunikaci přes http ho lze použít takříkajíc „out-of-the-box“. Test case, který volá vybranou URL a provádí základní kontrolu správnosti response (návratový kód 200), vypadá takto jednoduše:

Objekt endpoints drží datový set načtený do paměti, konkrétně sadu URL z csv souboru. Metoda getRecord() vrací náhodně vybrané URL z datového setu. Dále stojí za povšimnutí, že každé volání lze pojmenovat. Volání se stejným jménem totiž ve statistikách vystupují jako různé instance jednoho volání i v případě, že ve skutečnosti je pokaždé cíleno na odlišné URL. To umožňuje agregovat data již při běhu testu.

Rozšířený základní scénář a editační scénář

Jednoduchý základní scénář bylo třeba zkombinovat s běžícími procesy v CMS systému simulovanými v Azure aplikační vrstvě pomocí PowerShell skriptů. Spuštění skriptu simulovalo událost aktivující proces (job), jehož výsledek se projevil na prezentační vrstvě. Délka běhu procesu byla jednou ze sledovaných veličin. Za tímto účelem byl naimplementován test case, který měl za úkol spustit Azure job, na daném URL zachytit, že došlo ke kýžené změně, a změřit časovou periodu. Kód tohoto test casu je velmi podobný tomu z editačního scénáře, který si ukážeme a popíšeme:

Test case používá data uložená v objektu previews. Datové věty lze utilizovat opakovaně, ale jedna datová věta nesmí být zpracovávaná víc než jedním jobem. K tomu slouží metody

previews.getRecord()
previews.setReady( preview[‘rowId’] )

První metoda vrací volnou datovou větu a zamyká ji proti použití jinde, druhá pak větu uvolňuje k další utilizaci (implementace viz modul dataReader.py výše).

Na řádcích 13–35 exekujeme script spouštějící job job = utils.process_file( site_name, preview_name, type=”Preview”), zalogujeme výsledek s dobou běhu scriptu:

forwarder.add({
    "type": "batch"
    ,...

a po kontrole, zda exekuce scriptu neskončila s chybou if ( retVal == “ERR” ):…, zahájíme cyklus while proceeding:, který kontroluje, zda nedošlo ke změně obsahu na stránkách. Když je změna zaznamenána, opět zalogujeme včetně doby běhu jobu.

V kódu se opakovaně vyskytuje příkaz self.interrupt(), který v případě chyb neslučitelných s dalším pokračováním test casu běh přeruší, a daný virtuální uživatel, pod kterým instance test casu běžela, je vrácen do poolu.

Řízení mixu testovacích případů v rámci scénáře

V úvodu článku jsme si ukázali konfigurační soubor, kterým lze mimo jiné volit scénář pro daný běh zátěžového testu:

LOAD_SCENARIO = 2
## 1 .. web users visiting sites (test case: WebTest)
## 2 .. web users visiting sites + content server processing workers output (test cases: WebTest + ContentServerTest)
## 3 .. editors requesting pages previews (test case: PreviewTest). Note: amount of virtuals users should fit amount of data records.

Při zvolení scénáře 2 se v rámci testu spouštějí dva testovací případy, které se zásadně liší ve způsobu pracování s daty:

  1. WebTest vybírá z datového setu náhodně jednu větu (URL), přičemž nevadí, je-li vybráno pro více současně běžících virtuálních uživatelů stejné URL,
  2. ContentServerTest vybírá větu, která není ve stavu procesování jiným virtuálním uživatelem.

Před zapojením dalšího virtuálního uživatele do běhu testu je tedy potřeba logika, která na základě stavu datového setu pro ContentServerTest určí, zda si daný virtuální uživatel může vybírat z obou test casů nebo na něj zbyde jen ten první. Ukažme si následující kód:

Metoda testRecord() má za účel zjistit, zda v daném datovém objektu je volná věta ke zpracování (k jejímu zamknutí dojde až při skutečné aktivaci virtuálního uživatele pomocí getRecord(), viz výše).

Proměnná self.tasks se pak plní seznamem test casů, které připadají pro spouštěného virtuálního uživatele v úvahu. Vzhledem k tomu, že u test casů v seznamu neuvádíme váhy (viz tasks attribute), Locust vybere z dvojice [WebTest, ContentServerTest] zcela náhodně.

Závěrem

V repositáři kódu pro Locust si můžete ověřit, jak projekt žije - i za poslední rok přibyla řada klíčových features. To svědčí o tom, že je platforma stále více používána na seriózních projektech zátěžových testů.

Nám už se Locust osvědčil mnohokrát a i v tomto případě uplatnil své kvality. Vypíchněme pár bodů, díky kterým pomohl našemu zákazníkovi:

  • Velmi rychlá (v řádu jednotek dnů) realizace první verze zátěžového testu s nejkritičtějšími scénáři
  • Bezbolestná adopce zátěžového frameworku vývojářským týmem zákazníka
  • Snadná následná udržba stávajících a realizace nových zátěžových scénářů

Věříme, že i na příštím projektu Locust opět pomůže s vytvořením těch nejefektivnějších řešení pro naše klienty.

Autor: Viktor Terinek

Viktor sbírá zkušenosti na poli testování softwaru už více než 15 let. Za svou kariéru vystřídal prakticky všechny výkonné testerské role spojené s analýzou, designem a implementací testů, včetně různých dimenzí automatizace. Prošel si několika manažerskými rolemi a aktuálně se naplno věnuje zefektivňování testovacích procesů a navrhování nových řešení.