% !TeX root = ../thesis.tex
\chapter{A rendszer teljesítménymérése}
Dolgozatunk e fejezetében bemutatunk egy olyan módszertant és eszközöket, amik segítségével egy komplex mikroszolgáltatásokból álló rendszerben megtalálhatók az esetleges gócpontok, valamint azok orvoslására javaslat tehető. Emellett bemutatjuk az ennek kidolgozásához és ellenőrzéséhez felhasznált eszközöket. 
\section{Mérési környezet ismertetése}
A fejlesztés és teljesítménymérés idejére létrehoztunk egy Kubernetes klasztert, amiben futtattuk a rendszert. A klaszterben három Master és ugyanennyi Worker volt, mindegyik virtuális gépen futott, Microsoft Hyper-V hypervisorban. A Mastereknek kettő processzormagja és nyolc gigabájt memóriája volt, a Workereknek négy magot, valamint tizenhat gigabájt memóriát allokáltunk. A klaszter három fizikai gépen futott, mindegyikben Intel Xeon E5-2450L processzor, valamint 64 gigabájt memória volt. Egy Master és egy Worker példányt telepítettünk mindegyik fizikai gépre.
<Ábra a fizikai architektúráról>
A fizikai hosztgépek kétszer egy gigabites hálózaton kerültek összekötésre. Amint az <ÁBRAHIVATKOZÁS> -n is látható, az egyik hálózaton a hypervisorok és a virtuális gépek az internetet érték el, ezt csak frissítések és csomagok letöltésére használtuk. Emellett egy fizikailag teljesen különálló hálózat állt a virtuális gépek rendelkezésére a klaszter forgalmának lebonyolítására.
Mivel a fürtnek számos tagja volt, telepítettünk egy külön fizikai gépre egy HAProxy HTTP és TCP terheléselosztót, ami a kéréseket roundrobin algoritmussal osztotta el az egyes node-ok között. Ennek segítségével volt elérhető az egyes mikroszolgáltatások API-ja és az MQTT broker az IoT eszközök számára. Ebben a fizikai gépben egy Intel Core i5 2550K és 16 gigabájt ram volt.
A Kubernetes klaszterben a Calico hálózati implementációt használtuk, ami nem hoz létre virtuális hálózatot a meglévő fölé, hanem az egyes alkalmazások és kompnensek között különálló IP alhálózatok és útvonalválasztás segítségével biztosítja a megfelelő izolációt. Emellett NGINX Ingress Controllert használtunk. Ez egy NGINX webszerver konfigurációjával realizálja az Ingress Controller funkcionalitását.
A mikroszolgáltatásokat kiszolgáló relációs adatbázisokat egy külön virtuális gépen futó PostgreSQL szolgálta ki, aminek 2 processzormagot és két gigabájt memóriát allokáltunk.
A méréseket futtató számítógépben egy Core i7 3770 és 16 gigabájt ram volt, valamint csatlakozott mind a publikus, mint a magánhálózatra.

\section{Mérőszoftver ismertetése}
Mint azt az előző fejezetben ismertettük a leghosszabb út, amit egy minta és az arra generált válasz megtehet egy seregélyként kategorizált minta tesz meg. Ebből következik, hogy egy kritikus mérés a beküldött HTTP kérések és az érkező MQTT üzenetek között eltelt idő lehet. Olyan kész mérőeszköz viszont, ami ennek a mérésére képes nem elérhető, ezért egy saját mérőeszköz fejlesztése mellett döntöttünk.
Annak érdekében, hogy a lehető leggyorsabban legyen képes a szoftver kéréseket generálni azokat indulás után először kigenerálja      azok törzsét az Input Service elvárt sémában található eszközazonosító kitöltésével, a többi mező és rész előre beállított sablont érintetlenül hagyásával. Parancssori paraméterként megadható a használni kívánt konkurenciaszám, ami egy külön folyamatot jelent. Az egyes folyamatok a következő lépésben egy saját listában nyilvántartják az aktuális rendszeridőt, elküldenek egy HTTP kérést az előre elkészítettek közül, majd egy másik listába is feljegyzik a rendszeridő aktuális értékét. Fontos kiemelni, hogy az egyes worker folyamatok minden kérés végéig blokkolnak, azaz megvárják, amíg megérkezik a válasz. Ezt a lépést addig ismétlik, amíg a mérést felügyelő folyamat – az előre beállított idő leteltével – le nem állítja őket. Ez alatt és után 600 másodpercig, szintén külön folyamatban, a szoftver fogadja az MQTT-n érkező üzeneteket és feljegyzi azok érkezési idejét egy listába.
Miután minden folyamat végzett, a különböző HTTP kéréseket generáló folyamatoktól lekérdezi a küldési és fogadási időpontokat tartalmazó listákat és egyesíti azokat egy-egy nagy listába, amik a kérések elküldése szerint kerülnek rendezésre. Ez úgy lehetséges, hogy a szoftver minden kéréshez rendel egy egyedi azonosítót, amit a listában az időbélyeg mellé csatol. Ugyanez teszi lehetővé az MQTT üzenetek HTTP kérésekhez párosítását, ugyanis a mérni kívánt rendszer a bemeneti API-ján elfogad egy egyedi azonosítót, amit felhasznál az MQTT téma összeállítása során. Amennyiben a mérések során minden esetben egyedi azonosítóval látunk el egy-egy kérést, azok végig egyértelműen azonosíthatók maradnak lehetővé téve az egyes mérőkomponensek külön folyamatba választását.

