Szerkesztés

Megosztás a következőn keresztül:


API-k tervezése mikroszolgáltatásokhoz

Azure DevOps

A jó API-tervezés fontos a mikroszolgáltatás-architektúrában, mivel a szolgáltatások közötti összes adatcsere üzeneteken vagy API-hívásokon keresztül történik. Az API-knak hatékonynak kell lenniük a csevegéses I/O-k létrehozásának elkerülése érdekében. Mivel a szolgáltatásokat önállóan dolgozó csapatok tervezték, az API-knak jól definiált szemantikával és verziószámozási sémákkal kell rendelkezniük, hogy a frissítések ne szeghessenek meg más szolgáltatásokat.

API-tervezés mikroszolgáltatásokhoz

Fontos különbséget tenni két API-típus között:

  • Nyilvános API-k, amelyeket az ügyfélalkalmazások meghívnak.
  • A szolgáltatásközi kommunikációhoz használt háttérbeli API-k.

Ez a két használati eset némileg eltérő követelményekkel rendelkezik. A nyilvános API-knak kompatibilisnek kell lenniük az ügyfélalkalmazásokkal, általában böngészőalkalmazásokkal vagy natív mobilalkalmazásokkal. Ez legtöbbször azt jelenti, hogy a nyilvános API REST-et fog használni HTTP-en keresztül. A háttérbeli API-k esetében azonban figyelembe kell vennie a hálózati teljesítményt. A szolgáltatások részletességétől függően a szolgáltatásközi kommunikáció sok hálózati forgalmat eredményezhet. A szolgáltatások gyorsan I/O-kötéssé válhatnak. Emiatt fontosabbá válnak az olyan szempontok, mint a szerializálás sebessége és a hasznos adatok mérete. A REST HTTP-n keresztüli használatának népszerű alternatívái közé tartozik a gRPC, az Apache Avro és az Apache Thrift. Ezek a protokollok támogatják a bináris szerializálást, és általában hatékonyabbak, mint a HTTP.

Megfontolások

Az API-k implementálásának kiválasztásakor az alábbiakban néhány dolgot érdemes átgondolni.

REST és RPC. Fontolja meg a REST-stílusú felület és az RPC-stílusú felület közötti kompromisszumot.

  • A REST modellek erőforrásai, amelyek természetes módon fejezhetik ki a tartománymodellt. A HTTP-igéken alapuló egységes felületet határoz meg, amely ösztönzi az evolvitást. Jól definiált szemantikával rendelkezik az idempotencia, a mellékhatások és a válaszkódok szempontjából. Az állapot nélküli kommunikációt pedig kényszeríti ki, ami javítja a méretezhetőséget.

  • Az RPC inkább a műveletek vagy parancsok köré van irányítva. Mivel az RPC-felületek úgy néznek ki, mint a helyi metódushívások, előfordulhat, hogy túlságosan csevegős API-kat tervez. Ez azonban nem jelenti azt, hogy az RPC-nek csevegősnek kell lennie. Ez csak azt jelenti, hogy körültekintően kell megterveznie a felületet.

A RESTful felület esetében a leggyakoribb választás a REST http-en keresztül, JSON használatával. Az RPC-stílusú felülethez számos népszerű keretrendszer tartozik, például a gRPC, az Apache Avro és az Apache Thrift.

Hatékonyság. Fontolja meg a hatékonyságot a sebesség, a memória és a hasznos adatok mérete szempontjából. A gRPC-alapú felület általában gyorsabb, mint a REST HTTP-n keresztül.

Interfészdefiníciós nyelv (IDL). Az IDL egy API metódusainak, paramétereinek és visszatérési értékeinek meghatározására szolgál. Az IDL használatával ügyfélkódot, szerializációs kódot és API-dokumentációt hozhat létre. AZ AZONOSÍTÓK API-tesztelési eszközökkel is használhatók. Az olyan keretrendszerek, mint a gRPC, az Avro és a Thrift, saját IDL-specifikációkat határoznak meg. A HTTP-n keresztüli REST nem rendelkezik szabványos IDL-formátummal, de gyakori választás az OpenAPI (korábbi nevén Swagger). A HTTP REST API-t formális definíciós nyelv használata nélkül is létrehozhatja, de így elveszíti a kódgenerálás és a tesztelés előnyeit.

