Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Reactive REST service Spring WebFlux használatával

Szűrő megjelenítése

Reactive REST service Spring WebFlux használatával

Mi a reaktív programozás?

A reaktív programozás - ha nagyon röviden szeretnénk megfogalmazni - adatfolyamok (streamek) feldolgozását jelenti. Imperatív megközelítéssel az alkalmazás komponensei explicit kérnek adatot egymástól, majd azt feldolgozzák, végül a folyamat vagy újrakezdődik, vagy befejeződik. A lényeg, hogy ebben az esetben a folyamat elindulása külső behatástól függ, például REST web-service esetében a folyamatot egy standard HTTP kérés fogja elindítani. Kérést küldünk a megfelelő endpointra, az pedig válaszol, ilyen egyszerű. Sok web-alkalmazás esetében ez a viselkedés akár elégséges is lehet, hiszen gondoljunk bele, bejelentkezünk egy ügyfélszolgálati rendszerbe, hogy befizessünk egy számlát, küldünk egy email-t valakinek, elolvassuk a híreket - minden esetben küldünk egy kérést egy service-nek, az pedig teljesíti azt.

A probléma nem ezen service-ek esetében jelentkezik, és így már az elején érdemes is leszögezni, hogy nem minden esetben van értelme reaktív service fejlesztésének. Azonban nem kell messzire menni olyan példákért, ahol elkerülhetetlen a reaktív megközelítés. Videómegosztó portálok (Youtube, Netflix, ...), zeneáruházak (Spotify, Deezer, ...), hírfolyamok a közösségi portálokon, és még sok más példát lehetne felsorolni. A közös pont a reaktív modellre épülő szerver-kliens architektúra alkalmazása, amely nélkül egyik sem volna képes üzemelni.

Felmerülhet a kérdés, hogy miért is van ez így? Hiszen mint a klasszikus web-szolgáltatásoknál, ezen esetekben is felcsapjuk az adott oldalt a böngészőben, vagy megnyitjuk a natív alkalmazást a telefonunkon, megkeressük a számunkra érdekes tartalmat és nézzük/hallgatjuk/olvassuk. Kérést küldünk a szolgáltatásnak, amire az válaszol - semmi különbség. Ez azonban nem igaz, mivel a háttérben egy sokkal összetettebb folyamat indul el, aminek a megértéséhez először is meg kell ismerkednünk a reaktív programozás kapcsán felmerülő egyik legfontosabb tervezési minta, az Observer fogalmával.

Az Observer tervezési minta

Az Observer minta egyike annak a 23 tervezési mintának, melyet a Gang of Four néven elhíresült, négy szoftvermérnökből álló társaság dokumentált le a tervezési mintákról szóló könyvükben még 1994-ben. Az Observer ezen belül a viselkedési minták családjába tartozik, melyek az objektumok közötti kommunikációra írnak le - mondhatni - best practice-eket. Maga a minta azt írja le, hogyan lehet megoldani azt, hogy az objektumok belső állapotának változása delegálható legyen más objektumok számára anélkül, hogy az őket "megfigyelő" (observer) objektumok erre explicit lépéseket tennének. Tehát a megfigyelő objektum rögtön értesül a megfigyelt (observable) objektumok állapot-változásairól.

A minta szerint a megfigyelő objektum feliratkozik a megfigyelhető objektumokra, a feliratkozáson (Subscription) keresztül pedig a megfigyelt objektum értesíteni tudja saját belső változásairól a megfigyelőt. Ez implementációs szinten úgy néz ki, hogy a megfigyelt objektumhoz (Observable, Subject, Publisher) hozzárendeljük a megfigyelőt vagy megfigyelőket (Observer, Subscriber), majd a megfigyelt objektumokban mindazon állapotváltozást előidéző metódusban meghívjuk a megfigyelő objektum egy az értesítésre szolgáló metódusát, melyre annak figyelnie kell. Látható, hogy a belső változások delegálása így a megváltozó (megfigyelt) objektum felelőssége lesz, a megfigyelő feladata "csupán" a változásokra való reagálás.