\section{Mérőszoftver értékelése}
Amennyiben a mérőszoftver segítségével méréseket futtatnánk a kapott eredmények csekély információt hordoznának egyéb adatok híján, ugyanis bizonytalan lenne, hogy a mérés a rendszer vagy a mérőeszköz legjobb teljesítményét mutatja, nem tudjuk melyik teljesítményét mértük ki. A mérőszoftvert Pythonban készítettük,    ezért annak teljesítménye nem feltétlen haladja meg a mért rendszerét.
Ezt a problémát megoldandó elkészítettünk egy a rendszert imitáló webes alkalmazást. Az alkalmazást Kotlinban írtuk, amiben a korutinok segítségével könnyű aszinkron, nem blokkoló kód írása. Ennek köszönhetően az alkalmazás minden HTTP kérést aszinkron módon szolgál ki, ezzel emelve az alkalmazás áteresztőképességét. Amennyiben az alkalmazáshoz GET kérés érkezik, az egy nullás karakterrel tér vissza. Ezt a viselkedést a mérő alkalmazás működésének tesztelésére hoztuk létre. Ha az alkalmazáshoz POST kérés érkezik, ellenőrzi, hogy a kérés megfelel-e az Input Service által elvártaknak. Amennyiben nem, 500-as hibakóddal válaszol. Amennyiben a kérés átmegy a validáción, a kérés törzséből beolvassa az eszközt – vagy ez esetben a kérést – azonosító mező értékét és aszinkron módon küldd egy üzenetet az MQTT brókernek továbbításra a megfelelő témára, majd válaszol a kérésre egy húsz karakter hosszú karakterlánccal. Ez utóbbi célja, hogy a HTTP válasz mérete is nagyságrendileg akkora legyen, mint amekkorát az Input Service küldene.
Ennek az alkalmazásnak referenciaként teszteltük a teljesítményét az Apache Benchmark (ab) eszközzel.
<barchart: BirbBench – requests: 130, BirbBench – pycurl: 560, ab: 698>
Az <ÁBRAHIVATKOZÁS> n is látható, hogy az ab (ab oszlop) és az általunk készített szoftver (BirbBench – requests oszlop) nagy a különbség teljesítmény téren. Ezt a különbséget nem tartottuk elfogadhatónak . A gyanú gyorsan az általunk használt Python HTTP kliensre, a requests-re terelődött, ugyanis az teljes egészében Pythonban készült, valamint a PyCurlhöz összehasonlító teljesítménymérésekben  lemarad. Utóbbi egy vékony Python réteg a C-ben implementált libcurl felett. Valóban, a PyCurl használata esetén a mérőeszköz által mért áteresztőképesség jobban megközelítette az ab által elértet, de nem érte azt el, viszont ez nem volt célunk. Ezen mérések által van referenciánk arról, milyen karakterisztikákkal rendelkezik a mérőszoftverünk HTTP kliense.
Az MQTT komponens értékelése során is nem várt anomáliát figyeltünk meg. Amint az <ÁBRAHIVATKOZÁS>n megfigyelhető, az MQTT üzenetek késleltetési ideje a HTTP kérésekhez képest a mérés során szigorúan monoton növekedett, ami arra enged következtetni, hogy vagy a broker áteresztőképességénél nagyobb rátával küldi neki az üzeneteket a Kotlinos alkalmazás, vagy a kliens képes túl lassan fogadni az üzeneteket. Előbbire enged következtetni, hogy a broker folyamata a Workeren egy processzormagot teljesen kihasznál.
<ábra: mqtt latency megy fel, mint a 21-es busz a normáfra>
A fenti hipotézis ellenőrzésére elkészítettünk egy olyan Kotlin programot, ami letárolja az egy másodperc alatt érkezett üzeneteket, majd ezt összehasonlítottuk a Pythonban készült mérőszoftverrel. Az összehasonlítás során 
Ezek alapján arra következtettünk , hogy az általunk használt Apache Artemis MQTT broker egy példányt használva a számunkra elérhető hardveren körülbelül száz egyedi témára érkező üzenetet képes továbbítani.  