Szerializálás. Hogyan vannak szerializálva az objektumok a vezetéken? A lehetőségek közé tartoznak a szövegalapú formátumok (elsősorban JSON) és a bináris formátumok, például a protokollpuffer. A bináris formátumok általában gyorsabbak, mint a szöveges formátumok. A JSON azonban az interoperabilitás szempontjából előnyös, mivel a legtöbb nyelv és keretrendszer támogatja a JSON szerializálását. Egyes szerializálási formátumokhoz rögzített séma szükséges, mások pedig sémadefiníciós fájl összeállítását igénylik. Ebben az esetben ezt a lépést be kell építenie a buildelési folyamatba.

Keretrendszer és nyelvi támogatás. A HTTP szinte minden keretrendszerben és nyelven támogatott. A gRPC, az Avro és a Thrift mind rendelkezik C++, C#, Java és Python kódtárak használatával. A Thrift és a gRPC a Go-t is támogatja.

Kompatibilitás és együttműködés. Ha olyan protokollt választ, mint a gRPC, szükség lehet egy protokollfordítási rétegre a nyilvános API és a háttér között. Az átjáró képes elvégezni ezt a funkciót. Ha szolgáltatáshálót használ, fontolja meg, hogy mely protokollok kompatibilisek a szolgáltatáshálóval. A Linkerd például beépített támogatást nyújt a HTTP, a Thrift és a gRPC számára.

Az alapkonfigurációnk a REST http-en keresztüli kiválasztása, hacsak nincs szüksége a bináris protokoll teljesítménybeli előnyeire. A HTTP-n keresztüli REST nem igényel speciális kódtárakat. Minimális összekapcsolást hoz létre, mivel a hívóknak nincs szükségük ügyfélcsomópontra a szolgáltatással való kommunikációhoz. A RESTful HTTP-végpontok sémadefinícióit, tesztelését és monitorozását támogató eszközök gazdag ökoszisztémái találhatók. Végül a HTTP kompatibilis a böngészőügyfelekkel, így nincs szükség protokollfordítási rétegre az ügyfél és a háttér között.

Ha azonban a REST-et HTTP-en keresztül választja, a fejlesztési folyamat korai szakaszában teljesítmény- és terheléstesztelést kell végeznie annak ellenőrzéséhez, hogy az megfelelően teljesít-e a forgatókönyvhöz.

RESTful API-tervezés

A RESTful API-k tervezéséhez számos erőforrás áll rendelkezésre. Az alábbiakban hasznosnak talál néhányat:

Az alábbiakban néhány konkrét szempontot érdemes szem előtt tartani.

  • Figyelje meg azokat az API-kat, amelyek belső megvalósítási részleteket szivárogtatnak ki, vagy egyszerűen csak tükröznek egy belső adatbázissémát. Az API-nak modellezheti a tartományt. Ez egy szerződés a szolgáltatások között, és ideális esetben csak új funkciók hozzáadásakor kell megváltoznia, nem csak azért, mert újrabontást végzett egy kódon, vagy normalizált egy adatbázistáblát.

  • A különböző ügyféltípusok, például a mobilalkalmazás és az asztali webböngésző eltérő hasznos adatméretet vagy interakciós mintákat igényelhetnek. Fontolja meg a Háttérrendszer for Frontends minta használatát, hogy minden ügyfélhez külön háttérrendszereket hozzon létre, amelyek optimális felületet fednek le az adott ügyfél számára.

  • A mellékhatásokkal járó műveletek esetében érdemes idempotenssé tenni őket, és PUT metódusként implementálni őket. Ez lehetővé teszi a biztonságos újrapróbálkozásokat, és javíthatja a rugalmasságot. A szolgáltatásközi kommunikáció című cikk részletesebben ismerteti ezt a problémát.

  • A HTTP-metódusok aszinkron szemantikával rendelkezhetnek, ahol a metódus azonnal választ ad vissza, de a szolgáltatás aszinkron módon hajtja végre a műveletet. Ebben az esetben a metódusnak egy HTTP 202-válaszkódot kell visszaadnia, amely azt jelzi, hogy a kérést elfogadták feldolgozásra, de a feldolgozás még nem fejeződött be. További információ: Aszinkron kérés-válasz minta.

REST leképezése DDD-mintákhoz

