Messaging

A REST API megvalósításokban használt technikák alapvetően a szinkron kommunikációt támogatját. A microservice-ek elterjedésével az aszinkron kommunikációs formák is megszaporodtak, melyek általában üzenetek formájában történnek.

Alapfogalmak

Az üzenet alapú kommunikációban két fél vesz részt: a küldő (sender) és a fogadó (receiver). A két fél között a csatorna (channel) felelős az üzenet továbbításáért, mely lehet egy egyszerű függvényhívástól kezdve, web socket, vagy HTTP kérés, vagy bármi egyéb.

Amikor a következő elvárásokkal rendelkezünk a rendszerünk felé, akkor érdemes lehet az üzenet alapú architektúrát választanunk:

  • Garantált kézbesítés: A fejlesztők biztosak akarnak abban lenni, hogy az üzenet amit küldenek eléri a megjelölt célállomást, azaz eljut a fogadóhoz. Kritikus fontosságú üzeneteknél fontos ez a szempont (pl.: fizetés, részvények árfolyamok).
  • Laza csatoltság: Egy modern architektúrában elvárás lehet, hogy a rendszer komponensei lazán csatoltak legyenek, mely cél eléréséhez az üzenet alapú architektúrák nagyban hozzájárulnak.
  • Skálázhatóság és magas elérhetőség: Több küldő és fogadó húzható be a rendszerbe, melyekkel biztosíthatjuk, hogy elbírják a megnövekedett terhelést is. Az üzeneteket több brokeren (a kapcsolatok, üzenetekért, az üzenetek küldéséért felelős elem, mely szinkron és aszinkron is tud működni) keresztül is küldhetjük. Ezzel biztosítható, hogy ha valamelyik komponens lehal, akkor lesz aki átvegye a szerepét.
  • Aszinkron: Ha az alkalmazásunknak gyorsnak kell lennie (gyorsan kell fogadnia a kéréseket), akkor az aszinkron üzenetekkel ezt el tudjuk érni.
  • Interoperability: Az előállított üzeneteket a különböző fogadóknak tudnia kell értelmeznie. Az üzenet lehet plain text, JSON, XML vagy szerializált objektum. Ezt az üzenet alapú rendszerekben alkalmazott protokollokkal tudjuk elérni.

Üzenetküldési modellek

  • Point-to-point: A küldő az üzenetet egy Queue-ba (FIFO) helyezi el, ahonnan pontosan egy fogadó veheti ki az üzenetet. Az üzenetet általában perzisztálják a rendszerek (broker), hogy garantálják a kézbesítést. Miután az üzenetet kiolvasta és feldolgozta a fogadó, akkor küld egy értesítést (acknowledgement), mellyel nyugtázza az üzenet fogadását. Ezután az üzenet kikerül a queue-ból (többé már nem kézbesíthető). Az ilyen rendszerekben több küldő is küldhet üzeneteket ugyanabba a queue-ba, illetve több fogadó is vehet ki üzeneteket ugyanabból a queue-ból, mely biztosítja a skálázhatóságot is, mivel a küldők és a fogadók mit sem tudnak egymásról.
  • Publish-subscribe: Ebben az modellben a publisher elküld egy üzenetet (melyet ilyenkor topic-nak nevezünk), melyet aztán a topic-ra feliratkozók megkapnak. Tehát a fő különbség, hogy egy üzenetet többen is megkapnak, nem csak egy fogadója van.

Az üzenetek lehetnek tartósak (durable) vagy nem tartósak (non-durable), mely azt jelenti, hogy az adott üzenet képes-e túlélni egy rendszerleállást vagy sem. Előbbire példa lehet egy rendelés (webes áruházban), míg utóbbira egy tőzsdei aktuális árfolyam érték.