Nézzünk egy egyszerű, kicsit talán mókás példát - amolyan kedvenc hasonlatomat a témában. :) Adott egy kutya (ő lesz a megfigyelő, azaz az Observer), illetve egy macska, aki egész nap próbálja bosszantani a kutyát (megfigyelt objektum, vagy Observable). A feliratkozás (Subscription) a macska elhaladása a kutya előtt. Imperatív világban a kutya felelőssége lenne a macska elhaladásának vizsgálata, ennek megfelelően pedig jobb híján folyamatosan felkelne, kisétálna a házából, megnézné, ott van-e a macska, majd szomorúan visszabandukolna. Ezzel szemben a reaktív kutyus "feliratkozik" a macska állapotváltozásaira. Amikor elhalad a macska a háza előtt, a kutya a "feliratkozáson" keresztül (tegyük fel, az állapotváltozás az, hogy megérzi a macska szagát) észleli, hogy a macska ott van, felkel, kimegy, megugatja, mire a macska elszalad, majd visszabújik a házába. Lényegében ugyanez történik egy olyan alkalmazásban, ami az observer mintára épülő komponenseket tartalmaz, és ugyanez történik egy reaktív szerver-kliens alkalmazáspár között is.

Ha visszatérünk a fentebb említett szolgáltatásokra, látható, hogy nem volna kifejezetten hatékony, ha a kliens minden - tegyük fel - ezredmásodpercben megkérdezné a szervert, hogy "van még adat?" - ez egyébként is nagyjából lehetetlen lenne, hiszen még nagyon jó internetkapcsolattal is számításba kell vennünk a válaszidő hosszát. Pedig imperatív megközelítéssel ez történne, és látható, hogy ez rövid időn belül súlyos teljesítményproblémákhoz vezetni. Ezzel ellentétben a kliens csupán "feliratkozik" az adatfolyamra, majd várja, hogy a szerver addig küldje, amíg az véget nem ér. A feliratkozás miatt a kliens mindig értesül arról, ha új adatcsomag érkezett, és hozzá tud kezdeni a feldolgozásához. Ha a szerver, vagy akár a kliens lelassul, az sem feltétlenül baj, hisz a kommunikáció nincs szigorú időszeletekre osztva, bár azt mindannyian jól tudjuk mi történik akkor, ha a kliens oldalon elfogynak a feldolgozott adatcsomagok.

Reaktív komponensek, szereplők

Az Observer minta megismerése után ideje most már megnézni, hogyan kapcsolódik mindez össze a reaktív szemlélettel. Bizonyára észrevehető volt, hogy több különböző megnevezést is használtam a fentebbi példa bemutatása során. Ennek oka, hogy egyrészt a reaktív adatfeldolgozásban több különböző komponens vesz részt, illetve más-más terminológiával élnek az egyes implementációk. A legjobb lesz talán, ha az alig néhány éve megjelent Reactive Streams API terminológiáját vesszük alapul. Ennek megfelelően az alábbi résztvevőket tudjuk megkülönböztetni egy reaktív folyamatban:

Publisher

A Publisher feladata az adatfolyam biztosítása a kliens számára. Az Observer minta terminológiájában az observable (a megfigyelt) objektum szerepét tölti be.

Subscriber

A Subscriber komponens - mint azt a neve mutatja - feliratkozik a Publisher által biztosított adatfolyamra. Amennyiben az "emittál" egy értéket, a Subscriber feldolgozza azt. A feldolgozás négy különböző esemény szerint történhet, melyek a következők:

  • Feliratkozás (onSubscribe): értelemszerűen a Publisher komponensre való feliratkozáskor váltódik ki. A metódusnak átadható Subscription objektum feladata lesz az adatfolyam elindításának kérése.
  • Következő emittált érték (onNext): a Publisher új értéket helyezett a streamre, megkezdődhet annak feldolgozása.
  • Emittált hiba: (onError): a Publisher nem tudta a streamre helyezni a következő hasznosítható értéket, ezért egy kivétellel tért vissza. Bár a kivétel kezelhető így, a stream ekkor megszakad, így ha folytatni akarjuk az adatok feldolgozását, új subscriptionre lesz szükség.
  • A stream befejeződött (onComplete): a Publisher nem tud több értéket a streamre helyezni, ezért megszakította a streamet.

Subscription