Az olyan minták, mint az entitások, az összesítés és az értékobjektumok úgy vannak kialakítva, hogy bizonyos korlátozásokat helyezzenek el a tartománymodellben lévő objektumokra. A DDD számos vitafórumában a minták objektumorientált (OO) nyelvi fogalmak, például konstruktorok vagy tulajdonságválasztók és -beállítók használatával modellezhetők. Az értékobjektumoknak például nem módosíthatóknak kell lenniük . Az OO programozási nyelvben ezt a konstruktor értékeinek hozzárendelésével és a tulajdonságok írásvédetté tételével kényszerítheti ki:

export class Location {
    readonly latitude: number;
    readonly longitude: number;

    constructor(latitude: number, longitude: number) {
        if (latitude < -90 || latitude > 90) {
            throw new RangeError('latitude must be between -90 and 90');
        }
        if (longitude < -180 || longitude > 180) {
            throw new RangeError('longitude must be between -180 and 180');
        }
        this.latitude = latitude;
        this.longitude = longitude;
    }
}

Az ilyen kódolási eljárások különösen fontosak egy hagyományos monolitikus alkalmazás létrehozásakor. Nagy kódbázis esetén számos alrendszer használhatja az Location objektumot, ezért fontos, hogy az objektum helyes viselkedést kényszerítsen ki.

Egy másik példa az adattár mintája, amely biztosítja, hogy az alkalmazás más részei ne végezhessenek közvetlen olvasást vagy írást az adattárba:

Drónadattár diagramja.

A mikroszolgáltatás-architektúrában azonban a szolgáltatások nem ugyanazt a kódbázist használják, és nem osztják meg az adattárakat. Ehelyett api-kon keresztül kommunikálnak. Fontolja meg azt az esetet, amikor a Scheduler szolgáltatás információt kér egy drónról a Drone szolgáltatástól. A Drone szolgáltatás egy drón belső modelljével rendelkezik, kóddal kifejezve. De a Ütemező nem látja. Ehelyett visszaadja a drón entitásának egy reprezentációját – talán egy JSON-objektumot egy HTTP-válaszban.

Ez a példa ideális a repülőgép- és repülőgépipar számára.

A Drone szolgáltatás ábrája.

A Scheduler szolgáltatás nem tudja módosítani a Drónszolgáltatás belső modelljeit, és nem tud írni a Drone szolgáltatás adattárában. Ez azt jelenti, hogy a Drone szolgáltatást megvalósító kódnak kisebb a felszíne, mint egy hagyományos monolit kódjának. Ha a Drone szolgáltatás helyosztályt határoz meg, az osztály hatóköre korlátozott – más szolgáltatás nem fogja közvetlenül használni az osztályt.

Ezen okok miatt ez az útmutató nem összpontosít sokat a kódolási gyakorlatokra, mivel azok a taktikai DDD-mintákhoz kapcsolódnak. De kiderült, hogy a DDD-minták nagy részét REST API-k használatával is modellezheti.

Példa:

  • Az összesítés természetesen megfelelteti a REST-erőforrásokat. A Kézbesítési aggregátum például erőforrásként lesz közzétéve a Delivery API-nak.

  • Az aggregátumok konzisztenciahatárok. Az aggregátumokon végzett műveleteknek soha nem szabad inkonzisztens állapotban hagyniuk az összesítést. Ezért ne hozzon létre olyan API-kat, amelyek lehetővé teszik az ügyfél számára az összesítés belső állapotának manipulálását. Ehelyett előnyben részesítse a durva szemcsés API-kat, amelyek erőforrásokként teszik elérhetővé az aggregátumokat.

  • Az entitások egyedi identitásokkal rendelkeznek. A REST-ben az erőforrások egyedi azonosítókkal rendelkeznek URL-ek formájában. Hozzon létre egy entitás tartományi identitásának megfelelő erőforrás-URL-címeket. Előfordulhat, hogy az URL-címről a tartomány identitására való leképezés átlátszatlan az ügyfél számára.

  • Az aggregátum gyermek entitásai a legfelső szintű entitásból való navigálással érhetőek el. Ha a HATEOAS-alapelveket követi, a gyermek entitások a szülő entitás ábrázolása hivatkozásokkal érhetők el.

  • Mivel az értékobjektumok nem módosíthatók, a frissítések a teljes értékobjektum cseréjével hajthatók végre. A REST-ben a frissítéseket PUT vagy PATCH kérésekkel implementálhatja.

  • Az adattár lehetővé teszi, hogy az ügyfelek lekérdezik, hozzáadják vagy eltávolítsák a gyűjteményben lévő objektumokat, és absztrakcióval kivonatolják az alapul szolgáló adattár részleteit. A REST-ben a gyűjtemény lehet különálló erőforrás, amely metódusokkal lekérdezi a gyűjteményt, vagy új entitásokat ad hozzá a gyűjteményhez.

