微服务的合同、寻址和 API

区域 ID

REGION_ID 是 Google 根据您在创建应用时选择的区域分配的缩写代码。此代码不对应于国家/地区或省,尽管某些区域 ID 可能类似于常用国家/地区代码和省代码。对于 2020 年 2 月以后创建的应用,REGION_ID.r 包含在 App Engine 网址中。对于在此日期之前创建的现有应用,网址中的区域 ID 是可选的。

详细了解区域 ID

App Engine 上的微服务通常使用基于 HTTP 的 RESTful API 相互调用。您也可以使用任务队列在后台调用微服务,采用这种方法时此处所述 API 设计原则适用。为了确保基于微服务的应用稳定、安全且性能良好,遵循某些模式非常重要。

使用强合同

对于基于微服务的应用,最重要的一个方面是微服务能够完全互不干扰地独立部署。为了实现这种独立性,每项微服务必须向其客户端(这些客户端是另一些微服务)提供有版本控制、定义明确的合同。除非确认没有其他微服务依赖于有版本控制的特定合同,否则每项服务都必须遵守这些有版本控制的合同。请记住,其他微服务可能需要回滚到某个之前的代码版本(需要之前的合同),因此请务必在弃用和停用政策中考虑到这一点。

在设计运行稳定、基于微服务的应用时,如何养成使用有版本控制的强合同的习惯可能是最具挑战性的组织问题。开发团队必须深刻理解重大更改与非重大更改的区别。他们必须知道何时需要新的主要版本,了解如何及何时停用旧合同。各团队必须采用适当的沟通方法(包括弃用和停用通知),以确保了解微服务合同的更改。虽然这可能听起来很困难,但从长远角度考虑,将这些做法纳入开发文化将显著提高开发速度和质量。

为微服务指定网址

您可以直接为服务和代码版本指定网址。因此,您可以将新代码版本与现有代码版本并行部署,并且可以对新代码进行测试,然后再将其设为默认提供的版本。

每个 App Engine 项目都有一项默认服务,每项服务都有一个默认代码版本。如需为项目默认版本的默认服务指定网址,请使用以下网址:
https://PROJECT_ID.REGION_ID.r.appspot.com

如果部署名为 user-service 的服务,可以使用以下网址访问该服务的默认服务版本:

https://user-service-dot-my-app.REGION_ID.r.appspot.com

如果将另一个名为 banana 的非默认代码版本部署到 user-service 服务,则可以使用以下网址直接访问该代码版本:

https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com

请注意,如果将另一个名为 cherry 的非默认代码版本部署到 default 服务,则可以使用以下网址访问该代码版本:

https://cherry-dot-my-app.REGION_ID.r.appspot.com

App Engine 的规则规定,默认服务中的代码版本名称不得与服务名称冲突。

为特定代码版本直接指定网址应仅用于冒烟测试以及协助 A/B 测试、前滚和回滚。因此,您的客户端代码仅应为默认服务或特定服务的默认服务版本指定网址:


https://PROJECT_ID.REGION_ID.r.appspot.com

https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com

利用这种网址指定方式,微服务可以部署其服务的新版本(包括 bug 修复),而无需对客户端进行任何更改。

使用 API 版本

每个微服务 API 都应在网址中指定一个主要 API 版本,例如:

/user-service/v1/

这个主要 API 版本在日志中明确标识了被调用微服务的 API 版本。更重要的是,主要 API 版本会生成不同的网址,因此可以将新的主要 API 版本与旧的主要 API 版本并行提供:

/user-service/v1/
/user-service/v2/

不需要在网址中包含次要 API 版本,因为根据定义,次要 API 版本不会引入任何重大更改。实际上,在网址中包含次要 API 版本会造成网址数量激增,导致客户端无法保证移到新的次要 API 版本。

请注意,本文假设使用的是持续集成和交付环境,其中的主分支始终部署到 App Engine。本文中有两个不同的版本概念:

  • 代码版本,它与 App Engine 服务版本直接对应,代表主分支的特定提交标记。

  • API 版本,它与 API 网址直接对应,代表请求参数的形状、响应文档的形状和 API 行为。

本文还假设单次代码部署将在一个共同的代码版本中同时实现 API 的旧版本和新版本。例如,部署的主分支可能同时实现 /user-service/v1//user-service/v2/。在发布新的次要版本和补丁程序版本时,您可以通过这种方法在两个代码版本之间拆分流量,而无需考虑代码实际实现的 API 版本。

您的组织可以选择在不同的代码分支上开发 /user-service/v1//user-service/v2/;也就是说,任何一次代码部署都不会同时实现这两个版本。此模型也可以在 App Engine 上使用,但若要拆分流量,则需要在服务名称本身中包含主要 API 版本。例如,您的客户端将使用以下网址:

http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/
http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/