A Publisher és a Subscriber között fennálló kapcsolat. Az API két metódust definiál a Subscription objektum számára, ezek a request (új elemek kérése) és a cancel (feliratkozás megszakítása).

Processor

A Processor implementációk feldolgozó lépéseket reprezentálnak, melyek egyszerre lehetnek Publisherek és Subscriberek is.

A reaktív szabályrendszer

Mert hogy bizony van ilyen is. A reaktív megközelítés eddig ismertetett része még csak a szigorúan vett technikai (vagyis inkább implementációs) oldal, azonban a reaktív rendszerektől további szempontokat is megkövetel a szakma, melyeket a Reactive Manifesto rögzíti négy pontban. Nézzük, mik azok:

  • Reszponzivitás: egy reaktív rendszernél elvárt a lehető legrövidebb válaszidő. Gyors és konzisztens válaszidőt kell a rendszernek fenntartani, hiszen így biztosítható a szolgáltatás stabilitása és megbízhatósága.
  • Ellenálló képesség: a hibák azonban elkerülhetetlenek, legyen szó szoftveres vagy akár hardveres kiesésről, képtelenség elvárni azt, hogy mindig minden flottul működjön. Így a reaktív rendszerektől elvárjuk, hogy azok egy hibával szemben ellenállóak legyenek, azok a rendszernek a hibát elszenvedő részénél többet ne érintsenek, továbbá gyorsan és hatékonyan képesek legyenek helyreállni. Erre több módszer is létezik, a legfontosabbak talán a replikáció (több példány futtatása ugyanabból a komponensből), az izoláció (a komponensek egymástól elszigetelten "élnek") és a delegáció (a végrehajtás alatt álló taszkok delegálhatóak egy másik egyenrangú komponens-példánynak, ezzel hiba esetén a feladat végrehajtása nem szakad meg).
  • Rugalmasság: a rendszer rugalmasan alkalmazkodik az aktuális terheléshez, így költséghatékonyan képes kiszolgálni a forgalmat, de annak megnövekedése esetén sem omlik össze.
  • Üzenet-vezéreltség: talán a legfontosabb, hogy a reaktív rendszerek nem kérnek adatot, hanem kapják azokat és reagálnak rájuk - röviden: üzenet-vezéreltek. Ezzel a rendszer komponenseinek izoláltsága és azok egymástól való függetlensége javul (úgynevezett loose coupling valósul meg). Az üzenet-vezéreltséggel együtt jár a nem-blokkoló IO használata, mely lehetővé teszi a hívó fél számára azt, hogy ne kelljen megvárnia a rendszer válaszát, így akár más feladatokat is elvégezhet közben. Fontos továbbá megemlíteni a backpressure fogalmát, mely kliens és szerver oldalon is megvalósítható, és az adatfolyam terheléstől függő lassítására szolgál, ezzel elkerülve a kommunikációban résztvevő felek túlterhelését.

Spring WebFlux

Ennyi elmélet bőven elég is volt, nézzük, hogyan történik mindez a gyakorlatban. A Spring 5-ös verziójában jelent meg a WebFlux kiegészítés, mely reaktív támogatással vértezte fel a keretrendszert.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>

Maga a reaktív library egyébként egy Reactive Streams referencia implementáció, ami a Project Reactor nevet kapta. Spring Boot használatával a beállításával sem kell különösebben vesződnünk: a keretrendszer engedélyezi a támogatást, illetve megfelelő containert is indít (akár egy standard MVC alkalmazás esetén), ami ebben az esetben a Netty lesz. Ez egy fontos tény, ugyanis bár Tomcat is használható, a hatékonysága megkérdőjelezhető és a használatából eredendő problémák miatt érdemes inkább nem erőltetni. A Tomcat ugyanis alapvetően nem támogatja a nem-blokkoló IO használatát, továbbá a benne levő Servlet API sincs arra felkészítve. A Netty ezzel szemben eleve reaktív felhasználásra lett kialakítva, így érdemes annak használatánál maradni. Egy érdekesség, hogy a Netty nem támogatja a context-path használatát, így azzal hiába próbálkozunk majd, az alkalmazás mindig a gyökér alatt fog futni.

