Szerkesztés

Share via


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 egymástól függetlenül dolgozó csapatok tervezték, az API-knak jól meghatározott 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 hívnak meg.
  • A szolgáltatások közötti 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, jellemzően böngészőalkalmazásokkal vagy natív mobilalkalmazásokkal. Ez általában azt jelenti, hogy a nyilvános API HTTP-en keresztül fogja használni a REST-et. 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ások közötti kommunikáció nagy 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.

Megfontolandó szempontok

Íme néhány dolog, amit át kell gondolni az API implementálásának kiválasztásakor.

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 kifejezhetik a tartománymodellt. A HTTP-parancsok alapján egységes felületet határoz meg, amely elősegíti az evolvitást. Jól meghatározott szemantikával rendelkezik az idempotencia, a mellékhatások és a válaszkódok tekintetében. Emellett állapot nélküli kommunikációt kényszerít ki, ami javítja a méretezhetőséget.

  • Az RPC inkább a műveletekre vagy parancsokra összpontosít. Mivel az RPC-felületek úgy néznek ki, mint a helyi metódushívások, előfordulhat, hogy túlságosan beszédes API-kat tervez. Ez azonban nem jelenti azt, hogy az RPC-nek beszédesnek 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 tekintetében. A gRPC-alapú felület általában gyorsabb, mint a REST HTTP-n keresztül.

Felületdefiní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 IDL-eket api-tesztelési eszközök, például a Postman is használhatjá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 elveszíti a kódlétrehozás és -tesztelés előnyeit.

Szerializálás. Hogyan szerializálják az objektumokat a vezetéken keresztül? 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övegalapú 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ást. Egyes szerializálási formátumokhoz rögzített sémára van szükség, mások pedig sémadefiníciós fájl fordítására. 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 végrehajthatják ezt a funkciót. Ha szolgáltatáshálót használ, vegye figyelembe, 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 használatához.

Az alapkonfigurációnk az, hogy a REST-et HTTP-en keresztül válassza, hacsak nincs szüksége a bináris protokoll teljesítménybeli előnyeire. A HTTP-n keresztüli REST használatához nincs szükség speciális kódtárakra. Minimális összekapcsolást hoz létre, mivel a hívóknak nincs szükségük ügyfélcsonkra a szolgáltatással való kommunikációhoz. A RESTful HTTP-végpontok sémadefinícióinak, tesztelésének és monitorozásának támogatására számos eszköz áll rendelkezésre. 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 végezze el a teljesítmény- és terheléstesztelést annak ellenőrzéséhez, hogy az elég jól 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:

Íme néhány megfontolandó szempont.

  • Figyelje meg azokat az API-kat, amelyek kiszivárogtatják a belső implementáció részleteit, vagy egyszerűen tükröznek egy belső adatbázissémát. Az API-nak modellezheti a tartományt. Ez egy szolgáltatások közötti szerződés, é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érrendszert hozzon létre, amely optimális felületet tesz elérhetővé 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 biztonságos újrapróbálkozásokat tesz lehetővé, é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 Request-Reply minta.

REST leképezése DDD-mintákhoz

Az olyan minták, mint az entitások, az összesítések és az értékobjektumok úgy vannak kialakítva, hogy bizonyos korlátozásokat alkalmazzanak az objektumokra a tartományi modellben. A DDD számos vitafórumában a minták objektumorientált (OO) nyelvi fogalmak, például konstruktorok vagy tulajdonság getterek és setterek használatával modellezhetők. Az értékobjektumoknak például nem módosíthatónak 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 minta, amely biztosítja, hogy az alkalmazás más részei ne tegyenek közvetlen olvasást vagy írást az adattárba:

Drónadattár diagramja.

A mikroszolgáltatási architektúrákban 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. Vegyük azt az esetet, amikor a Scheduler szolgáltatás adatokat 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 az ütemező ezt nem látja. Ehelyett a drónentitás egy reprezentációját kapja vissza – 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 Drone szolgáltatás belső modelljeit, és nem tud írni a Drone szolgáltatás adattárában. Ez azt jelenti, hogy a drónszolgáltatást megvalósító kód kisebb felülettel rendelkezik, mint a hagyományos monolitban lévő kód. 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 foglalkozik sokat a kódolási gyakorlatokkal, mivel ezek a taktikai DDD-mintákhoz kapcsolódnak. Kiderült azonban, hogy a DDD-minták nagy részét REST API-k használatával is modellezheti.

