Bewerken

Delen via


API's ontwerpen voor microservices

Azure DevOps

Een goed API-ontwerp is belangrijk in een microservicesarchitectuur, omdat alle gegevensuitwisseling tussen services plaatsvindt via berichten of API-aanroepen. API's moeten efficiënt zijn om te voorkomen dat er sprake is van chatty I/O. Omdat services zijn ontworpen door teams die onafhankelijk werken, moeten API's goed gedefinieerde semantiek- en versiebeheerschema's hebben, zodat updates geen andere services breken.

API-ontwerp voor microservices

Het is belangrijk om onderscheid te maken tussen twee typen API:

  • Openbare API's die clienttoepassingen aanroepen.
  • Back-end-API's die worden gebruikt voor communicatie tussen services.

Deze twee gebruiksvoorbeelden hebben enigszins verschillende vereisten. Een openbare API moet compatibel zijn met clienttoepassingen, meestal browsertoepassingen of systeemeigen mobiele toepassingen. Meestal betekent dit dat de openbare API REST via HTTP gebruikt. Voor de back-end-API's moet u echter rekening houden met netwerkprestaties. Afhankelijk van de granulariteit van uw services kan communicatie tussen services leiden tot veel netwerkverkeer. Services kunnen snel I/O-gebonden worden. Daarom worden overwegingen zoals serialisatiesnelheid en payloadgrootte belangrijker. Enkele populaire alternatieven voor het gebruik van REST via HTTP zijn gRPC, Apache Avro en Apache Thrift. Deze protocollen ondersteunen binaire serialisatie en zijn over het algemeen efficiënter dan HTTP.

Overwegingen

Hier volgen enkele dingen die u moet bedenken wanneer u kiest hoe u een API implementeert.

REST versus RPC. Houd rekening met de afwegingen tussen het gebruik van een REST-stijlinterface versus een RPC-stijlinterface.

  • REST-modellenresources, wat een natuurlijke manier kan zijn om uw domeinmodel uit te drukken. Het definieert een uniforme interface op basis van HTTP-werkwoorden, wat suggesties stimuleert. Het heeft goed gedefinieerde semantiek in termen van idempotentie, bijwerkingen en responscodes. En het dwingt staatloze communicatie af, waardoor de schaalbaarheid wordt verbeterd.

  • RPC is meer gericht op bewerkingen of opdrachten. Omdat RPC-interfaces eruitzien als lokale methodeaanroepen, kan dit ertoe leiden dat u te veel chat-API's ontwerpt. Dat betekent echter niet dat RPC chatty moet zijn. Het betekent alleen dat u voorzichtig moet zijn bij het ontwerpen van de interface.

Voor een RESTful-interface is REST de meest voorkomende keuze via HTTP met behulp van JSON. Voor een RPC-interface zijn er verschillende populaire frameworks, waaronder gRPC, Apache Avro en Apache Thrift.

Efficiëntie. Overweeg efficiëntie in termen van snelheid, geheugen en nettoladinggrootte. Normaal gesproken is een op gRPC gebaseerde interface sneller dan REST via HTTP.

Interface definition language (IDL). Een IDL wordt gebruikt om de methoden, parameters en retourwaarden van een API te definiëren. Een IDL kan worden gebruikt voor het genereren van clientcode, serialisatiecode en API-documentatie. ID's kunnen ook worden gebruikt door HULPPROGRAMMA's voor API-tests. Frameworks zoals gRPC, Avro en Thrift definiëren hun eigen IDL-specificaties. REST via HTTP heeft geen standaard-IDL-indeling, maar een veelvoorkomende keuze is OpenAPI (voorheen Swagger). U kunt ook een HTTP REST API maken zonder een formele definitietaal te gebruiken, maar dan verliest u de voordelen van het genereren en testen van code.

Serialisatie. Hoe worden objecten geserialiseerd via de kabel? Opties zijn onder andere op tekst gebaseerde indelingen (voornamelijk JSON) en binaire indelingen, zoals protocolbuffer. Binaire indelingen zijn over het algemeen sneller dan op tekst gebaseerde indelingen. JSON heeft echter voordelen op het gebied van interoperabiliteit, omdat de meeste talen en frameworks ondersteuning bieden voor JSON-serialisatie. Voor sommige serialisatie-indelingen is een vast schema vereist en sommige indelingen vereisen het compileren van een schemadefinitiebestand. In dat geval moet u deze stap opnemen in uw buildproces.

Framework- en taalondersteuning. HTTP wordt ondersteund in vrijwel elk framework en elke taal. gRPC, Avro en Thrift hebben allemaal bibliotheken voor C++, C#, Java en Python. Thrift en gRPC bieden ook ondersteuning voor Go.

