Custom caching in Azure API Management (Anpassad cachelagring i Azure API Management)

GÄLLER FÖR: Alla API Management-nivåer

Azure API Management-tjänsten har inbyggt stöd för CACHElagring av HTTP-svar med resurs-URL:en som nyckel. Nyckeln kan ändras av begärandehuvuden vary-by med hjälp av egenskaperna. Detta är användbart för cachelagring av hela HTTP-svar (kallas även representationer), men ibland är det användbart att bara cachelagra en del av en representation. Principerna cache-lookup-value och cache-store-value ger möjlighet att lagra och hämta godtyckliga datadelar från principdefinitioner. Den här möjligheten lägger också till värde i principen för att skicka begäran eftersom du kan cachelagrar svar från externa tjänster.

Arkitektur

API Management-tjänsten använder en intern datacache per klientorganisation så att du fortfarande får åtkomst till samma cachelagrade data när du skalar upp till flera enheter. Men när du arbetar med en distribution i flera regioner finns det oberoende cacheminnen i var och en av regionerna. Det är viktigt att inte behandla cacheminnet som ett datalager, där det är den enda källan till viss information. Om du gjorde det och senare bestämde dig för att dra nytta av distributionen i flera regioner kan kunder med användare som reser förlora åtkomsten till dessa cachelagrade data.

Kommentar

Den interna cachen är inte tillgänglig på förbrukningsnivån för Azure API Management. Du kan använda en extern Azure Cache for Redis istället. En extern cache möjliggör större cachekontroll och flexibilitet för API Management-instanser på alla nivåer.

Cachelagring av fragment

Det finns vissa fall där svar som returneras innehåller en del data som är dyra att fastställa och ändå förblir färska under en rimlig tid. Tänk dig till exempel en tjänst som skapats av ett flygbolag som tillhandahåller information om flygreservationer, flygstatus och så vidare. Om användaren är medlem i flygbolagens poängprogram skulle de också ha information om sin aktuella status och ackumulerade körsträcka. Den här användarrelaterade informationen kan lagras i ett annat system, men det kan vara önskvärt att inkludera den i svar som returneras om flygstatus och reservationer. Detta kan göras med hjälp av en process som kallas fragmentcachelagring. Den primära representationen kan returneras från ursprungsservern med någon form av token för att ange var den användarrelaterade informationen ska infogas.

Överväg följande JSON-svar från ett serverdels-API.

{
  "airline" : "Air Canada",
  "flightno" : "871",
  "status" : "ontime",
  "gate" : "B40",
  "terminal" : "2A",
  "userprofile" : "$userprofile$"
}  

Och den sekundära resursen ser /userprofile/{userid} ut så här:

{ "username" : "Bob Smith", "Status" : "Gold" }

För att fastställa lämplig användarinformation som ska inkluderas måste API Management identifiera vem slutanvändaren är. Den här mekanismen är implementeringsberoende. I följande exempel används anspråket för Subject en JWT token.

<set-variable
  name="enduserid"
  value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Subject)" />

API Management lagrar enduserid värdet i en kontextvariabel för senare användning. Nästa steg är att avgöra om en tidigare begäran redan har hämtat användarinformationen och lagrat den i cacheminnet. För detta använder cache-lookup-value API Management principen.

<cache-lookup-value
key="@("userprofile-" + context.Variables["enduserid"])"
variable-name="userprofile" />

Om det inte finns någon post i cacheminnet som motsvarar nyckelvärdet skapas ingen userprofile kontextvariabel. API Management kontrollerar om sökningen lyckades med hjälp av choose kontrollflödesprincipen.

<choose>
    <when condition="@(!context.Variables.ContainsKey("userprofile"))">
        <!-- If the userprofile context variable doesn’t exist, make an HTTP request to retrieve it.  -->
    </when>
</choose>

userprofile Om kontextvariabeln inte finns måste API Management göra en HTTP-begäran för att hämta den.

<send-request
  mode="new"
  response-variable-name="userprofileresponse"
  timeout="10"
  ignore-error="true">

  <!-- Build a URL that points to the profile for the current end-user -->
  <set-url>@(new Uri(new Uri("https://apimairlineapi.azurewebsites.net/UserProfile/"),
      (string)context.Variables["enduserid"]).AbsoluteUri)
  </set-url>
  <set-method>GET</set-method>
</send-request>

API Management använder enduserid för att konstruera URL:en till användarprofilresursen. När API Management har svaret hämtar det brödtexten ur svaret och lagrar den tillbaka till en kontextvariabel.