Például:

  • Az összesítések természetesen a REST-ben lévő erőforrásokra vannak leképítve. A Kézbesítési összesítést például a Delivery API erőforrásként teszi közzé.

  • Az összesítések konzisztenciahatárok. Az összesítéseken végzett műveleteknek soha nem szabad inkonzisztens állapotban hagyniuk az összesítést. Ezért kerülje az olyan API-k létrehozását, amelyek lehetővé teszik az ügyfél számára az összesítés belső állapotának módosítását. Ehelyett inkább a durva szemű API-kat részesítse előnyben, amelyek erőforrásokként teszik közzé az összesítéseket.

  • Az entitások egyedi identitásokkal rendelkeznek. A REST-ben az erőforrások egyedi azonosítókkal rendelkeznek URL-címek 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ányi identitásra való leképezés átlátszatlan az ügyfél felé.

  • Az összesítés gyermekentitásai a gyökérentitásból való navigálással érhetőek el. Ha követi a HATEOAS-alapelveket , a gyermekentitások a szülőentitást ábrázoló hivatkozásokon keresztül érhetők el.

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

  • 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 a mögöttes adattár részleteinek absztrakciójával. A REST-ben a gyűjtemény egy különálló erőforrás lehet, amely metódusokkal kérdezi le 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
Gyermekentitá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 ügyfelek vagy a szolgáltatás fogyasztó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 megszakadnak, függetlenül attól, hogy külső ügyfelekről vagy más mikroszolgáltatásokról van-e szó. Ezért érdemes minimalizálni a végrehajtott API-módosítások számát. A mögöttes 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 meg a válaszban. A szolgáltatásnak azonban kezelnie kell azt az esetet, amikor egy régebbi ügyfél kihagyja egy kérés új mezőjét.

Az API-szerződés verziószámozásának támogatása. Ha kompatibilitástörő API-módosítást vezet be, hozzon létre egy új API-verziót. Folytassa az előző verzió támogatásával, é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 elérhetővé tegye ugyanabban a szolgáltatásban. 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 a 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 egyaránt 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 több verzió támogatása a fejlesztői idő, a tesztelés és az üzemeltetési többletterhelés szempontjából költségekkel jár. Ezért jó, ha a lehető leggyorsabban elavulnak a régi verziók. Belső API-k esetén 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 folyamatot kell végrehajtani. Külső (nyilvános) API-k esetében nehezebb lehet az API-verziót elavultizálni, különösen akkor, ha az API-t külső felek vagy natív ügyfélalkalmazások használják.

Amikor egy szolgáltatás implementációja megváltozik, hasznos lehet 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. Fontolja meg a szemantikai verziószámozás használatát a szolgáltatásverziókhoz. A szemantikai verziószámozás főverziót használ. KISEBB. PATCH formátum. Az ügyfeleknek azonban csak a főverziószám, esetleg az alverzió alapján kell kiválasztaniuk egy API-t, ha az alverziók között jelentős (de nem kompatibilitástörő) változások történnek. 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égi szintet, azzal a kockázattal jár, hogy támogatnia kell a verziók elterjedését.

Az API-verziószámozással kapcsolatos további információkért lásd: RESTful webes API verziószámozása.

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 eredményez. 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 minősül "idempotensnek", ha az adott módszerrel több azonos kérés kiszolgálójára gyakorolt tervezett hatás megegyezik egyetlen ilyen kérés 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: . Ezt az URI-t adja vissza a rendszer a válasz Hely fejlécében. 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 ilyen URI-val rendelkező entitás, a kiszolgáló lecseréli a meglévő entitást a kérelemben szereplő verzióra. Ha az URI-val nem létezik entitás, 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 ezzel 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 ú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

Tudnivalók az API-átjárók ügyfélalkalmazások és mikroszolgáltatások közötti határán történő használatáról.