服务名称本身包含主要 API 版本,例如 user-service-v1user-service-v2。(路径中的 /v1//v2/ 部分在此模型中是多余的,可以移除,但它们在日志分析中仍然有用。)此模型需要更多工作,因为它可能需要更新部署脚本,以便在主要 API 版本更改中部署新服务。另外,请注意每个 App Engine 应用允许的最大服务数

重大更改与非重大更改对比

了解重大更改与非重大更改之间的区别非常重要。重大更改通常是减法性质的,这意味着它们会删除请求或响应文档的某些部分。更改文档的框架或更改键的名称可能造成重大更改。新增必需参数始终属于重大更改。如果微服务的行为更改,也可能引发重大更改。

非重大更改往往属于加法性质。新增可选请求参数或在响应文档中增加新部分都属于非重大更改。如需实现非重大更改,不停机序列化的选择至关重要。许多序列化都非常适合执行非重大更改:JSON、Protocol Buffers 或 Thrift。在反序列化时,这些序列化机制会静默地忽略额外的和意外的信息。在动态语言中,额外信息只出现在反序列化对象中。

考虑服务 /user-service/v1/ 的以下 JSON 定义:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

以下重大更改需要将服务版本重新设置为 /user-service/v2/

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

但是,以下非重大更改不需要新版本:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

部署新的非重大次要 API 版本

部署新的次要 API 版本时,App Engine 允许将新代码版本与旧代码版本并行发布。在 App Engine 中,虽然您可以直接为任何已部署的版本指定网址,但只有一个版本是默认提供的版本;请牢记每个服务都有一个默认提供的版本。在本示例中,我们有一个名为 apple 的旧代码版本,它恰好是默认的服务版本,我们要将名为 banana 的新代码版本部署为并行版本。请注意,两者的微服务网址都是 /user-service/v1/,因为我们部署的是非重大次要 API 更改。

App Engine 通过将新代码版本 banana 标记为默认的服务版本,提供了将流量从 apple 自动迁移到 banana 的机制。设置新的默认服务版本后,所有新请求都不会路由到 apple,而会路由到 banana。通过这种方式可以前滚到某个新代码版本,从而实现新的次要 API 版本或补丁程序 API 版本,而不会影响客户端微服务。

如果出现错误,则通过反向执行上述过程来实现回滚,即将默认的服务版本设置回旧版本,在我们的示例中为 apple。所有新请求都将路由回旧代码版本,而不会路由到 banana。请注意,系统会允许正在进行的请求完成。

App Engine 还能够仅将一定比例的流量定向到新代码版本;此过程通常称为 Canary 版发布流程,该机制在 App Engine 中称为流量拆分。您可以视需要为新代码版本定向 1%、10%、50% 或任何百分比的流量,并且可以随时间变化调整此数量。例如,您可以在 15 分钟内发布新的代码版本,缓慢增加流量,并观察是否出现任何可能确定需要回滚的问题。您还可以使用这一机制对两个代码版本进行 A/B 测试:将流量拆分设置为 50%,并比较两个代码版本的性能和错误率特征,以确认是否达到了预期的改进。

下图展示了 Google Cloud 控制台中的流量拆分设置:

Google Cloud 控制台中的流量拆分设置

部署新的重大主要 API 版本

部署重大主要 API 版本时,前滚和回滚的过程与非重大次要 API 版本相同。但是,您通常不会执行任何流量拆分或 A/B 测试,因为重大 API 版本是新发布的网址,例如 /user-service/v2/。当然,如果更改了旧主要 API 版本的基础实现,您可能仍需要使用流量拆分来测试旧的主要 API 版本是否继续按预期运行。

部署新的主要 API 版本时,请务必记住旧的主要 API 版本可能仍在提供服务。例如,/user-service/v1/ 可能在 /user-service/v2/ 发布后仍提供服务。这种情况是独立代码版本不可避免的。如需关停旧的主要 API 版本,您必须首先确认没有其他微服务需要它们(包括可能需要回滚到旧代码版本的其他微服务)。

举一个具体的例子:假设您有一个名为 web-app 的微服务,它依赖于另一个名为 user-service 的微服务。假设 user-service 需要更改一些基础实现(例如将 firstNamelastName 合并到名为 name 的单个字段中),导致无法支持 web-app 当前使用的旧主要 API 版本。也就是说,user-service 需要停用旧的主要 API 版本。

如需完成此更改,必须进行三次独立的部署:

  • 首先,user-service 必须部署 /user-service/v2/,同时仍支持 /user-service/v1/。此部署可能需要编写临时代码以支持向后兼容性,这是基于微服务的应用中常见的结果

  • 接下来,web-app 必须部署更新的代码,以将其依赖项从 /user-service/v1/ 更改为 /user-service/v2/

  • 最后,在 user-service 团队确认 web-app 不再需要 /user-service/v1/web-app 不需要回滚之后,团队可以部署用于移除旧 /user-service/v1/ 端点的代码以及支持它所需的任何临时代码。

虽然所有这些工作看起来都很繁琐,但这是基于微服务的应用中必不可少的过程,并且正是实现独立开发发布周期的过程。需要明确指出的是,虽然此过程看起来前后关联非常紧密,但重要的是,上述每个步骤都可以在独立的时间轴上进行,并且前滚和回滚都在单个微服务的范围内进行。只不过步骤的顺序是固定的,每个步骤可能需要数小时、数天甚至数周的时间来完成。

后续步骤