Ugyanúgy, ahogy más domain-ekben is a messaging témakörében is vannak gyakran felmerülő problémák, melyeket a tervezési minták segítenek jól megoldani. A fentieken kívül néhány további megfontolás, melyet a messaging során használhatunk:

  • Message type patterns: Az üzenet típusára vonatkozó javaslatok (JSON, XML, plain text, byte array, stb.)
  • Message channel patterns: A közvetítő szerepet betöltő channel típusa és annak attribútumai. A küldő és a fogadó a csatornát használja nem pedig közvetlenül beszélgetnek egymással. Lehetséges attribútumok: kérés-válasz típusú csatorna, egyirányú csatorna. A csatornára vonatkozó tervezési minta lehet például a point-to-point modell alkalmazása.
  • Routing patterns: A küldő és a fogadó közötti útvonalra vonatkozó küldési módot adja meg. Ezt általában vagy le kell programozni vagy éppenséggel maga az üzenetküldő rendszer (broker) támogatja azt (pl.: RabbitMQ).
  • Service consumer patterns: A fogadó viselkedésének megadását írja le. Például tranzakciós viselkedés.
  • Contract patterns: A küldő és a fogadó közötti egyszerű kommunikációs interface biztosítása (REST API esetében például megszabjuk, hogy JSON vagy XML alapon kell küldeni az infot).
  • Message construction patterns: Az üzenet létrehozásának folyamatát adja meg úgy, hogy az üzenetet aztán az üzenetküldő rendszer (broker) továbbítani tudja. Például a HTTP alapú üzenetküldőkben lehetnek Header információink és hozzá tartozó body rész, melyben az üzenet törzse található meg.
  • Transformation patterns: Azt szabja meg, hogy az üzenet törzsét, hogyan kell átalakítani akkor amikor az beérkezik a broker-hez. Ezt használhatjuk akkor, amikor egy üzenetet on-the-fly még ki kell bővítenünk valamilyen információkkal.

Messaging API-k

A kliensek különféle üzenetküldő API-kat használhatnak annak érdekében, hogy az üzenetküldő rendszerrel, azaz a broker-rel kommunikálni tudjanak (küldeni és fogadni üzeneteket). Néhány üzenetküldő rendszer saját API-t kínál, vannak elfogadott standardok, illetve készülőben lévőek is. A következőekben ezekről adunk egy rövid összefoglalást.

Java Messaging Service (JMS)

Kliensek közötti üzenetek küldését teszi lehetővé, mely egy régóta meglévő problémára adott standard megoldást (JEE specifikáció részeként). Teszi mindezt termelő és fogyasztó alapon, laza csatoltság mellett, megbízhatóan és aszinkron módon, melyet elosztott környezetben is remekül lehet használni.

A JMS maga szintén egy API, melyet azért hoztak létre, hogy a különböző üzenetküldő rendszerek (broker) együttes használatát elősegítse. Ugyanolyan köztes szintet képvisel, mint mondjuk a JDBC a relációs adatbázisok kezelésében, mivel a létrehozásakor a létező üzentküldő rendszerek által kínált funkcinalitásokat fogta össze egy standard API-ba.

Támogatja mind a point-to-point illetve a publish-subscribe üzenetküldési modelleket.

Mivel egy standardról van szó, így a legtöbb üzenetküldő rendszer támogatja a JMS-t. Ugyanakkor, mivel a standard Java környezetre fogalmazza meg az API-t, így a JMS-t csak Java környezetben tudjuk használni, mely limitálhatja annak használhatóságát (pl.: különböző nyelvű microservice-ek esetén nem használható).

Az egyik JMS-t teljes egészében támogató üzenetküldési rendszer az Apache (ActiveMQ) Artemis rendszere.

Rendszer specifikus API-k

A JMS rendszer egyik hátránya a közös nevezőben rejlik, mely miatt az adott üzenetküldő rendszernek nem tudjuk kihasználni az összes extra funkcionalitását, melyet amúgy biztosítana (más rendszerben ezek lehet nem is léteznek).

A fent említett Apache Artemis rendelkezik saját API-val is.

Restful APIs

Az utóbbi időben igen népszerű a REST API alapú üzenetküldés is. Jelenleg de-facto stantard REST API felé halad a világ, mely a JMS korlátjait képes kiküszöbölni, illetve nem korlátozódik egyfajta üzenetküldő rendszerre. Általában HTTP-t használnak, mint a küldést biztosító protokoll, mely előnye az egyszerűség, illetve az, hogy már elég jól kiforrott.

Az Artemis rendszer is rendelkezik REST interface-el.

Advanced Message Queuing Protocol (AMQP)

Az AMQP egy specifikáció, mely lehetővé teszi a különböző üzenetküldő rendszerek együttműködését (interoperable messaging). Az Artemis megvalósítja az AMQP-t, így bármely kliens, amelyik támogatja az AMQP-t az használni tudja azt.

