Partilhar via


Colocação em cache personalizada da Gestão de API do Azure

APLICA-SE A: Todas as camadas de gerenciamento de API

O serviço de Gerenciamento de API do Azure tem suporte interno para cache de resposta HTTP usando a URL do recurso como chave. A chave pode ser modificada por cabeçalhos de solicitação usando as vary-by propriedades. Isso é útil para armazenar em cache respostas HTTP inteiras (também conhecidas como representações), mas às vezes é útil armazenar em cache apenas uma parte de uma representação. As políticas cache-lookup-value e cache-store-value fornecem a capacidade de armazenar e recuperar partes arbitrárias de dados de dentro de definições de política. Essa capacidade também agrega valor à política de solicitação de envio porque você pode armazenar em cache respostas de serviços externos.

Arquitetura

O serviço de Gerenciamento de API usa um cache de dados interno compartilhado por locatário para que, à medida que você aumenta a escala para várias unidades, ainda tenha acesso aos mesmos dados armazenados em cache. No entanto, ao trabalhar com uma implantação de várias regiões, há caches independentes dentro de cada uma das regiões. É importante não tratar o cache como um armazenamento de dados, onde ele é a única fonte de alguma informação. Se você fez isso, e mais tarde decidiu aproveitar a implantação em várias regiões, os clientes com usuários que viajam podem perder o acesso a esses dados armazenados em cache.

Nota

O cache interno não está disponível na camada de Consumo do Gerenciamento de API do Azure. Em vez disso, você pode usar um Cache Redis do Azure externo. Um cache externo permite maior controle de cache e flexibilidade para instâncias de Gerenciamento de API em todos os níveis.

Cache de fragmentos

Há certos casos em que as respostas que estão sendo retornadas contêm alguma parte dos dados que são caros para determinar e ainda permanecem atualizados por um período razoável de tempo. Como exemplo, considere um serviço criado por uma companhia aérea que fornece informações relacionadas a reservas de voos, status de voos e assim por diante. Se o usuário for membro do programa de pontos das companhias aéreas, ele também terá informações relacionadas ao seu status atual e quilometragem acumulada. Essas informações relacionadas ao usuário podem ser armazenadas em um sistema diferente, mas pode ser desejável incluí-las nas respostas retornadas sobre o status do voo e as reservas. Isso pode ser feito usando um processo chamado cache de fragmentos. A representação primária pode ser retornada do servidor de origem usando algum tipo de token para indicar onde as informações relacionadas ao usuário devem ser inseridas.

Considere a seguinte resposta JSON de uma API de back-end.

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

E recurso secundário em /userprofile/{userid} que se parece com,

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

Para determinar as informações apropriadas do usuário a serem incluídas, o Gerenciamento de API precisa identificar quem é o usuário final. Este mecanismo depende da implementação. O exemplo a seguir usa a Subject declaração de um JWT token.

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

O Gerenciamento de API armazena o enduserid valor em uma variável de contexto para uso posterior. A próxima etapa é determinar se uma solicitação anterior já recuperou as informações do usuário e as armazenou no cache. Para isso, o Gerenciamento de API usa a cache-lookup-value política.

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

Se não houver nenhuma entrada no cache que corresponda ao valor da chave, nenhuma userprofile variável de contexto será criada. O Gerenciamento de API verifica o sucesso da pesquisa usando a choose política de fluxo de controle.

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

Se a userprofile variável de contexto não existir, o Gerenciamento de API terá que fazer uma solicitação HTTP para recuperá-la.

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

O Gerenciamento de API usa o enduserid para construir a URL para o recurso de perfil de usuário. Depois que o Gerenciamento de API tiver a resposta, ele retira o corpo do texto da resposta e o armazena de volta em uma variável de contexto.

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

Para evitar que o Gerenciamento de API faça essa solicitação HTTP novamente, quando o mesmo usuário fizer outra solicitação, você poderá especificar para armazenar o perfil de usuário no cache.

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

O Gerenciamento de API armazena o valor no cache usando a mesma chave com a qual o Gerenciamento de API tentou recuperá-lo originalmente. A duração que o Gerenciamento de API escolhe para armazenar o valor deve ser baseada na frequência com que as informações são alteradas e na tolerância dos usuários a informações desatualizadas.

