Redigera

Dela via


Utforma API:er för mikrotjänster

Azure DevOps

Bra API-design är viktigt i en arkitektur för mikrotjänster, eftersom allt datautbyte mellan tjänster sker antingen via meddelanden eller API-anrop. API:er måste vara effektiva för att undvika att skapa trafikintensiva I/O. Eftersom tjänsterna är utformade av team som arbetar oberoende av varandra måste API:er ha väldefinierade semantik- och versionsscheman, så att uppdateringar inte bryter andra tjänster.

API-design för mikrotjänster

Det är viktigt att skilja mellan två typer av API:

  • Offentliga API:er som klientprogram anropar.
  • Serverdels-API:er som används för kommunikation mellan tjänster.

Dessa två användningsfall har något olika krav. Ett offentligt API måste vara kompatibelt med klientprogram, vanligtvis webbläsarprogram eller interna mobilprogram. För det mesta innebär det att det offentliga API:et använder REST över HTTP. För serverdels-API:erna måste du dock ta hänsyn till nätverksprestanda. Beroende på tjänsternas kornighet kan kommunikation mellan tjänster resultera i mycket nätverkstrafik. Tjänster kan snabbt bli I/O-bundna. Därför blir överväganden som serialiseringshastighet och nyttolaststorlek viktigare. Några populära alternativ för att använda REST via HTTP är bland annat gRPC, Apache Avro och Apache Thrift. Dessa protokoll stöder binär serialisering och är vanligtvis effektivare än HTTP.

Överväganden

Här följer några saker att tänka på när du väljer hur du ska implementera ett API.

REST jämfört med RPC. Överväg kompromisserna mellan att använda ett REST-gränssnitt jämfört med ett RPC-gränssnitt.

  • REST modellerar resurser, vilket kan vara ett naturligt sätt att uttrycka din domänmodell. Det definierar ett enhetligt gränssnitt baserat på HTTP-verb, vilket uppmuntrar till utveckling. Det har väldefinierade semantik när det gäller idempotens, biverkningar och svarskoder. Och den framtvingar tillståndslös kommunikation, vilket förbättrar skalbarheten.

  • RPC är mer inriktat på åtgärder eller kommandon. Eftersom RPC-gränssnitt ser ut som lokala metodanrop kan det leda till att du utformar alltför trafikintensiva API:er. Det betyder dock inte att RPC måste vara trafikintensivt. Det innebär bara att du måste vara försiktig när du utformar gränssnittet.

För ett RESTful-gränssnitt är det vanligaste valet REST över HTTP med JSON. För ett RPC-gränssnitt finns det flera populära ramverk, inklusive gRPC, Apache Avro och Apache Thrift.

Effektivitet. Överväg effektivitet när det gäller hastighet, minne och nyttolaststorlek. Vanligtvis är ett gRPC-baserat gränssnitt snabbare än REST över HTTP.

Gränssnittsdefinitionsspråk (IDL). En IDL används för att definiera metoder, parametrar och returvärden för ett API. En IDL kan användas för att generera klientkod, serialiseringskod och API-dokumentation. IDL:er kan också användas av API-testverktyg som Postman. Ramverk som gRPC, Avro och Thrift definierar sina egna IDL-specifikationer. REST över HTTP har inget standard-IDL-format, men ett vanligt val är OpenAPI (tidigare Swagger). Du kan också skapa ett HTTP REST API utan att använda ett formellt definitionsspråk, men sedan förlorar du fördelarna med kodgenerering och testning.

Serialisering. Hur serialiseras objekt över kabeln? Alternativen omfattar textbaserade format (främst JSON) och binära format, till exempel protokollbuffert. Binära format är vanligtvis snabbare än textbaserade format. JSON har dock fördelar när det gäller samverkan, eftersom de flesta språk och ramverk stöder JSON-serialisering. Vissa serialiseringsformat kräver ett fast schema och vissa kräver kompilering av en schemadefinitionsfil. I så fall måste du införliva det här steget i byggprocessen.

Ramverks- och språkstöd. HTTP stöds i nästan alla ramverk och språk. gRPC, Avro och Thrift har alla bibliotek för C++, C#, Java och Python. Thrift och gRPC har också stöd för Go.