Compatibiliteit en interoperabiliteit. Als u een protocol zoals gRPC kiest, hebt u mogelijk een protocolomzettingslaag nodig tussen de openbare API en de back-end. Een gateway kan die functie uitvoeren. Als u een service-mesh gebruikt, moet u overwegen welke protocollen compatibel zijn met de service-mesh. Linkerd heeft bijvoorbeeld ingebouwde ondersteuning voor HTTP, Thrift en gRPC.

Onze basislijnaanveling is om REST via HTTP te kiezen, tenzij u de prestatievoordelen van een binair protocol nodig hebt. REST via HTTP vereist geen speciale bibliotheken. Er wordt een minimale koppeling gemaakt, omdat bellers geen client-stub nodig hebben om met de service te communiceren. Er zijn uitgebreide ecosystemen met hulpprogramma's ter ondersteuning van schemadefinities, tests en bewaking van RESTful HTTP-eindpunten. Ten slotte is HTTP compatibel met browserclients, dus u hebt geen protocolomzettingslaag nodig tussen de client en de back-end.

Als u echter REST via HTTP kiest, moet u vroeg in het ontwikkelproces prestatie- en belastingstests uitvoeren om te controleren of het goed genoeg presteert voor uw scenario.

RESTful API-ontwerp

Er zijn veel resources voor het ontwerpen van RESTful-API's. Hier volgen enkele die nuttig kunnen zijn:

Hier volgen enkele specifieke overwegingen waarmee u rekening moet houden.

  • Let op API's die interne implementatiedetails lekken of gewoon een intern databaseschema spiegelen. De API moet het domein modelleren. Het is een contract tussen services en moet in het ideale geval alleen veranderen wanneer er nieuwe functionaliteit wordt toegevoegd, niet alleen omdat u bepaalde code hebt geherstructureerd of een databasetabel hebt genormaliseerd.

  • Voor verschillende typen clients, zoals mobiele toepassingen en desktopwebbrowsers, zijn mogelijk verschillende nettoladinggrootten of interactiepatronen vereist. Overweeg het patroon Back-ends voor front-ends te gebruiken om afzonderlijke back-ends te maken voor elke client, die een optimale interface voor die client beschikbaar maken.

  • Voor bewerkingen met bijwerkingen kunt u ze idempotent maken en implementeren als PUT-methoden. Dit maakt veilige nieuwe pogingen mogelijk en kan de tolerantie verbeteren. In het artikel Interservicecommunicatie wordt dit probleem uitvoeriger besproken.

  • HTTP-methoden kunnen asynchrone semantiek hebben, waarbij de methode onmiddellijk een antwoord retourneert, maar de service de bewerking asynchroon uitvoert. In dat geval moet de methode een HTTP 202-antwoordcode retourneren, die aangeeft dat de aanvraag is geaccepteerd voor verwerking, maar de verwerking nog niet is voltooid. Zie het patroon Asynchroon Request-Reply voor meer informatie.

REST toewijzen aan DDD-patronen

Patronen zoals entiteit, aggregaties en waardeobjecten zijn ontworpen om bepaalde beperkingen op de objecten in uw domeinmodel te plaatsen. In veel discussies over DDD worden de patronen gemodelleerd met behulp van objectgeoriënteerde (OO) taalconcepten zoals constructors of eigenschaps getters en setters. Waardeobjecten moeten bijvoorbeeld onveranderbaar zijn. In een OO-programmeertaal dwingt u dit af door de waarden in de constructor toe te wijzen en de eigenschappen alleen-lezen te maken:

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

Dit soort coderingsprocedures zijn met name belangrijk bij het bouwen van een traditionele monolithische toepassing. Met een grote codebasis kunnen veel subsystemen het Location object gebruiken, dus het is belangrijk dat het object het juiste gedrag afdwingt.

Een ander voorbeeld is het opslagplaatspatroon, dat ervoor zorgt dat andere onderdelen van de toepassing geen directe lees- of schrijfbewerkingen naar het gegevensarchief maken:

Diagram van een Drone-opslagplaats.

In een microservicesarchitectuur delen services echter niet dezelfde codebasis en delen ze geen gegevensarchieven. In plaats daarvan communiceren ze via API's. Denk na over het geval dat de Scheduler-service informatie opvragen over een drone van de Drone-service. De Drone-service heeft zijn interne model van een drone, uitgedrukt via code. Maar de Scheduler ziet dat niet. In plaats daarvan krijgt het een weergave van de drone-entiteit, misschien een JSON-object in een HTTP-antwoord.

Dit voorbeeld is ideaal voor de luchtvaart- en luchtvaartindustrie.

Diagram van de Drone-service.