<set-variable
    name="userprofile"
    value="@(((IResponse)context.Variables["userprofileresponse"]).Body.As<string>())" />

Om du vill undvika att API Management gör den här HTTP-begäran igen kan du när samma användare gör en annan begäran ange att användarprofilen ska lagras i cacheminnet.

<cache-store-value
    key="@("userprofile-" + context.Variables["enduserid"])"
    value="@((string)context.Variables["userprofile"])" duration="100000" />

API Management lagrar värdet i cacheminnet med samma nyckel som API Management ursprungligen försökte hämta det med. Den varaktighet som API Management väljer att lagra värdet ska baseras på hur ofta informationen ändras och hur toleranta användare är för inaktuell information.

Det är viktigt att inse att hämtning från cacheminnet fortfarande är en out-of-process-nätverksbegäran och potentiellt kan lägga till tiotals millisekunder i begäran. Fördelarna kommer när det tar längre tid att fastställa användarprofilinformationen än så på grund av att du behöver göra databasfrågor eller sammanställa information från flera serverdelar.

Det sista steget i processen är att uppdatera det returnerade svaret med användarprofilinformationen.

<!-- Update response body with user profile-->
<find-and-replace
    from='"$userprofile$"'
    to="@((string)context.Variables["userprofile"])" />

Du kan välja att inkludera citattecknen som en del av token så att även om ersättningen inte inträffar är svaret fortfarande en giltig JSON.

När du har kombinerat de här stegen är slutresultatet en princip som ser ut som följande.

<policies>
    <inbound>
        <!-- How you determine user identity is application dependent -->
        <set-variable
          name="enduserid"
          value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Subject)" />

        <!--Look for userprofile for this user in the cache -->
        <cache-lookup-value
          key="@("userprofile-" + context.Variables["enduserid"])"
          variable-name="userprofile" />

        <!-- If API Management doesn’t find it in the cache, make a request for it and store it -->
        <choose>
            <when condition="@(!context.Variables.ContainsKey("userprofile"))">
                <!-- Make HTTP request to get user profile -->
                <send-request
                  mode="new"
                  response-variable-name="userprofileresponse"
                  timeout="10"
                  ignore-error="true">

                   <!-- Build a URL that points to the profile for the current end-user -->
                    <set-url>@(new Uri(new Uri("https://apimairlineapi.azurewebsites.net/UserProfile/"),(string)context.Variables["enduserid"]).AbsoluteUri)</set-url>
                    <set-method>GET</set-method>
                </send-request>

                <!-- Store response body in context variable -->
                <set-variable
                  name="userprofile"
                  value="@(((IResponse)context.Variables["userprofileresponse"]).Body.As<string>())" />

                <!-- Store result in cache -->
                <cache-store-value
                  key="@("userprofile-" + context.Variables["enduserid"])"
                  value="@((string)context.Variables["userprofile"])"
                  duration="100000" />
            </when>
        </choose>
        <base />
    </inbound>
    <outbound>
        <!-- Update response body with user profile-->
        <find-and-replace
              from='"$userprofile$"'
              to="@((string)context.Variables["userprofile"])" />
        <base />
    </outbound>
</policies>

Den här cachelagringsmetoden används främst på webbplatser där HTML är sammansatt på serversidan så att den kan återges som en enda sida. Det kan också vara användbart i API:er där klienter inte kan göra HTTP-cachelagring på klientsidan eller det är önskvärt att inte lägga det ansvaret på klienten.

Samma typ av fragmentcachelagring kan också göras på serverdelswebbservrarna med hjälp av en Redis-cachelagringsserver, men det är användbart att använda API Management-tjänsten för att utföra det här arbetet när de cachelagrade fragmenten kommer från olika serverdelar än de primära svaren.

Transparent versionshantering

Det är vanligt att flera olika implementeringsversioner av ett API stöds samtidigt. Till exempel för att stödja olika miljöer (utveckling, testning, produktion osv.) eller för att stödja äldre versioner av API:et för att ge API-konsumenter tid att migrera till nyare versioner.

En metod för att hantera detta, i stället för att kräva att klientutvecklare ändrar URL:er från /v1/customers till /v2/customers är att lagra i konsumentens profildata vilken version av API:et som de för närvarande vill använda och anropa lämplig serverdels-URL. För att fastställa rätt serverdels-URL för att anropa en viss klient är det nödvändigt att köra frågor mot vissa konfigurationsdata. Genom att cachelagra dessa konfigurationsdata kan API Management minimera prestandapåföljden för att göra den här sökningen.