(MQTT)

Az MQTT egy lightweight üzenetküldéi standard. Leginkább az IoT világában használatos, ahol az eszközök és a hálózat is limitált teljesítménnyel bír.

STOMP

A STOMP egy egyszerű szöveges alapú üzenetküldési protokoll, mely HTTP alapú specifikációt ad meg. Egyszerűsége miatt gyorsan készíthetőek hozzá tetszőleges nyelvű kliense.

OpenWire

Az Apache ActiveMQ saját "wire protocol"-ja. A wire protokoll azt jelenti, hogy meg van adva, hogy milyen interface-eket/végpontokat kell biztosítani, hogy a kliens és a broker kommunikálni tudjon.

Klaszterezés

Sok üzenetküldő rendszer támogatja a klaszterbe szervezést, mely a skálázhatóság egyik alapköve. Klaszterezésről, akkor beszélünk, amikor több üzenetküldő szervert fogunk össze és azok együttese szolgálja ki a kéréseket (üzenetküldés és fogadás).

Apache ActiveMQ Artemis

Ebben az alfejezetben rövid áttekintést adunk az Apache ActiveMQ Artemis üzenetküldő rendszerről, illetve annak architektúrájáról.

Architektúra

A rendszert POJO-k mentén tervezték, így annak architektúrája könnyen megérthető. Minden Artemis szerver rendelkezik egy saját nagy teljesítményű perzisztáló egységgel, melyet journal-nak hívnak. Ezt a journal-t használja a rendszer az üzenetek és egyéb a rendszerben hosszan megőrizni kívánt elemek tartós tárolására. Ez a tartós tár sokkal de sokkal gyorsabb, mintha relációs adatbázisba mentenénk az üzeneteket (ugyanakkor JDBC-t is használhatunk, ha nagyon ragaszkodunk hozzá, de akkor számoljunk a performancia drasztikus esésével).

Az Artemis kétféle API implementációt ad számunkra, melyeket a kliensek használhatnak az üzenetküldésre:

  • A Core API-t, mely a saját rendszer specifikus Java API-ja. A broker-t direkt módon tudjuk kontrollálni (address és queue létrehozás, stb.)
  • JMS 2.0

Apache ActiveMQ Artemis biztosít továbbá egy sor protokoll megvalósítást is, így az azokat támogató kliensek széles körét alkalmazhatjuk az üzenetküldés során. Ezen prtokollok:

  • AMQP
  • OpenWire
  • MQTT
  • STOMP
  • HornetQ
  • Core (Artemis CORE protocol)

A JMS szemantikát (implementációt) egy JMS facade réteg takarja el. Ez azért kell, mert maga az Artemis broker nem beszél JMS-ül (nem tud semmit arról, hogy mi az), sőt abszolút protokoll agnosztikus módon van megírva a rendszer (legalábbis a core). Amikor a felhasználó (kliens) a JMS API-t használja, akkor ezek az utasítások mind átfordítódnak Artemis core hívásokra még azelőtt, hogy a hálózaton bármilyen üzenet elküldésre kerülne. Maga az Artemis broker mindig kizárólagosan a core API hívásokat használj.

Artemis arch

Mivel az Artemis egyszerű POJO-k mentén íródott, így lehetőség van azokat embedded módon is használni, azaz beépítjük a broker-t a saját alkalmazásunkba.

AMQP

Az Advanced Message Queuing Protocol (AMQP) egy nyílt alkalmazás szintű hálózati protokoll az üzenet alapú middleware-ekhez. Az AMQP protokoll használata biztosítja a különböző vendor-ok közötti átjárhatóságot (interoperable) hasonlóan, ahogy a HTTP, STMP, FTP is ezt teszi. Ahogy azt láttuk, korábban magukat az API-kat standardizálták (JMS). A JMS-el ellentétben az AMQP egy úgynevezett wire-level protocol, mely az adatok formátumát írja le, melyeket a hálózaton továbbítani szeretnénk byte adatfolyamként. Ennek következménye, hogy bármely eszköz (alkalmazás), mely képes a protocol által megszabott formátumú adatot előállítani illetve értelmezni (kiolvasni), az képes bármilyen másik AMQP-kompatibilis rendszerrel együttdolgozni, attól függetlenül, hogy az adott rendszer milyen programozási nyelven készült.

