1
0
Fork 0
birbnetes-onlab1-beszamolo/src/content/work.tex

135 lines
23 KiB
TeX

% !TeX root = ../thesis.tex
\section{Az elvégzett munka és eredmények ismertetése}
\label{sec:second}
\subsection{Rendszer tervezése}
A \ref{fig:birbnetes-architecture} ábrán látható architektúrát logisztikai okokból kifolyólag a mesterséges intelligenciát megvalósító szoftver ismerete nélkül alkottuk. Úgy tekintettünk rá, mint egy fekete dobozra, amely képes fájlokat beolvasni valamilyen módon és a vizsgálat eredményét is közli a felhasználóval valamilyen módon. Fontos szempont volt, hogy az egyes mikroszolgáltatások belső működése tetszőlegesen refaktorálható legyen anélkül, hogy az másik komponensben módosítást tenne szükségessé.
Jelenleg a rendszer csővezetékként működik, egyik végén beérkeznek a bemeneti adatok, amin a mesterséges intelligencia klasszifikációt hajt végre, majd ennek az eredményét továbbítja a kimenetet feldolgozó szolgáltatások felé, akik a pipeline másik vége.
\begin{figure}[!ht]
\centering
\includegraphics[width=\textwidth]{figures/architecture-simple.pdf}
\caption{A rendszer komponensei \'es azok k\"oz\"otti kapcsolatok}
\label{fig:birbnetes-architecture}
\end{figure}
A rendszer tervezésekor célunk volt a bővíthetőség minél jobb támogatása, ezért megadtuk a lehetőségét annak, hogy a beérkező hangfájlt több mesterséges intelligencia is megvizsgálja párhuzamosan, az ilyen lehetséges elágazási pontokon üzenetsort használtunk. Így, ha a jelenleg egyenes pipeline-t szeretnénk leágaztatni, a megfelelő helyekre fel kell iratkoztatni az új komponenseket. Lentebb olvashatók részletesen az általam javasolt komponensek.
\subsubsection{Input Service}
Fogadja a bemeneti hangfájlokat, ezeket továbbítja a hosszú idejű tárolást megvalósító Storage Service felé. Ezen felül ellátja egy egyedi azonosítóval is és minimális validációt végez.
Kapcsolódik egy relációs adatbázishoz, amelyben lementi az információkat, amelyeket fogadott a hangfájllal együtt. Ezeket összerendeli az egyedi azonosítóval, amelyet ő adott a hangfájlnak.
Miután a Storage Service lementette a hangfájlt, a service publikál egy üzenetet az üzenetsorba, amely tartalmazza a mentett hangfájl címkéjét. A feliratkozott komponensek ezekután a c\'imke használatával letölthetik a lementett hangot és folytathatják rajta a feldolgozást.
\subsubsection{Independent Results Service}
Feldolgozza a mesterséges intelligencia kimenetét, letárolja azt egy tetszőleges relációs adatbázisban. Lekérdezhető tőle - egymástól függetlenül - az egyes hangfájlokról hozott döntés. Az itt tárolt adatok összevethetők az Input Service-ben található adatokkal. Ez segíthet a hiba keresésben, valamint az itt található adatok segítségével a rendszerbe tanulás illeszthető.
\subsubsection{Results Statistics Service}
Feldolgozza a mesterséges intelligencia kimenetét. Az eredményeket egy idősoros adatbázisban tárolja le. Ez az adatbázis sokkal alkalmasabb és hatékonyabb az eredmények "mérés" szerű kezelésén, de cserébe nem lehet lekérni tőle egyesével a hangfájlokról hozott döntéseket. Használata lehetővé teszi dashboardok készítését, ahol heat-map vagy más grafikonok segítségével tájékozódhat a felhasználó.
\subsection{API-k tervez\'ese}
Mivel a projekten ketten dolgoztunk, fontos volt számunkra az egyes szolgáltatások API-jainak definiálása. Erre Swagger-t használtunk, ami egy jól dokumentált, nyitott definíciós eszköztár RESTful API-k leírására. Itt lényeges volt számomra, hogy az elvárt bemeneti formátumon felül minden lehetséges visszatérési státusz kódja és üzenetformátuma dokumentálva legyen az általam fejlesztett szolgáltatásoknak, ugyanis ez megkönnyíti a fejlesztést és a lehetséges félreértéseket is elkerüli. A rendszerben bevezettük a címkék fogalmát, amely egyedi azonosítója minden beküldött hangfájlnak. Az egyes c\'imk\'ekhez tárolt metaadatokat is le lehet kérdezni egy endpoint segítségével.
Az Input Service API-ján kifejezetten sokat gondolkodtam, ugyanis a fájlok és az azokat kísérő metaadatok fogadására több alternatíva létezik. Állományok feltöltése miatt adódik a http mulipart form használata, viszont a metaadatok kerülhetnek külön részbe a kérésnek, vagy egybe valamilyen JSON formátumú adatstruktúrában. Végül az utóbbi mellett döntöttem, ugyanis Pythonhoz rendkívül kiforrott JSON sémavalidációs könyvtárak érhetők el, ezzel szemben ilyen eszközt, ami elegánsan képes multipart formok sémáját validálni, nem találtam.
A Results Statistics Service nem szolgál ki REST-es API-t, csupán fogadja a message queue-n érkező eredményeket és azokat letárolja a hozzá kapcsolódó timeseries adatbázisban.
Ez előbbivel szemben az Independent Results Service egyik legfontosabb tulajdonsága, hogy egy REST API-t nyújt. Itt kihasználva a relációs adatbázis által nyújtott lehetőségeket definiáltam olyan végpontokat, melyek visszaadnak minden pozitív vagy negatív eredményt, valamint adott dátum előtt, illetve után rögzített eredményeket. A cél az volt, hogy le lehessen kérdezni azegyes hangfájlokhoz tartozó adatokat, és ezt egy olyan endpoint segítségével lehet megtenni, amelyben a hangfájl c\'imk\'eje alapján azonosítható ez be.
\subsection{Fejleszt\'es folyamata}
Fejlesztés során igyekeztem arra figyelni, hogy az általam írt kód helyes legyen, ezért a projekthez beállítottam egy folyamatos integrációs rendszert, amely minden git commitra lefuttatott teszteket, és amennyiben a kód átment ezeken a tesztken, container image-et épített és publikált az általam beállított container registry-be. Később a Kubernetes rendszerbe is innen kerültek be az egyes mikroszolgáltatások.
Az egyes komponensek futásakor keletkező kivételeket és hibaeseményeket egy központi rendszerben gyűjtöttük, ami képes volt kimutatások készítésére és egyes eseményekről értesítések küldésére. Mivel a rendszer Kubernetesben futott, jelentősen megkönnyítette a hibakeresési folyamatot, hogy a rendszer képes volt rámutatni arra a kódsorba, amely a hibát kiváltotta, valamint az aktuális környezetet és a hibaüzenetet is tárolta.
\subsubsection{Input Service}
Az Input Service implementálásához a Flask nevű Python web mikroframeworköt használtam. A Flask csak a webes rétegét valósítja meg egy alkalmazásnak, ORM-et (Object-relation mapping) vagy validációt nem nyújt a fejlesztőknek, viszont egyszerűen kiegészíthető tetszőleges beépülő modulokkal. Ebben a mikroszolgáltatásban viszont szükség volt adatbázissal történő kommunikációra és a kapott adatok sémájának validációjára.
Előbbire az SQLAlchemy nevű könyvtárat használtam, melynek kényelmes Flask-os pluginja van. Emiatt csupán definiálnom kellett az általam használt sémát, megadni a Flask-nak az adatbázis elérési helyét, ez után tetszőleges metódusban lehetett lekérdezni, beilleszteni és frissíteni rekordokat az adatbázisba.
Lényeges feladatom volt a konkrét adatbázis motor kiválasztása e. Számos népszerű adatbázis motort megvizsgáltam, hogy az általuk nyújtott szolgáltatások mennyire felelnek meg az igényeinknek. Az első és legfontosabb szempont az volt, hogy Kubernetesbe jól illeszkedjen, a másik lényeges szempont a kis erőforrás lábnyom.
Az első általam vizsgált adatbázis motor a MySQL volt. Ez az első szempontnak megfelelt, lévén, hogy ez az egyik legnépszerűbb adatbázis motor Kubernetesben futtatása megoldott. Emellett relatíve alacsony az erőforrás lábnyoma. Mindezek mellett viszont nem a MySQL-t tartottam a lehető legjobb választásnak a szegényes funkciókészlete miatt.
Kifejezetten tetszetős volt számomra a Microsoft által fejlesztett SQL Server, amit a legfrissebb kiadásban mélyen integráltak a Kubernetesbe és az általa nyújtott szolgáltatások is kedvezőek, viszont sajnos igen sok erőforrást használ.
Az ideális kompromisszumot a képességek, erőforrásigény és Kubernetesbe illeszthetőség között a Postgresql jelentette, amely egy népszerű, nyílt forráskódú adatbázis motor, emellett az SQLAlchemyvel is jól integrálható.
Mivel ez volt az első mikroszolgáltatás, amely üzenetsort is kellett használjon, itt is én választottam ki az általunk használt megoldást. Itt is megvizsgáltam számos megoldást. Ebben az esetben is fontos szempont volt a Kubernetesbe integrálhatóság, viszont funkciók terén csak annyi megkötésünk volt, hogy legyen képes egy küldő, több fogadó típusú üzenetsorok kezelésére.
A KubeMQ tűnt először az egyik legjobb csomagnak, ugyanis telepítése Kubernetesbe egyszerű, üzemeltetése pedig egyszerű. Ellene szól viszont, hogy az ingyenesen elérhető változata havi szinten limitált számú üzenetet képes továbbítani, emiatt használatát elvetettük.
Kipróbáltam az Apache Alapítvány több üzenetsor megoldását is. Az egyik ilyen az egyik legnépszerűbb message queue megoldás, a Kafka. Ez viszont timeseries üzenetek továbbítására specializálódott, ami kifejezetten nem a mi use-case-ünk. A másik az ActiveMQ, amely képességei lefedik a mi igényeinket, de Python-hoz másodrangú eszköztára van csak, a fejlesztők a Java-ban található Java Message Service API-ra koncentrálnak.
A választás végül a RabbitMQ-ra esett, amely egy Erlang nyelven készült, nyílt forráskódú megoldás. Elsőrangú Python könyvtárt készítettek hozzá, a Kubernetesbe telepítése Helm chart segítségével lehetséges. A fejlesztést megkönnyíti az adminisztrációs felülete, ahol minden üzenetsoron minden üzenetet, valamint ezekről statisztikákat is meg lehet tekinteni. Az egyes üzenetsorokhoz megadható a bejelentkezés szükségessége, így a mikroszolgáltatások biztonsági szempontból történő elválasztása is lehetséges. Sajnos nem létezik hozzá működő Flask plugin, ezért a kapcsolat életben tartását kézzel kellett elvégeznem. Ennek ellenére úgy gondoltam, hogy továbbra is a RabbitMQ a legjobb message queue megoldás, ami kielégíti minden igényünk.
A metaadatok validációjára Marshmallow-t használtam. Ez a könyvtár képes előre definiált séma alapján JSON-ből betölteni adatokat, valamint szerializálni őket. A fejlesztés során előkerült egy olyan probléma, hogy az SQLAlchemy segítségével lekérdezett rekordokat a Flask nem tudta automatikusan JSON-ba szerializálni a Python beépített szerializálójával. Erre is megoldást jelentett a Marshmallow, ugyanis létezik egy Marshmallow-SQLAlchemy nevű könyvtár, amely definiál olyan SQLAlchemy sémaosztályokat, amiket a Marshmallow képes JSON-be szerializálni és betölteni.
\subsubsection{Independent Results Service}
A mikroszolgáltatás architektúra lehetővé teszi divergens technológiák együttműködését. Ezt kihasználva az Independent Results Service mikroszolgáltatást nem Python, hanem Kotlin nyelven írtam. Ez azt jelentette, hogy a teljes eszköztár, melyet megismertem nem voltak használhatók e komponens fejlesztése esetében, viszont utóbbi programnyelvben elérhető coroutine-ok olyan előnyt jelentettek, ami miatt megérte megtanulni az új eszköztár használatát. Például a Kotlin esetében a webes logika megvalósítására a legnépszerűbb könyvtár a Ktor, melynek szolgáltatáskészlete hasonlít a Flaskhoz.
A Kotlin egy a Java Virtual Machine-t használó modern programnyelv, amelynek célja a Java hiányosságait, hátrányait javítani. A Java-hoz képest a legnagyobb különbség a null-safe viselkedése és a coroutine-ok használata. Utóbbi használatával könnyedén fejleszthető nagy teljesítményű alkalmazást. Mivel a Kotlin egy fiatal nyelv, valamint leginkább Android appok fejlesztésére használják, így az egyes könyvtárak nem feltétlen olyan fejlettek, mint a Python esetében. Ilyen volt például az általam használt ORM megoldás, az Exposed. Ennek előnye, hogy jól illeszkedik a kotlinos programozási paradigmákhoz, viszont nem képes connection poolingra másik library-k használata nélkül, a Postgresql-t viszont natívan támogatja.
Bár a Kotlin fiatal nyelv, mivel a kód JVM bájtkóddá fordul, minden Java nyelven írott kóddal együtt tud működni egy Kotlin nyelven írt alkalmazás. Ennek hála a a Kotlin RabbitMQ támogatása elsőrangú. A Javához készült RabbitMQ könyvtár használata mellett szükség volt egy olyan library-re, amely lehetővé teszi coroutine-t használó üzenetfeldolgozó metódusok írását.
A Kotlin használata akkor fizetődött ki, miután a REST API-t kiszolgáló réteget megírtam és az alkalmazásba kellett illesszem a message queuetól üzeneteket fogadó kódot. Itt egy tradicionális programnyelvben problémába ütköztem volna, ugyanis az üzenet feldolgozása vagy blokkolná a webes kérések kiszolgálását vagy bonyolult többszálú logikát kellene megvalósítsak. Ez esetben viszont elég volt egy új objektum létrehozása, amiben definiáltam a coroutine-t.
A mikroszolgáltatás fejlesztése során sok időt töltöttem az adatbázis réteg fejlesztésével, mert nehézkesen tudtam csak működésre bírni az Exposed könyvtárat a connection poolinggal és a JSON szerializációval.
\subsubsection{Result Statistics Service}
Timeseries adatbázist a rendszerben csak ez a mikroszolgáltatás használ, így ennek kiválasztása ezen komponens fejlesztése során történt. A kiválasztás során a szempontok csak a mikroszolgáltatás saját szempontjai voltak. Próbáltam kiválasztani a lehető legegyszerűbb megoldást, ugyanis nincs szükség például komplex adatstruktúrák támogatására, viszont a retention policy-k használatának lehetősége és a nagy teljesítmény követelmény volt.
Először a Prometheust vizsgáltam meg, amely egy népszerű timeseries adatbázis rendszer, amit főleg metrikák tárolására használnak. Emiatt támogatja a retention policy-ket, viszont nagy erőforrás lábnyomba van és a lehetőségeinek kis százalékát tudnám csak kihasználni.
Az InfluxDB egy nyílt forráskódú, nagy teljesítményű timeseries adatbázis. Nagy előnye az egyszerű API-ja és jó támogatottsága. Kellően alacsony az erőforrásigénye és SQL-szerű lekérdezőnyelve kedvező opcióvá teszi. Kifejezetten könnyű hozzá Grafana segítségével grafikonok, dashboardok készítése.
Végezetül az OpenTSDB timeseries adatbázist vizsgáltam meg. Könnyen lehet vele grafikus interfészén grafikonokat készíteni, viszont ezek az opciók limitáltibbak, mint a Grafana esetében, valamint az adatbázis rendszer Hadoop alapokon nyugszik, ami azt jelenti, hogy a projekt jelenlegi skáláján nem ez a legjobb választás.
A Result Statistics Service mikroszolgáltatást először C\# nyelven kezdtem el megvalósítani, ugyanis .NET fejlett aszinkron API-val rendelkezik és eredeti elképzeléseim szerint erre szükség lett volna annak érdekében, hogy az eredményeket megfelelő sebességgel le tudja tárolni a mikroszolgáltatás, viszont rövidesen olyan problémába ütköztem, amely megakadályozta a .NET Core platform használatát, ami az InfluxDB API-ját implementáló könyvtárak és azok dokumentációjának rossz minősége volt. Ez jelenthette volna az InfluxDB leváltását, viszont úgy gondoltam az itt ismertetett szakmai érvek erősek voltak annak megtartása mellett. Emiatt döntöttem úgy, hogy inkább a programnyelvet cserélem le Pythonra.
E mikroszolgáltatás esetében az üzleti logika eleve nem bonyolult, az üzenetsoron érkező üzenetben található adatot az aktuális idővel megcímkézve be kell illeszteni az InfluxDB-be. Ezt viszont szerettem volna aszinkron módon megtenni, ugyanis elképzelhető az üzenetsoron nagy számú üzenet érkezése rövid idő alatt. Ennek implementációját is elvetettem, ugyanis az InfluxDB, ahogy kutatásom során kiderült, olyan nagy számú kérést képes feldolgozni rövid idő alatt, hogy nem érné meg aszinkron logikával bonyolítani a mikroszolgáltatást \cite{influxdb-performance} .
\subsection{Kubernetes}
Az általunk használt Kubernetes klaszter egy fizikai számítógépen futott négy virtuális gépen. A rendszerben egy Master node és három Worker node volt. A tárterületet ugyanerről a fizikai gépről osztottuk ki a fürt számára NFS-sel, a Persistent Volume-okat statikusan definiáltuk. Az általam fejlesztett mikroszolgáltatásokat és az azokhoz kapcsolódó komponenseket magam telepítettem a klaszterbe.
Az általunk fejlesztett rendszer számára létrehoztam egy Kubernetes névteret, ahova telepítettük az általunk fejlesztett mikroszolgáltatásokat és minden általuk használt komponenst.
Minden mikroszolgáltatáshoz tartozó Kubernetes API obejktumokat leíró YAML állományt hasonlóan készítettem el. Magát az alkalmazást egy Deployment segítségével telepítettem a klaszterbe. Az egyes mikroszolgáltatások környezeti változók segítségével konfigurálhatók. Ezeket ConfigMap segítségével állítottam be. Mivel el kell érjék egymást a szolgáltatások, szükség volt Service objektum definiálása, ezek viszont ClusterIP típusúak voltak, hiszen a külvilágnak nem szabad közvetlen elérni egyes végpontokat.
A RabbitMQ telepítését Helm segítségével végeztem. Egészen pontosan a Helm legújabb, hármas verzióját használva, ami használatához nincs szükség semmilyen extra komponens telepítésére a klaszterbe. Az általam használt Helm Chartban lehetőség volt megadni a PersitentVolumeClaim (PVC) méretét, melyet 10 gigabájtra állítottam.
A PostgreSQL számára saját Deploymentet definiáltam, ugyanis a fejlesztők által készített konténer image ezt könnyedén elvégezhetővé teszi. Megadtam minden adatbázis számára egy-egy 10 gigabájtos PVC-t, valamint a saját komponenseimhez hasonlóan a konfigurációs környezeti változókat ConfigMapben adtam meg. Itt is szükség volt hálózati kommunikációra, ezért az adatbázis motoroknak is adtam meg saját ClusterIP típusú Service objektumot.
Az InfluxDB telepítése hasonló lépéseket követelt meg, csupán más paraméterekkel.
Mint azt az első fejezetben írtam, a mikroszolgáltatás alapú rendszerekhez gyakran használnak API Gateway-t. Ennek kiválasztását és telepítését is én végeztem. Megvizsgáltam olyan megoldásokat, amik Kubernetes Ingress Controllerként viselkednek és olyanokat is, amelyeket szimpla Kubernetesbe telepített alkalmazásként lehet használni.
Az egyik legnépszerűbb API Gateway, ami az utóbbi kategóriába sorolható a Netflix által Java nyelven fejlesztett Zuul. Ezt a megoldást nagy számú mikroszolgáltatásokból álló rendszerekhez tervezték, amelyek nagy számú kérést kell kiszolgáljanak. Konfigurációja Spring Boot segítségével lehetséges YAML fájllal, vagy Java kód alapú konfigurációval.
Alternatív a Spring projekt által készített Spring Cloud Gateway. Ez gyakorlatilag ugyanazon szolgáltatásokat nyújtja, mint a Zuul, viszont támogatja a reaktív programozási paradigmát, az általunk fejlesztett rendszer viszont nem ezt követi.
A Kubernetes-natív kategóriába sorolható API Gatewayek közé tartozik a Kong, ami a háttérben NGINX-et használ, annak konfigurációját generálja. Támogat minden fontosabb funkciót, melyet egy API Gateway-től elvárhatunk. Ilyen például a rate limiting, circuit breaking, fallback és autentikációs képességek. Előnye, hogy az Ingress objektumban lehet konfigurálni az egyes endpointokat.
Alternatíva az Ambassador használata, ami a háttérben Envoy-t használ. Szintén támogat minden fontos képességet, viszont hátránya, hogy konfigurációja label-be ágyazott YAML segítségével lehetséges. Ez nem kívánatos, hiszen erre Custom Resource Definitionöket hibabiztosabban és flexibilisebben lehetne használni.
Megvizsgáltam továbbá a Gloo-t, aminek különleges előnye, hogy képes automatikusan alkalmazások API-jának automatikus felismerésére OpenAPI definíciók segítségével, amiket mi a fejlesztési folyamat elején definiáltunk.
A választásom végül a Kong-ra esett, mert számomra szimpatikus volt, hogy egy egyszerű NGINX webszerver segítségével oldja meg funkcióit. A telepítést ez után az Ingress objektumok definiálásával és a klaszterbe telepítésével folytattam.
\subsection{Elkészült rendszer próbája}
A mikroszolgáltatások fejlesztése közben önmagukban teszteltem őket és próbáltam ki működésüket. A fejlesztési folyamat végén viszont közösen kipróbáltuk, hogy működnek együtt az általunk fejlesztett komponensek, milyen hibák jönnek itt elő.
Előkerültek olyan hibák, amik eltérő feltételezésekből következnek. Ezen esetekben egyeztettük a konvencióink, majd a javítás után újra teszteltük a működést.
A folyamat eredm\'enyek\'ek\'ent kaptunk egy műk\"odő rendszert, aminek minden komponense k\'epes volt egym\'assal egy\"uttműk\"odni. Beadtunk p\'eld\'aul egy olyan hangf\'ajlt, amely sereg\'ely \'eneket \'es egy olyat, amin motorhang volt. R\"ovid idő eltelt\'evel lek\'erzhető volt az eredm\'eny az Independent Results Service-től, mindkettőt motorhangk\'ent klasszifik\'alta az MI. Ugyanekor az InfluxDB-ből lek\'erezhető volt az időpontokban t\"ort\'ent m\'er\'esek. Ez nem probl\'ema a mi projekt\"unk szempontj\'ab\'ol, elv\'egre a f\'el\'ev elej\'en kiindul\'o felt\'etelez\'es\"unk volt, hogy az MI modellek adottak, azokon nem c\'elunk v\'altoztatni, azokat jav\'itani.
\subsection{\"Osszefoglal\'as}
A félév során számos új tudással bővültem, többek között elsajátítottam a Kotlin nyelv alapszintű működését, valamint megtanultam, hogy kell mikroszolgáltatás alapú rendszert tervezni, fejleszteni és az ezek Kubernetesbe telepítésével kapcsolatos megontolásokat is részletesebben megismertem. Több technológia működésébe nyertem betekintést, ezzel tágítva az eszköztáram.
Elkészítettem több újra használható Kubernetes Deploymentet és a hozzájuk tartozó egyéb API objektumokat, ami a későbbi munkám során segítségemre lehet. Megterveztem és végrehajtottam az általunk fejlesztett komponensek automatikus Kubernetesbe telepítését és a frissítések automatikus elvégzését.
Marcell-lel közösen egy majdnem kész terméket fejlesztettünk le, amire büszke vagyok. A munkafolyamataink részletesen dokumentáltuk, így amennyiben a jövőben a projektet folytatni kívánja valaki az itt leírtak és az általunk felhalmozott dokumentáció alapján hatékonyan megteheti.
A továbbiakban véleményem szerint érdekes lehet a kialakított rendszert teljesítmény, áteresztőképesség szempontjából megvizsgálni, mire képes és hogyan viselkedik komolyabb terhelés alatt.