Det första steget är att fastställa den identifierare som används för att konfigurera den önskade versionen. I det här exemplet valde jag att associera versionen med produktprenumerationsnyckeln.

<set-variable name="clientid" value="@(context.Subscription.Key)" />

API Management gör sedan en cachesökning för att se om den redan har hämtat den önskade klientversionen.

<cache-lookup-value
key="@("clientversion-" + context.Variables["clientid"])"
variable-name="clientversion" />

Sedan kontrollerar API Management om den inte hittade den i cacheminnet.

<choose>
    <when condition="@(!context.Variables.ContainsKey("clientversion"))">

Om API Management inte hittade det hämtar API Management det.

<send-request
    mode="new"
    response-variable-name="clientconfiguresponse"
    timeout="10"
    ignore-error="true">
            <set-url>@(new Uri(new Uri(context.Api.ServiceUrl.ToString() + "api/ClientConfig/"),(string)context.Variables["clientid"]).AbsoluteUri)</set-url>
            <set-method>GET</set-method>
</send-request>

Extrahera svarstexten från svaret.

<set-variable
      name="clientversion"
      value="@(((IResponse)context.Variables["clientconfiguresponse"]).Body.As<string>())" />

Lagra den i cacheminnet för framtida användning.

<cache-store-value
      key="@("clientversion-" + context.Variables["clientid"])"
      value="@((string)context.Variables["clientversion"])"
      duration="100000" />

Och uppdatera slutligen serverdels-URL:en för att välja den version av tjänsten som klienten vill ha.

<set-backend-service
      base-url="@(context.Api.ServiceUrl.ToString() + "api/" + (string)context.Variables["clientversion"] + "/")" />

Den fullständiga principen är följande:

<inbound>
    <base />
    <set-variable name="clientid" value="@(context.Subscription.Key)" />
    <cache-lookup-value key="@("clientversion-" + context.Variables["clientid"])" variable-name="clientversion" />

    <!-- If API Management doesn’t find it in the cache, make a request for it and store it -->
    <choose>
        <when condition="@(!context.Variables.ContainsKey("clientversion"))">
            <send-request mode="new" response-variable-name="clientconfiguresponse" timeout="10" ignore-error="true">
                <set-url>@(new Uri(new Uri(context.Api.ServiceUrl.ToString() + "api/ClientConfig/"),(string)context.Variables["clientid"]).AbsoluteUri)</set-url>
                <set-method>GET</set-method>
            </send-request>
            <!-- Store response body in context variable -->
            <set-variable name="clientversion" value="@(((IResponse)context.Variables["clientconfiguresponse"]).Body.As<string>())" />
            <!-- Store result in cache -->
            <cache-store-value key="@("clientversion-" + context.Variables["clientid"])" value="@((string)context.Variables["clientversion"])" duration="100000" />
        </when>
    </choose>
    <set-backend-service base-url="@(context.Api.ServiceUrl.ToString() + "api/" + (string)context.Variables["clientversion"] + "/")" />
</inbound>

Att göra det möjligt för API-konsumenter att transparent kontrollera vilken serverdelsversion som används av klienter utan att behöva uppdatera och distribuera om klienter är en elegant lösning som åtgärdar många API-versionsbekymmer.

Klientisolering

I större distributioner med flera klientorganisationer skapar vissa företag separata grupper av klienter på distinkta distributioner av serverdelsmaskinvara. Detta minimerar antalet kunder som påverkas av ett maskinvaruproblem på serverdelen. Det gör också att nya programvaruversioner kan distribueras i etapper. Den här serverdelsarkitekturen bör helst vara transparent för API-konsumenter. Detta kan uppnås på ett liknande sätt som transparent versionshantering eftersom det baseras på samma teknik för att manipulera serverdels-URL:en med hjälp av konfigurationstillstånd per API-nyckel.

I stället för att returnera en önskad version av API:et för varje prenumerationsnyckel returnerar du en identifierare som relaterar en klientorganisation till den tilldelade maskinvarugruppen. Den identifieraren kan användas för att konstruera lämplig serverdels-URL.

Sammanfattning

Friheten att använda Azure API-hanteringscache för att lagra alla typer av data ger effektiv åtkomst till konfigurationsdata som kan påverka hur en inkommande begäran bearbetas. Den kan också användas för att lagra datafragment som kan utöka svar som returneras från ett serverdels-API.