Kompatibilitet och samverkan. Om du väljer ett protokoll som gRPC kan du behöva ett protokollöversättningslager mellan det offentliga API:et och serverdelen. En gateway kan utföra den funktionen. Om du använder ett tjänstnät bör du överväga vilka protokoll som är kompatibla med tjänstnätet. Linkerd har till exempel inbyggt stöd för HTTP, Thrift och gRPC.

Vår baslinjerekommendering är att välja REST framför HTTP om du inte behöver prestandafördelarna med ett binärt protokoll. REST över HTTP kräver inga särskilda bibliotek. Den skapar minimal koppling eftersom anropare inte behöver en klientstub för att kommunicera med tjänsten. Det finns omfattande ekosystem med verktyg som stöder schemadefinitioner, testning och övervakning av RESTful HTTP-slutpunkter. Slutligen är HTTP kompatibelt med webbläsarklienter, så du behöver inget protokollöversättningslager mellan klienten och serverdelen.

Men om du väljer REST framför HTTP bör du utföra prestanda- och belastningstestning tidigt i utvecklingsprocessen för att verifiera om den presterar tillräckligt bra för ditt scenario.

RESTful API-design

Det finns många resurser för att utforma RESTful-API:er. Här är några som kan vara användbara:

Här följer några specifika överväganden att tänka på.

  • Se upp för API:er som läcker intern implementeringsinformation eller bara speglar ett internt databasschema. API:et bör modellera domänen. Det är ett kontrakt mellan tjänster och bör helst bara ändras när nya funktioner läggs till, inte bara för att du omstrukturerade kod eller normaliserade en databastabell.

  • Olika typer av klienter, till exempel mobilprogram och skrivbordswebbläsare, kan kräva olika nyttolaststorlekar eller interaktionsmönster. Överväg att använda mönstret Serverdelar för klientdelar för att skapa separata serverdelar för varje klient, som exponerar ett optimalt gränssnitt för klienten.

  • För åtgärder med biverkningar, överväg att göra dem idempotent och implementera dem som PUT-metoder. Det möjliggör säkra återförsök och kan förbättra återhämtning. Artikeln Interservice communication (Kommunikation mellan tjänster ) beskriver det här problemet mer detaljerat.

  • HTTP-metoder kan ha asynkron semantik, där metoden returnerar ett svar omedelbart, men tjänsten utför åtgärden asynkront. I så fall bör metoden returnera en HTTP 202-svarskod som anger att begäran accepterades för bearbetning, men bearbetningen har ännu inte slutförts. Mer information finns i Asynkront Request-Reply mönster.

Mappa REST till DDD-mönster

Mönster som entitets-, aggregerings- och värdeobjekt är utformade för att placera vissa begränsningar på objekten i din domänmodell. I många diskussioner om DDD modelleras mönstren med hjälp av objektorienterade språkbegrepp (OO), till exempel konstruktorer eller egenskaps getters och set-metoder. Värdeobjekt ska till exempel vara oföränderliga. I ett OO-programmeringsspråk skulle du framtvinga detta genom att tilldela värdena i konstruktorn och göra egenskaperna skrivskyddade:

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;
    }
}

Den här typen av kodningsmetoder är särskilt viktiga när du skapar ett traditionellt monolitiskt program. Med en stor kodbas kan många undersystem använda objektet Location , så det är viktigt att objektet framtvingar korrekt beteende.

Ett annat exempel är mönstret Lagringsplats, som säkerställer att andra delar av programmet inte gör direkta läsningar eller skrivningar till datalagret:

Diagram över en Drone-lagringsplats.

I en arkitektur för mikrotjänster delar tjänsterna dock inte samma kodbas och delar inte datalager. I stället kommunicerar de via API:er. Tänk på fallet där Scheduler-tjänsten begär information om en drönare från drönartjänsten. Drönartjänsten har sin interna modell av en drönare, uttryckt genom kod. Men Scheduler ser inte det. I stället får den tillbaka en representation av drönarentiteten – kanske ett JSON-objekt i ett HTTP-svar.

Det här exemplet är idealiskt för flygplans- och flygindustrin.