\section{Skálázódási lehetőségek felmérése}
Méréseinkkel a rendszer skálázhatóságát szerettük volna megvizsgálni, viszont ehhez tudnunk kellett, mely komponenseket érdemes skálázni, melyek azok a mikroszolgáltatások, amk egy adott kérést a legtovább dolgozzák fel, ezzel a rendszerben szűk keresztmetszetként viselkednek. Mivel a mikroszolgáltatásaink egy távoli Kubernetes klaszterben futnak a tradicionális, fejlesztői eszközökbe épített profilozó használata nem lehetséges. Ezt a problémát hivatottak megoldani az Alkalmazás Teljesítmény Monitorozó eszközök. Számos ilyen eszköz elérhető, mint például a Jaeger, vagy a mikroszolgáltatásokban keletkezett hibákat is gyűjteni képes Sentry. Ezek között a különbség tipikusan a támogatott nyelvek és a szerverben használt technológiákban rejlik, mindegyik képes egy megjelölt kódblokk futási idejét monitorozni. A projekt fejlesztése során Sentryt használtunk az elosztott hibakeresési képességei, valamint elsőrangú Python támogatása miatt.
Ezen vizsgálatok során a módszertanunk az volt, hogy a rendszerbe beküldtünk egyetlen mintát, valamint korábban ismertetett mérőeszközzel generáltunk nagyobb terhelést a rendszerben, majd a Sentry által aggregált adatokat összevetettük és értkeltük. Minden mérést húsz alkalommal ismételtünk és az eredmények mértani közepét vettük annak érdekében, hogy egy-egy kiemelkedő mérés ne befolyásolja aránytalanul a kapott eredményeket.
<barchart késleltetések 50. és 95. percentiliséről, egy kérés esetén>
Általánosan elmondható volt a tesztek alapján, hogy a hangmintákkal dolgozó, valamint mesterséges intelligenciát alkalmazó mikroszolgáltatások válaszideje nagyságrendekkel nagyobb, mint a csak szöveges adatokkal dolgozóké. Ennek oka valószínű az Input és a Storage mikroszolgáltatások esetében a HTTP kérések mérete lehet, a Classification Service-nél pedig a mesterséges intelligencia futási idejéből adódhat a magas átfutási idő. Ezt ellenőrizendő finomítottuk a használt tranzakciókat, hogy a kérdéses HTTP kérések során elvégzett műveletek ideje pontosan látszódjon.
<input-service futási idő waterfall chart>
Ez alapján (ahogy az <ÁBRAHIVATKOZÁS> is látható) megállapítható, hogy valóban a kérés fogadása, valamint a kezelése tart sokáig az Input Service-ben, az egyéb műveletek rövid idő alatt lezajlanak. Ennek okán az egyik skálázásra kijelölt komponenspár az Input és Storage Service-ek voltak.
<storage service futási idő waterfall chart>
Úgy döntöttünk ezeket közösen érdemes skálázni, hiszen az <ÁBRAHIVATKOZÁS>t összevetve az <ÁBRAHIVBATKOZÁS>val is megfigyelhető, hogy a Storage Service esetében a HTTP kérés fogadásával és a hangfájl multipart kérésből kivételével eltöltött idő igen közel van az Input Service-nél tapasztaltakhoz. Ez egy várható eredmény volt, ugyanis mindegyik mikroszolgáltatás Python nyelven, a Flask webes keretrendszert használva készült, így ezen műveletek implementációja és belső viselkedése nagyon hasonló.
<classification service waterfall chart>
A Classification Service-t górcső alá véve a korábbi hipotézisünk beigazolódni látszik, ugyanis a minta feldolgozásával eltöltött idő közel 85\%-át tölti a mesterséges intelligencia futtatásával a mikroszolgáltatás. Ez azt jelenti, hogy nagy számú egyidejű beérkező minta esetén mindenképpen skálázni kell ezt a mikroszolgáltatást is.
A többi mikroszolgáltatás esetében nem tartottuk indokoltnak a skálázás alkalmazását, ugyanis azok nagy számú konkurens kliens esetében is alacsony válaszidővel képesek voltak feldolgozni az egyes kéréseket, így azok a rendszer áteresztőképességét nem korlátozzák.