Nagy különbséget egyébként nem kifejezetten fogunk tapasztalni, sőt, akár maradhatunk is a standard, blokkoló endpointok használatánál, bár akkor az egésznek nagyjából semmi értelme. A controllerek definiálása szinte pontosan ugyanúgy történik, mint standard MVC alkalmazások esetében. Az eltérést a controllerek visszatérési típusában tapasztaljuk majd, ami így néz ki:

@RestController
@RequestMapping("/lsas/stack-status")
public class ServiceInfoController {

    private StackStatusService stackStatusService;

    // ...

    @GetMapping(value = "/discover", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServiceStatus> getServiceStatus() {
        return stackStatusService.getServiceStackStatus();
    }

    // ...
}

A visszatérés a reaktív controllerek esetében a Flux lesz, illetve lehet még Mono is. Mindkettő Publisher kiterjesztés, azonban nagy különbség a kettő között, hogy míg a Mono pontosan nulla vagy egy darab értéket tud emittálni, aztán bezárja a streamet, addig a Flux folyamatosan tud emittálni tetszőleges számú értéket. A fenti példa egyébként egy készülő új alkalmazásom controllere, mely a háttérben a többi alkalmazás státuszát fogja lekérni. Látható, hogy a controller által visszaadott Content-Type ez esetben text/event-stream. Amennyiben a "túloldalon" a kliens is WebFlux alapú, ezt nem szükséges megadni, minden egyéb esetben érdemes lehet odatenni, így a kliens tudni fogja, hogy ez nem egy klasszikus HTTP hívás, bár a protokoll valóban standard HTTP. Érdemes kipróbálni egyébként akár curl használatával. Azt fogjuk tapasztalni, hogy a szerver rekordonként tér majd vissza. A terminálon is folyamatosan látjuk majd az adatok visszatérését, méghozzá oly módon, hogy a rekordok mindig a data előtaggal kezdődnek, majd azután standard JSON dokumentumként a rekord tartalma olvasható. (Nem szeretnék megígérni semmit, de a következő cikkben terv szerint a kliens oldalt mutatom majd be - többfélét is. :) )

A mögöttes service implementáció így néz ki:

    @Override
    public Flux<ServiceStatus> getServiceStackStatus() {
        return Flux.fromStream(serviceStatusAdapterList.stream())
                .map(ServiceStatusAdapter::getStatus);
    }

A ServiceStatusAdapter interfész getStatus metódusa egy-egy HTTP kérést indít az adott service-eknek, megvárja a válaszukat, majd a visszatérést emittálja a streamre (ez a hívás látható a Flux objektum map hívásában). A Flux és Mono is számos feldolgozólépést biztosít, ráadásul azok nagyon hasonlóak a Stream API által biztosított metódusokhoz. Ilyenek a map, filter, sort, merge és még sok más. Emellett a Flux többféle forrásból is képes adatfolyamot építeni, úgy mint tömbök, vagy akár streamek (a fenti service implementációban ezutóbbira látható példa). A backpressure-t is támogatja a framework, méghozzá nagyon egyszerűen a delayElements, delaySubscription és delayUntil metódusok használatával. A legtriviálisabb az első, fix intervallummal késleltethető az elemek emittálása/feldolgozása. A második a feliratkozást késlelteti, a harmadik pedig egy trigger Function implementációt vár, mellyel változó várakoztatást implementálhatunk.

A fentebb ismertetett dolgokkal már egészen jól el lehet indulni a reaktív alkalmazásfejlesztés ingoványos talaján, bár itt természetesen azért nem érnek véget a Project Reactor és a Spring WebFlux képességei. Érdemes a dokumentációt böngészni, főleg ha célirányosan, a megoldandó feladat érdekében tesszük - indulásnak talán ezt a dokumentációt ajánlanám. Mint az látható, természetesen a biztonságot sem kell feladnunk, bár azzal fontos tisztában lenni, hogy némiképp máshogy működik, mint egy standard MVC alkalmazás esetén. Illetve egy reaktív alkalmazás attól lesz igazán reaktív, ha az alatta levő adatbázissal is reaktívan kommunikál. A Spring-nek több népszerű motorhoz is van reaktív támogatása, úgy mint a MongoDB, a Redis vagy például a Cassandra.

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.