Az AMQP protocol két fő verziójú specifikációja párhuzamosan létezik/használt valahogy úgy mint a Python 2.x és a Python 3.x voltak sokáig (Python 2 maintain már megszűnt).

Ezek alapján megkülönböztetünk:

  • 1.0 előtti AMQP
    • JORAM
    • Apache Qpid
    • StormMQ
    • RabbitMQ
  • 1.0 és a fölötti AMQP protokollt
    • Apache Qpid
    • Apache ActiveMQ (Artemis)
    • Azure Event Hubs
    • Azure Service Bus
    • Solace PubSub+

A következőkben az AMQP 0-9-1-es specifikációt vizsgáljuk meg részletesebben, majd kitértünk az 1.0-ás verzió eltéréseire is.

AMQP 0-9-1

A protokoll előírása szerint a broker-hez beérkező üzeneteket egy úgynevezett exchange-ben tárolja el, melyet egy postahivatalhoz szokás hasonlítani. Ebből az exchange-ből aztán az üzenetek másolatai átkerülnek a különböző queue-kba. Azt, hogy egy üzenet melyik queue-ba kerül szabályok segítségével adjuk meg, mely szabályokat binding-oknak nevezünk. A különböző queue-kból aztán a broker vagy továbbítja a feliratkozók számára az üzeneteket (push alapú) vagy a kliensek (consumer-ek) igény szerint vesznek ki üzenetet a queue-ből (fetch/pull alapú).

amqp

Egy üzenet küldésekor az üzenetnek különböző attribútumait is beállíthatjuk (meta adat). Ezeket a meta adatokat a broker aztán felhasználhatja, ha akarja, de ezek az adatok eljutnak majd végső soron az üzenet fogadójához is.

Mivel a hálózatok hibára hajlamosak, így az AMQP 0-9-1-ben is helyet kaptak az üzenetek beérkezését nyugtázó válaszok, azaz az acknowledgment-ek. Amikor egy fogadó megkap egy üzenetet, akkor értesíti a broker-t, hogy sikeresen eljutott hozzá az üzenet. Ez a nyugtázás lehet automatikus vagy a fejlesztő által megválasztott időpontban is. A broker csak azután veszi ki az üzenetet a megfelelő queue-ból miután az adott üzenetet (vagy üzeneteket) nyugtázták.

Előfordulhat olyan eset is, amikor egy üzenetet nem tud route-olni a broker, így azokat visszadobja a feladónak vagy "eldobja" (belekerülhet például egy dead letter queue-ba). Ezt a viselkedést is konfigurálhatjuk a broker-en belül.

A fent bemutatott elemek közül a queue-kat, az exchange-eket és a binding-okat közösen AMQP entitásoknak/komponenseknek szokás nevezni.

Az exchange típusa és a binding-ok együttesen meghatározzák a routing algoritmust, mely alapján az üzenetek az exchange-ből a queue-kba kerülnek. A következő leggyakoribb exchange típusok léteznek:

  • Direct exchange
  • Fanout exchange
  • Topic exchange
  • Headers exchange

Ezen felül az exchange-eknek lehetnek további attribútumai:

  • Name
  • Durability (az exchange túléli-e a broker restart-ot, pl.: hiba esetén)
  • Auto-delete (az exchange automatikusan törlésre kerül, ha az utolsó hozzácsatolt queue is "leválik")
  • Argumentumok (opcionális, plugin-ok és broker specifikus feature-ök használhatják)

A durability alapján megkülönböztetünk durable illetve transient exchange-eket, attól függően hogy túlélik-e a broker restart-ot vagy sem.

Most vizsgáljuk meg a különböző exchange típusokat!

Default Exchange: Egy speciális, a broker által előre létrehozott direct exchange, melynek nincs neve. Az egyszerű alkalmazásokban használhatjuk ezt az exchange típust. Különleges tulajdonsága, hogy az összes létrehozott queue hozzá lesz rendelve ehhez az exchange-hez mégpedig a queue neve lesz a routing key, mely meghatározza, hogy melyik queue-hoz kell továbbküldeni az üzenetet. A default exchange használatával, olyan hatást érhetünk el, mintha az üzeneteket a megadott nevű queue-ba közvetlenül küldenénk (a háttérben így is átmegy az exchange-en az üzenet).

