Azure API 管理中的自定义缓存

Azure API 管理服务使用资源 URL 作为键,提供对 HTTP 响应缓存的内置支持。 可以使用 vary-by 属性根据请求标头修改键。 这种做法适合用于缓存整个 HTTP 响应(也称为“表示形式”),但有时也适合用于只缓存一部分表示形式。 使用新的 cache-lookup-valuecache-store-value 策略可以存储和检索策略定义中的任意数据。 此功能使得以前推出的 send-request 策略更有价值,因为现在可以缓存来自外部服务的响应。

体系结构

API 管理服务使用基于租户的共享数据缓存,因此,在纵向扩展到多个单位后,仍可以访问相同的缓存数据。 但是,使用多区域部署时,每个区域内有独立的缓存。 出于这种原因,不得将缓存视为数据存储,因为数据存储是某些信息片段的唯一来源。 如果这样做,后来又决定利用多区域部署,则具有移动工作人员的客户可能会失去对该缓存数据的访问权限。

分段缓存

在某些情况下,返回的响应中包含的某些数据部分不但非常重要,而且还保留一段合理的时间。 航空公司构建的、提供航班预订、航班状态等信息的服务就是一个示例。如果用户是航空公司积分计划的会员,则他们也会获得其当前状态和累积里程的相关信息。 这些用户的相关信息可能存储在不同的系统中,但也可能需要包含在与航班状态和预订相关的返回响应中。 可以使用名为“分段缓存”的过程实现此目的。 可以从源服务器返回主要表示形式,使用某种令牌来指明要将用户相关的信息插入到何处。

假设后端 API 返回了以下 JSON 响应。

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

/userprofile/{userid} 中的辅助资源如下所示:

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

为了确定要包含哪些适当的用户信息,需要识别最终用户是谁。 此机制与实现相关。 例如,此处使用了 JWT 令牌的 Subject 声明。

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

我们将此 enduserid 值存储在一个上下文变量中供以后使用。 下一步是确定前面的请求是否已检索了用户信息并将其存储在缓存中。 为此,我们使用了 cache-lookup-value 策略。

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

如果缓存中没有任何条目对应于键值,则不会创建 userprofile 上下文变量。 使用 choose 控制流策略来检查查询是否成功。

<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 上下文变量不存在,则要发出 HTTP 请求来检索信息。

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

使用 enduserid 来构造用户配置文件资源的 URL。 获得响应后,可以从响应中提取正文文本,并将它存回到上下文变量。

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

为了避免当同一用户发出另一个请求时我们必须再次发出此 HTTP 请求,可以将用户配置文件存储在缓存中。

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

使用最初尝试检索值时使用的同一个键,将值存储在缓存中。 选择将值存储多长时间应该取决于信息更改的频率,以及用户对过期信息的容许度。

请务必注意,从缓存中检索信息仍是过程外的网络请求,可能使请求的时间增加数十毫秒。 如果在确定用户配置文件信息时所需的时间远远超过需要执行数据库查询或者从多个后端聚合信息的时间,这种方案的优势就会显现出来。

此过程的最后一步是使用用户配置文件信息更新返回的响应。

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

此处在令牌中包含了引号,是为了实现以下目的:即使替换没有发生,响应仍是有效的 JSON。 这主要是为了方便调试。

将所有这些步骤组合到一起后,最终会创建如下所示的策略。

<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 we don’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.cn/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>

此缓存方法主要用于其 HTML 在服务器端撰写的网站中,能够以单页的形式呈现 HTML。 但是,它也适用于其中的客户端无法执行客户端 HTTP 缓存的 API,或者不需要将此责任施加到客户端的情况。

这种分段缓存也可以在使用 Redis 缓存服务器的后端 Web 服务器上执行,但是,当缓存的段来自不同后端而不是主要响应时,使用 API 管理服务执行此工作便很有效。

透明版本控制

随时支持多个不同实现版本的 API 是常见做法。 这也许是为了支持不同的环境,例如开发、测试、生产等,或为了使旧版 API 能够让 API 使用者有时间迁移到新版本。

其中一种方法是将使用者目前想要使用的 API 版本存储在使用者的配置文件数据中,并调用适当的后端 URL,客户端开发人员无需将 URL 从 /v1/customers 更改为 /v2/customers。 为了确定特定客户端要调用的正确后端 URL,必须查询某些配置数据。 通过缓存此配置数据,可以最大程度地减少执行此查找所造成的不利性能影响。

第一步是确定用于配置所需版本的标识符。 在本示例中,我选择将版本关联到产品订阅密钥。

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

然后,执行缓存查找来查看是否已检索到了所需的客户端版本。

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

之后,检查在缓存中是否找到了它。

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

从响应中提取响应正文文本。

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

将它存回到缓存供将来使用。

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

最后,更新后端 URL 以选择客户端所需的服务版本。

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

完整的策略如下所示。

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

    <!-- If we don’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>

使 API 使用者无需更新和重新部署客户端就能透明控制客户端可以访问的后端版本是一种优雅的解决办法,能够解决 API 版本控制方面的许多问题。

租户隔离

在大型多租户部署中,有些公司在后端硬件的不同部署上创建独立的租户组。 这可以最大程度地减少后端硬件问题影响到的客户数目。 同时,可以将新软件版本分阶段推出。 在理想情况下,对 API 使用者而言,此后端体系结构应是透明的。 若要实现此目的,可以采用类似于透明版本控制的方式,因为这样可以根据相同的后端 URL 处理方法使用每个 API 密钥的配置状态。

结果是返回将租户与分配的硬件组相关联的标识符,而不是每个订阅密钥的首选 API 版本。 该标识符可用于构造相应的后端 URL。

摘要

由于可以自由使用 Azure API 管理缓存来存储任何类型的数据,因此可以有效访问可能影响入站请求处理方式的配置数据。 上述缓存也可以用于存储数据段(可以补充从后端 API 返回的响应)。

后续步骤

如果这些策略实现了其他方案,或者有方案想要实现但觉得目前不可行,请在本主题的 Disqus 贴子中提供反馈。