É importante perceber que a recuperação do cache ainda é uma solicitação de rede fora de processo e potencialmente pode adicionar dezenas de milissegundos à solicitação. Os benefícios vêm quando a determinação das informações de perfil de usuário leva mais tempo do que isso devido à necessidade de fazer consultas de banco de dados ou agregar informações de vários back-ends.

A etapa final do processo é atualizar a resposta retornada com as informações do perfil do usuário.

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

Você pode optar por incluir as aspas como parte do token para que, mesmo quando a substituição não ocorrer, a resposta ainda seja um JSON válido.

Depois de combinar essas etapas, o resultado final é uma política semelhante à seguinte.

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

Essa abordagem de cache é usada principalmente em sites onde o HTML é composto no lado do servidor para que possa ser renderizado como uma única página. Também pode ser útil em APIs onde os clientes não podem fazer cache HTTP do lado do cliente ou é desejável não colocar essa responsabilidade no cliente.

Esse mesmo tipo de cache de fragmentos também pode ser feito nos servidores Web de back-end usando um servidor de cache Redis, no entanto, usar o serviço de Gerenciamento de API para executar esse trabalho é útil quando os fragmentos armazenados em cache são provenientes de back-ends diferentes das respostas primárias.

Controle de versão transparente

É prática comum que várias versões de implementação diferentes de uma API sejam suportadas a qualquer momento. Por exemplo, para oferecer suporte a diferentes ambientes (desenvolvimento, teste, produção, etc.) ou para dar suporte a versões mais antigas da API para dar tempo para os consumidores de API migrarem para versões mais recentes.

Uma abordagem para lidar com isso, em vez de exigir que os desenvolvedores de clientes alterem as URLs de para /v2/customers é armazenar nos dados de /v1/customers perfil do consumidor qual versão da API eles desejam usar atualmente e chamar a URL de back-end apropriada. Para determinar a URL de back-end correta para chamar um cliente específico, é necessário consultar alguns dados de configuração. Ao armazenar em cache esses dados de configuração, o Gerenciamento de API pode minimizar a penalidade de desempenho de fazer essa pesquisa.

O primeiro passo é determinar o identificador usado para configurar a versão desejada. Neste exemplo, optei por associar a versão à chave de assinatura do produto.

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

Em seguida, o Gerenciamento de API faz uma pesquisa de cache para ver se ele já recuperou a versão do cliente desejada.

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

Em seguida, o Gerenciamento de API verifica se não o encontrou no cache.

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

Se o Gerenciamento de API não o encontrar, o Gerenciamento de API o recuperará.

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

Extraia o texto do corpo da resposta da resposta.

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

Armazene-o de volta no cache para uso futuro.

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

E, finalmente, atualize a URL de back-end para selecionar a versão do serviço desejada pelo cliente.

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

A política completa é a seguinte:

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

Permitir que os consumidores de API controlem de forma transparente qual versão de back-end está sendo acessada pelos clientes sem ter que atualizar e reimplantar clientes é uma solução elegante que aborda muitas preocupações de controle de versão de API.

Isolamento do inquilino

Em implantações maiores e multilocatário, algumas empresas criam grupos separados de locatários em implantações distintas de hardware de back-end. Isso minimiza o número de clientes afetados por um problema de hardware no back-end. Ele também permite que novas versões de software sejam lançadas em etapas. Idealmente, essa arquitetura de back-end deve ser transparente para os consumidores de API. Isso pode ser alcançado de forma semelhante ao controle de versão transparente porque é baseado na mesma técnica de manipulação da URL de back-end usando o estado de configuração por chave de API.

Em vez de retornar uma versão preferencial da API para cada chave de assinatura, você retornaria um identificador que relaciona um locatário ao grupo de hardware atribuído. Esse identificador pode ser usado para construir a URL de back-end apropriada.

Resumo

A liberdade de usar o cache de gerenciamento da API do Azure para armazenar qualquer tipo de dados permite o acesso eficiente aos dados de configuração que podem afetar a maneira como uma solicitação de entrada é processada. Ele também pode ser usado para armazenar fragmentos de dados que podem aumentar as respostas, retornadas de uma API de back-end.