\section{Input és Storage Service-ek skálázásának vizsgálata}
Száz konkurens kliens esetén az Input Service késleltetése nem növekszik meg lényegesen az egy, vagy húsz konkurens kliensek esetén mérttől, amint azt <ÁBRAHIVATKOZÁS>n is láthatjuk. Ez jó jel a skálázhatóságára tekintve, ugyanis ebből arra következtethetünk, hogy ez a mikroszolgáltatás kiszámíthatóan reagál a változatos terhelésekre. Ez alapján megsejthetjük, hogy amennyiben kettő példányt indítunk a komponensből annak áteresztőképessége közel kétszeresére növekedik. Ehhez viszont ki kell kössük, hogy a Storage Service skálázása is szükséges, ellenben az továbbra is szűk keresztmetszet marad.
<input service latency benchmarkok>
A sejtés beigazolódni látszik, ugyanis míg ez a két mikroszolgáltatás egy-egy példányt használtva tíz kérést volt képes feldolgozni másodpercenként, kettő példányt használva húszat, öt esetében pedig negyvennyolcat.
<input service rps benchmarkok>

\section{Classification Service skálázásának vizsgálata}
A Classification Service bemeneti interfésze, mivel üzenetsoron kapja a bemeneti adatokat más jellegű, mint az Input vagy a Storage Service-eké. A teljesen aszinkron bemeneti interfészén csak akkor kap adatot, ha épp nem dolgoz fel egy másikat, valamint az aszinkron kimeneti interfésze miatt nem blokkolja másik mikroszolgáltatás sem. Emiatt az üzenetsor egyfajta terheléselosztóként is felfogható.
<cnn rps benchmarkok>
A fent leírtak alapján azt várjuk, hogy a mikroszolgáltatás skálázása esetén a komponens áteresztőképessége a replikák számával arányosan fog nőni. Amint ez <ÁBRAHIVATKOZÁS>n látható, ez így van, amíg egy példány két mintát képes másodpercenként feldolgozni, tíz példány húszat, húsz példány pedig negyvenet.

\section{Eredmények értékelése}
A fejezetben ismertetett mérések eredményeiből számos konzekvencia levonható a rendszerrel kapcsolatban. Az első, hogy a teljesítmény szempontjából szűk keresztmetszetet képző komponensek jól skálázódnak. A szűk keresztmetszet megszüntetésére vagy enyhítésére viszont egyéb alternatívák is láthatók. Az Input és Storage Service-ek vizsgálata esetén érdemes lehet meggondolni azok összevonását, ugyanis ezzel a lépéssel az Input Service válaszideje a felére csökkenthető. Ezen túl a komponens skálázásával jól növelhető az áteresztőképessége.
A Classification Service esetében javasoljuk a Kubenetesben automatikus skálázás alkalmazását, egyedi metrikákat megfogalmazva az üzenetsoron egységnyi idő alatt beérkező üzenetek alapján. Ez megoldja a szűk keresztmetszet problémáját amellett, hogy nem pazarol feleslegesen erőforrást a klaszterben. Ennek a skálázódásnak implementációjánál viszont figyelni kell arra, hogy az új példányok az első minta feldolgozása során le kell kérjék az aktuális modellt a Model Service-től.

\section{A módszer általános leírása}
Az általunk itt alkalmazott módszer jól felhasználható más hasonló mikroszolgáltatásokra épülő szoftverek esetében. Első lépésként valamilyen Alkalmazás Teljesítmény Monitorozó megoldás használata és integrálása kulcsfontosságú. Ezek segítségével megállapítható, mely mikroszolgáltatások szükségesek vizsgálatra skálázási szempontból, melyek jelenthetik a szűk keresztmetszetet. Természetesen a skálázás előfeltétele, hogy a mikroszolgáltatásaink állapotmentesek legyenek. A szűk keresztmetszetet gyanúsan alkotó mikroszolgáltatások azok lehetnek, melyek legalább egy nagyságrenddel nagyobb idő alatt dolgoznak fel egységnyi kérést a rendszerben, ezek lehetnek érdemesek további vizsgálatra.
Második lépésként meg kell vizsgáljuk a kérdéses komponensek miként viselkednek skálázás esetében. Ezt terheléstesztek segítségével érhetjük el. Amennyiben egy komponens nem skálázódik jól, annak refaktorálására lehet szükség.
Az itt leírt tesztek segítségével elég adatot gyűjthetünk ahhoz, hogy javaslatokat tegyünk a rendszerben esetlegesen szükséges változásokra.