De Scheduler-service kan de interne modellen van de Drone-service niet wijzigen of schrijven naar het gegevensarchief van de Drone-service. Dat betekent dat de code die de Drone-service implementeert, een kleiner beschikbaar oppervlak heeft, vergeleken met code in een traditionele monolith. Als de Drone-service een locatieklasse definieert, is het bereik van die klasse beperkt. Er wordt geen andere service rechtstreeks gebruikt voor de klasse.

Om deze redenen richt deze richtlijnen zich niet veel op coderingsprocedures, omdat ze betrekking hebben op de tactische DDD-patronen. Maar het blijkt dat u ook veel DDD-patronen kunt modelleren via REST API's.

Voorbeeld:

  • Aggregaties zijn van nature toegewezen aan resources in REST. De aggregaties levering worden bijvoorbeeld weergegeven als een resource door de Delivery-API.

  • Aggregaties zijn consistentiegrenzen. Bewerkingen op aggregaties mogen nooit een aggregaties in een inconsistente status achterlaten. Daarom moet u voorkomen dat u API's maakt waarmee een client de interne status van een aggregatie kan bewerken. Geef in plaats daarvan de voorkeur aan grof korrelige API's die aggregaties beschikbaar maken als resources.

  • Entiteiten hebben unieke identiteiten. In REST hebben resources unieke id's in de vorm van URL's. Maak resource-URL's die overeenkomen met de domeinidentiteit van een entiteit. De toewijzing van URL naar domeinidentiteit kan ondoorzichtig zijn voor de client.

  • Onderliggende entiteiten van een aggregaties kunnen worden bereikt door vanuit de hoofdentiteit te navigeren. Als u HATEOAS-principes volgt, kunnen onderliggende entiteiten worden bereikt via koppelingen in de weergave van de bovenliggende entiteit.

  • Omdat waardeobjecten onveranderbaar zijn, worden updates uitgevoerd door het hele waardeobject te vervangen. Implementeer in REST updates via PUT- of PATCH-aanvragen.

  • Met een opslagplaats kunnen clients objecten in een verzameling opvragen, toevoegen of verwijderen, waarbij de details van het onderliggende gegevensarchief worden geabstraheerd. In REST kan een verzameling een afzonderlijke resource zijn, met methoden voor het opvragen van de verzameling of het toevoegen van nieuwe entiteiten aan de verzameling.

Wanneer u uw API's ontwerpt, moet u nadenken over hoe ze het domeinmodel uitdrukken, niet alleen de gegevens in het model, maar ook de bedrijfsactiviteiten en de beperkingen voor de gegevens.

DDD-concept REST-equivalent Opmerking
Samenvoegen Bron { "1":1234, "status":"pending"... }
Identiteit URL https://delivery-service/deliveries/1
Onderliggende entiteiten Koppelingen { "href": "/deliveries/1/confirmation" }
Waardeobjecten bijwerken PUT of PATCH PUT https://delivery-service/deliveries/1/dropoff
Opslagplaats Verzameling https://delivery-service/deliveries?status=pending

API-versiebeheer

Een API is een contract tussen een service en clients of consumenten van die service. Als een API wordt gewijzigd, bestaat het risico dat clients worden onderbroken die afhankelijk zijn van de API, ongeacht of dit externe clients of andere microservices zijn. Daarom is het een goed idee om het aantal API-wijzigingen dat u aanbrengt, te minimaliseren. Vaak zijn voor wijzigingen in de onderliggende implementatie geen wijzigingen in de API vereist. Op een bepaald moment wilt u echter nieuwe functies of nieuwe mogelijkheden toevoegen waarvoor een bestaande API moet worden gewijzigd.

Breng, indien mogelijk, API-wijzigingen compatibel aan met eerdere versies. Vermijd bijvoorbeeld het verwijderen van een veld uit een model, omdat dit clients kan breken die verwachten dat het veld er is. Als u een veld toevoegt, wordt de compatibiliteit niet verbroken, omdat clients alle velden die ze niet begrijpen in een antwoord moeten negeren. De service moet echter omgaan met de situatie waarin een oudere client het nieuwe veld in een aanvraag weglaat.

Ondersteuning voor versiebeheer in uw API-contract. Als u een belangrijke API-wijziging introduceert, introduceert u een nieuwe API-versie. Blijf de vorige versie ondersteunen en laat clients selecteren welke versie moet worden aangeroepen. Er zijn een aantal manieren om dit te doen. Een is simpelweg om beide versies in dezelfde service beschikbaar te maken. Een andere optie is om twee versies van de service naast elkaar uit te voeren en aanvragen te routeren naar een of de andere versie, op basis van HTTP-routeringsregels.

