Telemetria és a Java

Napjainkban a telemetria egy nagyon fejlődő terület, napról napra jelennek meg új eszközök és szabványok, melyeket igen nehéz nyomonkövetni. Ez a poszt ebben szeretne egy kis rendet tenni, hiszen úgy lehet valamit a legjobban megismerni, hogyha másoknak elmagyarázod.

A telemetria (telemetry) klasszikusan egy rendszer különböző elemein mérési adatok előállítása és továbbítása egy központi helyre. Ennek célja, hogy a múltbéli és jelenlegi adatok elemzésével meg lehessen bizonyosodni a rendszer helyes működéséről, észlelni, esetleg előre jelezni lehessen annak hibáit. Erre különböző eszközöket és folyamatokat használunk, melyek biztosítják a rendszer folyamatos megfigyelhetőségét (observability).

Szoftverrendszer esetén is pontosan erről van szó, különböző mérési adatokat állítunk elő különböző szinteken, pl. operációs rendszer szinten mérjük a CPU, memória és lemezhasználatot, de alkalmazás/szolgáltatás szinten mérhető a bejelentkezett felhasználók, beérkező kérések, tranzakciók száma, válaszidők. Ezen adatokból kinyert információkból következtethetünk a szoftver állapotára, hibára. A túl magas válaszidő biztos valami problémára utal, de az rossz jel lehet, ha hirtelen lecsökken a tranzakciószám.

AWS Lambda és a Spring Cloud Function

Az AWS Lambda egy AWS szolgáltatás, mely lehetővé teszi, hogy a különböző programozási nyelven megírt alkalmazásainkat (függvényeinket) anélkül tudjuk futtatni, hogy gondunk lenne az azt kiszolgáló infrastruktúrára. A szolgáltatás a serverless computing kategóriába tartozik, ami kicsit megtévesztő, hiszen itt is van futtató környezet, azonban ez számunkra láthatatlan. A másik ismert megnevezése a Function as a Service (FaaS). Természetesen más cloud szolgáltatóknak is van hasonló megoldásuk, a Microsoftnál az Azure Functions, a Google-nél a Cloud Functions.

A függvények tipikusan valamilyen beérkező eseményre reagálnak, ami lehet egy felhasználói kérés, valamilyen üzenet más szolgáltatás (pl. S3) felöl, vagy üzenet brókeren keresztül beérkező üzenet, pl. valamilyen IoT eszköz, vagy alkalmazás irányából. Miután megírjuk a választott programozási nyelven a függvényünket, azt össze kell csomagolni, és telepíteni. Ez lehet akár egy tömörített fájl, akár egy container image. A szolgáltatás rugalmassága miatt ma akár bármilyen programozási nyelven írhatunk ilyen függvényeket.

A futtatókörnyezettel nem kell foglalkozni, azt a szolgáltatás önmaga kezeli. Ez magában foglalja a hardvert és az operációs rendszert, annak frissítését, erőforrás kezelést, automatikus skálázást és naplózást. És fizetnünk is csak annyit kell, amennyit futott a függvényünk, idő alapján (ezredmásodperc alapon).

A kód futtatása természetesen konténerekben történik, melyek tartalmazzák az adott programozási nyelv futtatókörnyezetét. Azonban a szolgáltatás nem mindig indít minden bejövő eseményhez új konténert, hanem megpróbálja azokat újrahasznosítani. Ennek egyik következménye, hogy bizonyos kérések kiszolgálása tovább tart, mikor a konténert el kell indítani (ún. cold start). Bizonyos kérések kiszolgálása azonban nagyon gyorsan megtörténik (ún. warm start). A nem használt konténereket a szolgáltatás automatikusan leállítja. Másrészt mivel a kérések kiszolgálása ugyanabban a konténerben futhat, lehet ezeknek egymásra hatása (pl. ha használják a fájlrendszert).

Egy AWS Lambda konténer rendelkezésére álló memória konfigurálható 128 és 10240 MB között, természetesen egy ezredmásodperc ára is annál nagyobb, minél több memóriát használ a konténer. Valamint 512 MB és 10240 MB között tudunk hozzá tárhelyet is rendelni, minél többet, annál nagyobb felárral.

Amennyiben saját image-t állítunk elő, először a Amazon Elastic Container Registry (Amazon ECR) szolgáltatásba kell feltöltenünk.

Ha a Lambda függvényünket REST-en akarjuk meghívni, akkor még az API Gateway szolgáltatást kell használnunk.

Architektúra

Ezek alapján mikor érdemes az AWS Lambdát használni? Olyan funkciók megvalósítására ideális, amik eseményekre reagálnak, nincs rá folyamatosan szükségünk, nem kell azonnali választ adnunk, és az üzenetek beérkezése nem egyenletes. Ilyen lehet pl. a kép- és videófeldolgozás, bejövő események adatainak aggregálása, (PDF) dokumentumok generálása.