Direct exchange: Olyan exchange, mely az üzenetben található routing-key alapján továbbítja az üzenetet a megfelelő queue-ba.

amqp direct exchange

A queue-k egy \(K_i\) kulccsal vannak az exchange-hez rendelve (binding). Amikor egy új üzenet érkezik \(R\) routing key-el, akkor az üzenet továbbításra kerül az adott queue-ba, akkor és csak akkor, ha \(K_i = R\). A routing key tekinthető egyfajta filter-nek is.

A direct exchange-et általában akkor használjuk, ha több worker között akarjuk a taskokat elosztani (pl.: ugyanabból az alkalmazásból több instance is fut egyszerre). A szétosztás Round-Robin módon kerül megvalósításra (a fogadókra vonatkozólag, nem pedig magára a queue-kra).

Fan-out exchange: Ebben az esetben a routing key ignorálva lesz, mivel az exchange-hez rendelt összes queue megkapja az üzeneteket, melyek az exchange-be beérkeznek. Ha az exchange-hez \(N\) queue van hozzárendelve, akkor a beérkezett üzenet másolatát mind az \(N\) queue-hoz továbbítja a rendszer. Ebből látszik is, hogy broadcast-hoz ideális a fanout exchange. Lehetséges use-case-ek:

  • Globális események MMO játékokban (pl: leaderboard update-ek)
  • Sport események aktuális állásának továbbítása mobil kliensek felé
  • Elosztott rendszerekben állapotok és konfigurációk broadcast-olása
  • Csoport alapú chat alkalmazásokban az üzeneteket a résztvevők között terítjük (XMPP amúgy jobb lehet)

amqpfanout exchange

Topic exchange: Routing key és a queue-hoz megadott pattern alapján továbbítja az üzeneteket 1 vagy több queue-hoz. Publish-subscribe modellek megvalósítására használatos (multicast routing). Use-case-ek:

  • Geolokációs adatok küldése több helyre (pl.: points of sale)
  • Háttérfolyamatok több workerrel, ahol az összes worker valamilyen specifikus feladat ellátására képes
  • Részvény árfolyam frissítés (vagy bármilyen pénzügyi adat frissítése)
  • Hír frissítések, melyben vannak kategóriák/tagek
  • Build-elések különböző platformokra

Header exchange: Az üzenet header attribútumai alapján végzi a routing-ot, nem pedig a routing key alapján. A match-elés egyszerre több attribútumtól is függhet, mely alapján a fejlesztőnek azt is meg lehet adnia, hogy bármelyik attribútum teljesülése elégséges vagy az összes attribútumnak kell teljesülnie. Ehhez a header x-match attribútumát kell any-vel vagy all-al megadni. A header exchange-el tudjuk szimulálni a direct exchange-et, viszont ebben az esetben nincs megkötésünk arra vonatkozólag, hogy csak string-eket használjunk.

Miután megvizsgáltuk a különböző exchange típusokat, láthattuk, hogy az excahnge-ek milyen property-kkel rendelkeznek. Egy-egy queue hasonlóképpen rendelkezhet a következőkkel:

  • Name
  • Durable (túléli-e a broker restart-ot vagy sem)
  • Exclusive (kizárólag egy kapcsolat használja és amikor a kapcsolat lezárul, akkor a queue is megszűnik létezni)
  • Auto-delete (amikor az utolsó fogadó is leiratkozik a queue-ról, akkor a queue automatikusan törlésre kerül)
  • Arguments (opcionális, plugin-ok és broker-specifikus feature-ök használják, mint például TTL, queue hossz limit, stb.)

Mielőtt egy queue-t használhatnánk azt deklarálni kell, mely létrehozza a queue-t, ha az előtte még nem létezett (nincs hatása, ha a queue már előtte is létezett és a megadott attribútumai megegyeznek). Amennyiben már létezik ilyen queue, de más attribútumokkal, akkor egy kivételt kapunk.