Diagram met twee opties voor het ondersteunen van versiebeheer.

Het diagram heeft twee delen. 'Service ondersteunt twee versies' toont de v1-client en de v2-client die beide naar één service verwijzen. 'Side-by-side deployment' toont de v1-client die verwijst naar een v1-service en de v2-client die verwijst naar een v2-service.

Er zijn kosten verbonden aan het ondersteunen van meerdere versies, wat betreft tijd, testen en operationele overhead voor ontwikkelaars. Daarom is het goed om oude versies zo snel mogelijk te verwijderen. Voor interne API's kan het team dat eigenaar is van de API samenwerken met andere teams om ze te helpen migreren naar de nieuwe versie. Dit is wanneer een governanceproces voor meerdere teams nuttig is. Voor externe (openbare) API's kan het moeilijker zijn om een API-versie te verwijderen, met name als de API wordt gebruikt door derden of door systeemeigen clienttoepassingen.

Wanneer een service-implementatie wordt gewijzigd, is het handig om de wijziging te taggen met een versie. De versie biedt belangrijke informatie bij het oplossen van fouten. Het kan erg nuttig zijn voor analyse van hoofdoorzaak om precies te weten welke versie van de service is aangeroepen. Overweeg het gebruik van semantische versiebeheer voor serviceversies. Semantische versiebeheer maakt gebruik van een MAJOR. MINDERJARIGE. PATCH-indeling . Clients moeten echter alleen een API selecteren op basis van het primaire versienummer of mogelijk de secundaire versie als er belangrijke (maar niet-belangrijke) wijzigingen zijn tussen secundaire versies. Met andere woorden, het is redelijk dat clients kiezen tussen versie 1 en versie 2 van een API, maar niet om versie 2.1.3 te selecteren. Als u dat granulariteitsniveau toestaat, loopt u het risico dat u een verspreiding van versies ondersteunt.

Zie Versiebeheer van een RESTful-web-API voor meer informatie over API-versiebeheer.

Idempotente bewerkingen

Een bewerking is idempotent als deze meerdere keren kan worden aangeroepen zonder extra bijwerkingen na de eerste aanroep te produceren. Idempotentie kan een nuttige tolerantiestrategie zijn, omdat een upstream-service een bewerking meerdere keren veilig kan aanroepen. Zie Gedistribueerde transacties voor een bespreking van dit punt.

De HTTP-specificatie geeft aan dat GET-, PUT- en DELETE-methoden idempotent moeten zijn. POST-methoden zijn niet gegarandeerd idempotent. Als een POST-methode een nieuwe resource maakt, is er over het algemeen geen garantie dat deze bewerking idempotent is. De specificatie definieert idempotent op deze manier:

Een aanvraagmethode wordt beschouwd als 'idempotent' als het beoogde effect op de server van meerdere identieke aanvragen met die methode hetzelfde is als het effect voor één dergelijke aanvraag. (RFC 7231)

Het is belangrijk om inzicht te hebben in het verschil tussen PUT en POST-semantiek bij het maken van een nieuwe entiteit. In beide gevallen verzendt de client een weergave van een entiteit in de aanvraagbody. Maar de betekenis van de URI is anders.

  • Voor een POST-methode vertegenwoordigt de URI een bovenliggende resource van de nieuwe entiteit, zoals een verzameling. Als u bijvoorbeeld een nieuwe levering wilt maken, kan de URI zijn /api/deliveries. De server maakt de entiteit en wijst deze toe aan een nieuwe URI, zoals /api/deliveries/39660. Deze URI wordt geretourneerd in de locatieheader van het antwoord. Telkens wanneer de client een aanvraag verzendt, maakt de server een nieuwe entiteit met een nieuwe URI.

  • Voor een PUT-methode identificeert de URI de entiteit. Als er al een entiteit met die URI bestaat, vervangt de server de bestaande entiteit door de versie in de aanvraag. Als er geen entiteit met die URI bestaat, maakt de server er een. Stel dat de client een PUT-aanvraag verzendt naar api/deliveries/39660. Ervan uitgaande dat er geen levering met die URI is, maakt de server een nieuwe. Als de client nu dezelfde aanvraag opnieuw verzendt, vervangt de server de bestaande entiteit.

Hier volgt de implementatie van de Delivery Service van de PUT-methode.

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

Er wordt verwacht dat de meeste aanvragen een nieuwe entiteit maken, dus de methode roept optimistisch CreateAsync het opslagplaatsobject aan en verwerkt vervolgens eventuele uitzonderingen voor dubbele resources door de resource bij te werken.

Volgende stappen

Meer informatie over het gebruik van een API-gateway op de grens tussen clienttoepassingen en microservices.