Vannak azonban esetek, amelyeknél el kell gondolkodnunk, hogy tényleg az AWS Lambda-e a jó választás. Ilyenek például a klasszikus webes alkalmazások. Itt egy gyenge forgalom esetén lehet, hogy a cold start miatt nagyobb lesz a válaszidő. Valamint egy nagyon nagy terhelésű alkalmazás esetén pedig lehet, hogy az idő alapú számlázás nem a legkedvezőbb.

A Spring Cloud Function egy olyan projekt, mely a Spring ökoszisztéma tagja, Spring Boothoz illeszkedik, és segítségével az üzleti logikát Java 8 funkcionális interfészek implementálásával tudjuk megvalósítani. Ezáltal teljesen leválasztja az infrastruktúrális elemekről. Azonban adapterekkel, konfigurációt használva össze tudjuk kapcsolni az üzleti logikát különböző szolgáltatásokkal, pl. AWS Lambdaval is. A Spring Cloud Stream használatával pedig különböző message brokerrekkel, mint pl. RabbitMQ-val vagy Apache Kafkával.

Mivel nem mindegy, hogy a function mennyi memóriát használ, és mennyi idő alatt indul el, ezért a Java bytekód nem feltétlenül a legjobb választás. Azonban a Spring Boot alkalmazásokat könnyű natívvá fordítani.

Ebben a posztban azt mutatom be, hogyan lehet egy natívra fordított, Spring Cloud Functiont használó alkalmazást AWS Lambda függvényként futtatni.

(A példa ingyenesen kipróbálható az AWS Free Tier használatával. Bankkártya adatok megadása szükséges.)

Project Reactor és a szálkezelés

A Project Reactor egy Springhez közel álló reaktív könyvtár. Erre épül a Spring Framework 5-ben megjelent WebFlux webes keretrendszer reaktív webes alkalmazások készítésére. Ez nagyban hasonlít a Spring MVC-re, azonban reaktív módon működik. Ezzel találkozhatunk akkor is, ha WebClient-et használunk REST webszolgáltatások hívására (ez a RestTemplate-et hivatott leváltani, de úgy tűnik, hogy mégis szükség van egy szinkron megvalósításra is, ezért jelent meg a 6.1-es Spring Frameworkben a RestClient). A reaktív elvekről és keretrendszerekről már írtam a Reaktív programozás posztomban.

A Reactor ún. concurrency-agnostic, ami azt jelenti, hogy nem erőltet ránk semmilyen párhuzamossági modellt. Azonban a fejlesztőnek lehetőséget ad a szálak használatára.

Ez a poszt azzal foglalkozik, hogy lehet szálakat használni, hogyan befolyásolja a szálak használatát a publishOn és subscribeOn operátor (Mono és Flux osztályokban lévő metódusok).

A REST API hypertext-driven legyen

Bár erős vitákat tapasztalok a REST körül, valahogy a hypertext-driven API-val ritkán találkozom.

Roy T. Fielding, a REST megálmodója a gyakran idézett REST APIs must be hypertext-driven cikkjében a következőt mondja:

In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.

Erre erősít rá a Richardson Maturity Model is, mely három lépésben mutatja be a REST alapvető elemeit:

  • Level 1 - Resources
  • Level 2 - HTTP Verbs
  • Level 3 - Hypermedia Controls

És egy API csak akkor nevezhető RESTfulnak, ha az összes szinten leírtakat teljesíti, igen a hypermedia control használatát is.

De mit is jelent ez? Az állítás egyszerű: a kliensnek minden előzetes tudás nélkül igénybe kell tudnia venni a RESTful API mögötti szolgáltatásokat, és ez linkek segítségével valósulhat meg.

Pl. az előző Workflow REST API-n posztomban szereplő Issue Tracker alkalmazás a következőképp adja vissza a hibajegyek listáját:

{
  "_embedded": {
    "jtl:issueResourceList": [
      {
        "id": 1,
        "title": "Write a post about REST",
        "state": "NEW",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/issues/1"
          },
          "actions": {
            "href": "http://localhost:8080/api/issues/1/actions"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/issues"
    }
  }
}

Látható a hibajegyek listáját reprezentáló URL-jét tartalmazó self link, valamint hibajegyenként a hibajegyet reprezentáló URL-jének linkje szintén self névvel, valamint a hibajegyhez tartozó lépéseket reprenzentáló URL actions névvel.

Ez a Hypermedia as the engine of application state (HATEOAS), melyet szintén Roy T. Fielding vezetett be a disszertációjában. A HATEOAS szerint a szerver a kliens számára linkekkel jelzi, hogy mit tehet.

Az elv mögötte az, hogy a REST API-t is úgy lehessen használni, mint a webet. Ahol vannak oldalak és linkek és semmilyen előzetes információra nincs szükségem a használatához.

Hogy hogyan viszonyul ez az OpenAPI-hoz (Swaggerhez)? Elvileg a cél ugyanaz, és míg a HATEOAS a REST gonolatiságához illeszkedik, az OpenAPI inkább az RPC-hez.

Workflow REST API-n

Bár kétségtelenül a REST a legelterjedtebb kommunikációs mód, akár ugyanolyan platformon fejlesztett alkalmazások között is, REST API tervezéskor nekem sok kérdés merül fel, melyre nem kapok megnyugtató választ, és sok olyan megoldással találkozom, amelyek nem felelnek meg a REST elveknek, vagy egyszerűen csak nem tetszenek.

Sokáig próbáltam megfogalmazni, hogy mi az alapvető problémám, és talán most sikerült közelebb kerülni. Vegyünk két microservice-t, melyek mindegyike Javaban készült, Spring Boot használatával. Mindkét oldalon a programozási nyelv alapeleme az osztályok, ahol érvényesül az egységbezárás, azaz az attribútumok mellett ott vannak a metódusok, a megfelelő paraméterekkel és visszatérési értékekkel. Ez a legtöbb fejlesztőnek triviális.

A REST a kettő között viszont egy teljesen más absztrakció. Bár a REST nem mondja ki, hogy csak HTTP-vel együtt használható, más protokollal működni még nem láttam. A REST alapfogalma az erőforrás, és az azon végzett műveletek, melyeket a HTTP metódusok valósítanak meg, mint a GET, POST, PUT és DELETE. Ez nehezen feleltethető meg az objektumorientált világnak. Egy objektum létrehozására tipikusan konstruktort használunk, ebből nem feltétlen egy van. Ezen kívül egy objektumnak lehet több metódusa, amik nem feltétlenül feleltethetőek meg a HTTP metódusoknak, hiszen nem csak ezeket az alapműveleteket használjuk, valamint lehet belőle sokkal több is, mint négy. Míg a Java nyelv szabvány, addig a REST csak egy ajánlás, és az sincs megfelelően definiálva, hogy hogyan kéne implementálni, az meg aztán pláne nincs, hogy a kettőt hogyan feleltessük meg egymásnak. És ennek az az eredménye, hogy minden projekten teljesen más megoldásokat látok, a legtöbb helyen kompromisszumokkal.

Absztrakciók

Az RPC alapú kommunikáció esetén igazából nincs ilyen probléma, mert nem szükséges átfordítás, nincs szükség az absztrakció váltására. Hiszen ott is eljárások vannak, paraméterekkel és visszatérési értékekkel. A Spring Frameworkben volt is ilyen lehetőség, hogy egy távoli eljáráshívás történt, azonban a protokollt kódolás nélkül cserélni lehetett alatta, pl. választhattunk RMI-t, vagy ki emlékszik még a Hessian és Burlap protokollokra. Sajnos ez azóta eltűnt. A SOAP esetén sem volt akkora probléma az átfordítás, hiszen ott is vannak az operációk (eljárásnak felel meg), és a paraméter, valamint a visszatérési érték is egy-egy (XML) dokumentum. Vannak modernebb RPC alapú kommunikációs protokollok is, mint pl. JSON-RPC, vagy az egyre inkább elterjedő multiplatform gRPC, bináris Protocol Buffers formátummal.

Amennyiben CRUD alkalmazásról van szó, a REST használata talán triviális. De ha egy olyan alkalmazásról beszélünk, amiben komoly üzleti logika van (és hiszem, hogy a legtöbb alkalmazásnak ilyennek kéne lennie, csak a fejlesztők “butítják le” CRUD alkalmazássá, amely végső soron oda is vezethet, hogy borzasztó felhasználói felületek kerülnek kifejlesztésre), akkor már több kérdés merül fel.

Vegyünk például egy hibajegykezelő alkalmazást (issue tracker), mely rendelkezzen egy minimális üzleti logikával. Ha hibajegy kerül felvételre, az új (new) állapottal kerül létrehozásra. Amennyiben valaki elkezd rajta dolgozni, átkerül folyamatban (in_progress) állapotba. Amennyiben elkészült, átkerül megoldott (resolved) állapotba. Bár a legtöbb hibajegykezelő alkalmazás lehetővé teszi, hogy egyedi munkafolyamatot lehessen benne létrehozni, ettől most tekintsünk el, mert akkor megint más problémák merülnek fel. Ezt egy egyszerű állapotdiagrammal lehet a legegyszerűbben szemléltetni.

Absztrakciók

Talán egy hibajegy felvételét még el tudom képzelni REST API-n (bár már ott is vannak kérdéseim), azonban a különböző munkafolyamat lépések implementálásakor már bizonytalanabb vagyok. Ez a poszt ezt a problémakört járja körbe, forráskódokkal alátámasztva.