A queue-k neve egy 255 byte hosszú UTF-8 kódolású név lehet, mely nevet adhatja az alkalmazásunk vagy megkérhetjük a broker-t is, hogy generáljon egy ilyen nevet, mely biztosan egyedi lesz (ilyenkor egyszerűen üresen kell hagyni a queue nevét). A generált queue nevét a queue deklarációra adott válaszban küldi el a broker. Egy megkötés van a nevekkel kapcsolatban: az amq. kezdetű nevek foglaltak a broker által (belső használatra), így ilyen nevekkel ne is próbálkozzunk.

A queue-kon és exchange-eken kívül az üzeneteknek is lehetnek attribútumai, melyek közül néhány:

  • Content type
  • Content encoding
  • Routing key
  • Delivery mode (perzisztens vagy sem)
  • Message priority
  • Message publishing timestamp
  • Expiration period
  • Publisher application id

Az üzenetek attribútumai az üzenet küldésekor állítódnak be. A legtöbb attribútumot a fogadó kliensek használhatják fel, de akadnak olyanok is, melyeket a broker használ fel. Az opcionális attribútumokat header-nek nevezzük, melyek hasonlóak a HTTP X-Header-ökhöz.

Az üzenet másik része a payload, mely maga az adat byte tömbként tárolva és továbbítva. A broker-ek a payload-ot nem vizsgálják meg és nem módosítják azt. Előfordulhat olyan eset, amikor egy üzenet csak header-t tartalmaz payload-ot viszont nem.

Az AMQP 0-9-1-ben a kapcsolatok tipikusan hosszú időtartamúak. Az AMQP a TCP-t használja, mely kapcsolatokat TLS-el tehetünk biztonságossá. Vannak azonban olyan alkalmazások, melyek több kapcsolatot is szeretnének nyitni a broker felé, ugyanakkor ez pazarolja az erőforrásokat, illetve megnehezíti a tűzfal beállításokat. Ezért az AMQP protokollban egy kapcsolathoz több ún. channel tartozhat, melyeken keresztül zajlik a kommunikáció (ugyanazt a TCP kapcsolatot osztják meg végső soron). Minden kliens által végzett operáció channel-eken keresztül hajtódik végre. A különböző channel-ek egymástól teljes mértékben szeparáltak. Mivel több channel is létezhet egyszerre, így az elküldött operációknak tartalmaznia kell a channel egyedi azonosítóját is (melyet channel number-nek is szokás hívni, mivel egy egész számmal adják meg). Amikor egy kapcsolatot lezárunk, akkor a hozzá tartozó összes channel is lezárásra kerül.

Egy broker több független környezetet is biztosíthat egyszerre (hasonlóan, mint például egy Apache Web Server), így a különböző típusú kliensek a megfelelő környezetet használhatják. Ezeket a környezeteket ebben a domain-ben is virtual host-oknak hívják.

Az AMQP 0-9-1-es szabvány egyik széles körben alkalmazott megvalósítását a RabbitMQ jelenti, mely egy open-source implementáció.

AMQP 1.0

Az AMQP 1.0 az elődjéhez képest egy teljesen más módon írja le a protokollt, illetve az is kijelenthető, hogy a két szabvány nem kompatibilis.

A legfőbb eltérés, hogy az 1.0-ás szabvány teljes egészében kihagyja a protokoll specifikációjából a broker belső felépítését, azaz nincs szó benne arról, hogy mi az az exchange, így nincsenek exchange típusok sem, ahogy nem létezik az a fogalom sem, hogy queue.

Maga az AMQP 1.0 úgy fogalmaz, hogy az 1.0-ás protokoll a következőket kell biztosítsa:

  • biztonságos
  • kompakt
  • szimmetrikus
  • multiplexelt
  • megbízható
  • bináris átviteli protokoll

, amely üzenetek mozgatását teszi lehetővé alkalmazások között. Maga a specifikáció az ISO/IEC 19464:2014-es szabvány része.

A protokoll a következő rétegekbe szerveződik:

  • Transport and connection security protokoll
  • Frame transfer protokoll
  • Message transfer protokoll

Ezen felül részletesebben nem foglalkozunk az AMQP 1.0 szabvánnyal. Az érdeklődők megtekinthetik a teljes specifikációs dokumentumot itt. További remek bevezető videóanyagot készített a Microsoft is a témában, mely lejátszási lista elérhető itt.

Kapcsolódó referenciák


Utolsó frissítés: 2020-11-18 12:05:12