Az API-k tervezésekor gondolja át, hogyan fejezik ki a tartománymodellt, nem csak a modellen belüli adatokat, hanem az üzleti műveleteket és az adatokra vonatkozó korlátozásokat is.

DDD-koncepció REST-ekvivalens Példa
Összesítés Erőforrás { "1":1234, "status":"pending"... }
Identitás URL-cím https://delivery-service/deliveries/1
Gyermek entitások Hivatkozások { "href": "/deliveries/1/confirmation" }
Értékobjektumok frissítése PUT vagy PATCH PUT https://delivery-service/deliveries/1/dropoff
Adattár Gyűjtemény https://delivery-service/deliveries?status=pending

API-verziószámozás

Az API egy szolgáltatás és az adott szolgáltatás ügyfelei vagy felhasználói közötti szerződés. Ha egy API megváltozik, fennáll annak a kockázata, hogy az API-tól függő ügyfelek feltörése történik, függetlenül attól, hogy ezek külső ügyfelek vagy más mikroszolgáltatások. Ezért érdemes minimalizálni a végrehajtott API-módosítások számát. Az alapul szolgáló implementáció változásai gyakran nem igényelnek módosításokat az API-ban. Reálisan azonban egy bizonyos ponton új funkciókat vagy új képességeket szeretne hozzáadni, amelyek egy meglévő API módosítását igénylik.

Amikor csak lehetséges, végezze el az API-módosítások visszamenőleges kompatibilitását. Kerülje például egy mező eltávolítását egy modellből, mert ez megszakíthatja azokat az ügyfeleket, amelyek elvárják, hogy a mező ott legyen. A mezők hozzáadása nem szakítja meg a kompatibilitást, mert az ügyfeleknek figyelmen kívül kell hagyniuk azokat a mezőket, amelyeket nem értenek a válaszban. A szolgáltatásnak azonban kezelnie kell azt az esetet, amikor egy régebbi ügyfél kihagyja a kérés új mezőjét.

Az API-szerződés verziószámozásának támogatása. Ha egy kompatibilitástörő API-módosítást vezet be, egy új API-verziót vezet be. Továbbra is támogassa az előző verziót, és hagyja, hogy az ügyfelek kiválasztják a meghívandó verziót. Ezt többféleképpen is megteheti. Az egyik egyszerűen az, hogy mindkét verziót ugyanabban a szolgáltatásban tegye közzé. Egy másik lehetőség a szolgáltatás két verziójának egymás melletti futtatása, és a kérések átirányítása az egyik vagy a másik verzióra HTTP-útválasztási szabályok alapján.

A verziószámozás támogatásának két lehetőségét bemutató ábra.

A diagram két részből áll. A "Szolgáltatás két verziót támogat" a v1-ügyfél és a v2-ügyfél egy szolgáltatásra mutat. A "Párhuzamos üzembe helyezés" azt mutatja, hogy a v1-ügyfél egy v1-szolgáltatásra mutat, a v2-ügyfél pedig egy v2-szolgáltatásra mutat.

A fejlesztői idő, a tesztelés és a működési költségek szempontjából több verzió támogatása is költséges. Ezért érdemes a régi verziókat a lehető leggyorsabban elavulni. A belső API-k esetében az API-t birtokló csapat együttműködhet más csapatokkal, hogy segítsen nekik az új verzióra való migrálásban. Ez akkor hasznos, ha csapatközi szabályozási folyamat van. Külső (nyilvános) API-k esetében nehezebb lehet az API-verziót elavultni, különösen akkor, ha az API-t harmadik felek vagy natív ügyfélalkalmazások használják.