Diagram över drönartjänsten.

Scheduler-tjänsten kan inte ändra drönartjänstens interna modeller eller skriva till Drone-tjänstens datalager. Det innebär att koden som implementerar drönartjänsten har en mindre exponerad yta jämfört med kod i en traditionell monolit. Om drönartjänsten definierar en platsklass är omfånget för den klassen begränsat – ingen annan tjänst förbrukar klassen direkt.

Av dessa skäl fokuserar den här vägledningen inte särskilt mycket på kodningsmetoder eftersom de relaterar till de taktiska DDD-mönstren. Men det visar sig att du också kan modellera många av DDD-mönstren via REST-API:er.

Exempel:

  • Aggregat mappar naturligt till resurser i REST. Leveransaggregatet skulle till exempel exponeras som en resurs av LEVERANS-API:et.

  • Aggregat är konsekvensgränser. Åtgärder i aggregeringar bör aldrig lämna en aggregering i ett inkonsekvent tillstånd. Därför bör du undvika att skapa API:er som gör att en klient kan ändra det interna tillståndet för en aggregering. I stället föredrar du grova API:er som exponerar aggregeringar som resurser.

  • Entiteter har unika identiteter. I REST har resurser unika identifierare i form av URL:er. Skapa resurs-URL:er som motsvarar en entitets domänidentitet. Mappningen från URL till domänidentitet kan vara ogenomskinlig för klienten.

  • Underordnade entiteter av en aggregering kan nås genom att navigera från rotentiteten. Om du följer HATEOAS-principer kan underordnade entiteter nås via länkar i representationen av den överordnade entiteten.

  • Eftersom värdeobjekt inte kan ändras utförs uppdateringarna genom att hela värdeobjektet ersätts. I REST implementerar du uppdateringar via PUT- eller PATCH-begäranden.

  • Med en lagringsplats kan klienter fråga, lägga till eller ta bort objekt i en samling och abstrahera information om det underliggande datalagret. I REST kan en samling vara en distinkt resurs, med metoder för att köra frågor mot samlingen eller lägga till nya entiteter i samlingen.

När du utformar dina API:er bör du tänka på hur de uttrycker domänmodellen, inte bara data i modellen, utan även verksamheten och begränsningarna för data.

DDD-koncept REST-motsvarighet Exempel
Aggregera Resurs { "1":1234, "status":"pending"... }
Identitet URL https://delivery-service/deliveries/1
Underordnade entiteter Länkar { "href": "/deliveries/1/confirmation" }
Uppdatera värdeobjekt PUT eller PATCH PUT https://delivery-service/deliveries/1/dropoff
Lagringsplats Samling https://delivery-service/deliveries?status=pending

API-versionshantering

Ett API är ett kontrakt mellan en tjänst och klienter eller användare av den tjänsten. Om ett API ändras finns det risk för att klienter som är beroende av API:et bryts, oavsett om det är externa klienter eller andra mikrotjänster. Därför är det en bra idé att minimera antalet API-ändringar som du gör. Ändringar i den underliggande implementeringen kräver ofta inga ändringar i API:et. Realistiskt sett vill du dock någon gång lägga till nya funktioner eller nya funktioner som kräver att du ändrar ett befintligt API.

När det är möjligt gör du API-ändringar bakåtkompatibla. Undvik till exempel att ta bort ett fält från en modell, eftersom det kan bryta klienter som förväntar sig att fältet ska finnas där. Att lägga till ett fält bryter inte kompatibiliteten, eftersom klienter bör ignorera alla fält som de inte förstår i ett svar. Tjänsten måste dock hantera det fall där en äldre klient utelämnar det nya fältet i en begäran.

Stöd för versionshantering i ditt API-kontrakt. Om du introducerar en icke-bakåtkompatibel API-ändring introducerar du en ny API-version. Fortsätt att stödja den tidigare versionen och låt klienter välja vilken version som ska anropas. Det finns ett par sätt att göra detta. En är helt enkelt att exponera båda versionerna i samma tjänst. Ett annat alternativ är att köra två versioner av tjänsten sida vid sida och dirigera begäranden till den ena eller den andra versionen, baserat på HTTP-routningsregler.