Amikor egy szolgáltatás implementációja megváltozik, érdemes a módosítást egy verzióval megjelölni. A verzió fontos információkat nyújt a hibák elhárításához. Nagyon hasznos lehet, ha a kiváltó okok elemzése pontosan tudja, hogy a szolgáltatás melyik verzióját hívták meg. Érdemes szemantikai verziószámozást használni a szolgáltatásverziókhoz. A szemantikus verziószámozás MAJOR-t használ. KISKORÚ. PATCH formátum. Az ügyfeleknek azonban csak a fő verziószám alapján kell kiválasztaniuk egy API-t, vagy esetleg az alverziót, ha jelentős (de nem kompatibilitástörő) változások történnek az alverziók között. Más szóval ésszerű, ha az ügyfelek az API 1. és 2. verziója között választanak, de nem a 2.1.3-at. Ha engedélyezi ezt a részletességet, azzal a kockázattal jár, hogy támogatnia kell a verziók elterjedését.

Az API-verziószámozásról további információt a RESTful webes API verziószámozása című témakörben talál.

Idempotens műveletek

A művelet idempotens , ha többször is meghívható anélkül, hogy az első hívás után további mellékhatásokat okoz. Az idempotencia hasznos rugalmassági stratégia lehet, mivel lehetővé teszi, hogy egy felsőbb rétegbeli szolgáltatás többször is biztonságosan meghívjon egy műveletet. Erről a pontról az Elosztott tranzakciók című témakörben olvashat bővebben.

A HTTP-specifikáció szerint a GET, PUT és DELETE metódusnak idempotensnek kell lennie. A POST metódusok nem garantáltan idempotensek. Ha egy POST metódus új erőforrást hoz létre, általában nincs garancia arra, hogy ez a művelet idempotens. A specifikáció így határozza meg az idempotenst:

A kérelemmetódus akkor tekinthető "idempotensnek", ha a kiszolgálóra több azonos kérelemnek az adott módszerrel való tervezett hatása megegyezik egyetlen ilyen kérelem hatásával. (RFC 7231)

Fontos megérteni a PUT és a POST szemantika közötti különbséget egy új entitás létrehozásakor. Az ügyfél mindkét esetben egy entitást küld a kérelem törzsében. Az URI jelentése azonban más.

  • POST metódus esetén az URI az új entitás szülőerőforrását jelöli, például egy gyűjteményt. Új kézbesítés létrehozásához például az URI lehet /api/deliveries. A kiszolgáló létrehozza az entitást, és egy új URI-t rendel hozzá, például /api/deliveries/39660. Ez az URI a válasz Hely fejlécében jelenik meg. Minden alkalommal, amikor az ügyfél kérést küld, a kiszolgáló létrehoz egy új entitást egy új URI-val.

  • PUT metódus esetén az URI azonosítja az entitást. Ha már létezik egy entitás ezzel az URI-val, a kiszolgáló lecseréli a meglévő entitást a kérelemben szereplő verzióra. Ha nem létezik entitás ezzel az URI-val, a kiszolgáló létrehoz egyet. Tegyük fel például, hogy az ügyfél PUT kérést küld a következőnek api/deliveries/39660: . Feltételezve, hogy az URI-val nincs kézbesítés, a kiszolgáló létrehoz egy újat. Ha az ügyfél ismét elküldi ugyanazt a kérést, a kiszolgáló lecseréli a meglévő entitást.

Íme a Kézbesítési szolgáltatás PUT metódus implementációja.

[HttpPut("{id}")]
[ProducesResponseType(typeof(Delivery), 201)]
[ProducesResponseType(typeof(void), 204)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
    logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
    try
    {
        var internalDelivery = delivery.ToInternal();

        // Create the new delivery entity.
        await deliveryRepository.CreateAsync(internalDelivery);

        // Create a delivery status event.
        var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
        await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);

        // Return HTTP 201 (Created)
        return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
    }
    catch (DuplicateResourceException)
    {
        // This method is mainly used to create deliveries. If the delivery already exists then update it.
        logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);

        var internalDelivery = delivery.ToInternal();
        await deliveryRepository.UpdateAsync(id, internalDelivery);

        // Return HTTP 204 (No Content)
        return NoContent();
    }
}

A legtöbb kérés egy új entitást fog létrehozni, ezért a metódus optimista módon meghívja CreateAsync az adattárobjektumot, majd az erőforrás frissítésével kezeli az ismétlődő erőforrás-kivételeket.

Következő lépések

Megtudhatja, hogyan használhat api-átjárót az ügyfélalkalmazások és a mikroszolgáltatások határán.