Diagram som visar två alternativ för att stödja versionshantering.

Diagrammet har två delar. "Tjänsten stöder två versioner" visar v1-klienten och v2-klienten som båda pekar på en tjänst. "Sida vid sida-distribution" visar v1-klienten som pekar på en v1-tjänst och v2-klienten som pekar på en v2-tjänst.

Det finns en kostnad för att stödja flera versioner, när det gäller utvecklartid, testning och driftkostnader. Därför är det bra att föråldrade gamla versioner så snabbt som möjligt. För interna API:er kan teamet som äger API:et arbeta med andra team för att hjälpa dem att migrera till den nya versionen. Det är när en styrningsprocess mellan team är användbar. För externa (offentliga) API:er kan det vara svårare att föråldrade en API-version, särskilt om API:et används av tredje part eller av interna klientprogram.

När en tjänstimplementering ändras är det bra att tagga ändringen med en version. Versionen innehåller viktig information vid felsökning av fel. Det kan vara till stor hjälp för rotorsaksanalysen att veta exakt vilken version av tjänsten som anropades. Överväg att använda semantisk versionshantering för tjänstversioner. Semantisk versionshantering använder en MAJOR. MINDRE. PATCH-format . Klienter bör dock bara välja ett API efter huvudversionsnumret, eller eventuellt delversionen om det finns betydande (men icke-icke-bakåtkompatibla) ändringar mellan delversioner. Med andra ord är det rimligt att klienter väljer mellan version 1 och version 2 av ett API, men inte att välja version 2.1.3. Om du tillåter den granulariteten riskerar du att behöva stödja en spridning av versioner.

Mer information om API-versionshantering finns i Versionshantering av ett RESTful-webb-API.

Idempotentåtgärder

En åtgärd är idempotent om den kan anropas flera gånger utan att skapa ytterligare biverkningar efter det första anropet. Idempotens kan vara en användbar återhämtningsstrategi eftersom den gör att en överordnad tjänst kan anropa en åtgärd flera gånger på ett säkert sätt. En diskussion om den här punkten finns i Distribuerade transaktioner.

HTTP-specifikationen anger att metoderna GET, PUT och DELETE måste vara idempotent. POST-metoder är inte garanterade att vara idempotent. Om en POST-metod skapar en ny resurs finns det vanligtvis ingen garanti för att den här åtgärden är idempotent. Specifikationen definierar idempotent på följande sätt:

En begärandemetod anses vara "idempotent" om den avsedda effekten på servern för flera identiska begäranden med den metoden är densamma som för en enda sådan begäran. (RFC 7231)

Det är viktigt att förstå skillnaden mellan PUT och POST-semantik när du skapar en ny entitet. I båda fallen skickar klienten en representation av en entitet i begärandetexten. Men innebörden av URI:n är annorlunda.

  • För en POST-metod representerar URI:n en överordnad resurs för den nya entiteten, till exempel en samling. Om du till exempel vill skapa en ny leverans kan URI:n vara /api/deliveries. Servern skapar entiteten och tilldelar den en ny URI, till exempel /api/deliveries/39660. Den här URI:n returneras i platsrubriken för svaret. Varje gång klienten skickar en begäran skapar servern en ny entitet med en ny URI.

  • För en PUT-metod identifierar URI:n entiteten. Om det redan finns en entitet med den URI:n ersätter servern den befintliga entiteten med versionen i begäran. Om det inte finns någon entitet med den URI:n skapar servern en. Anta till exempel att klienten skickar en PUT-begäran till api/deliveries/39660. Förutsatt att det inte finns någon leverans med den URI:n skapar servern en ny. Om klienten nu skickar samma begäran igen ersätter servern den befintliga entiteten.

Här är leveranstjänstens implementering av PUT-metoden.

[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();
    }
}

Det förväntas att de flesta begäranden skapar en ny entitet, så metoden anropar CreateAsync optimistiskt lagringsplatsobjektet och hanterar sedan eventuella undantag för dubbletter av resurser genom att uppdatera resursen i stället.

Nästa steg

Lär dig mer om att använda en API-gateway vid gränsen mellan klientprogram och mikrotjänster.