微服务性能最佳做法

区域 ID

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

详细了解区域 ID

软件开发需要权衡利弊,微服务也不例外。 在代码部署和操作方面实现独立性,性能开销就需要有所牺牲。本部分提供了一些建议,帮助您采取相关措施,最大限度地减轻这一影响。

将 CRUD 操作转换为微服务

微服务特别适合使用创建、检索、更新、删除 (CRUD) 模式访问的实体。使用此类实体时,您通常一次只使用一个实体(例如用户),并且通常一次只执行一项 CRUD 操作。因此,您只需调用一个微服务即可完成操作。查找具有 CRUD 操作的实体以及可在应用的许多部分中使用的一组业务方法。这些实体可作为微服务的理想候选对象。

提供批处理 API

除了 CRUD 形式的 API 之外,您还可以通过提供批量 API 来为实体组提供良好的微服务性能。例如,您可以提供一个 API 来接受一组用户 ID 并返回一个包含相应用户的字典,而不是仅公开用于检索单个用户的 GET API 方法:

请求

/user-service/v1/?userId=ABC123&userId=DEF456&userId=GHI789

响应

{
  "ABC123": {
    "userId": "ABC123",
    "firstName": "Jake",
    … },
  "DEF456": {
    "userId": "DEF456",
    "firstName": "Sue",
    … },
  "GHI789": {
    "userId": "GHI789",
    "firstName": "Ted",
    … }
}

App Engine SDK 支持许多批处理 API(例如通过单个 RPC 从 Cloud Datastore 提取许多实体的功能),因此提供这类批处理 API 服务的效率很高。

使用异步请求

通常,您需要与许多微服务交互才能编写响应。 例如,您可能需要提取已登录用户的偏好设置以及用户的公司详细信息。通常,这些信息并不相互依赖,而且可以并行提取。App Engine SDK 中的 Urlfetch 库支持异步请求,允许您并行调用微服务。

以下 Python 示例代码直接使用 RPC 来采用异步请求

from google.appengine.api import urlfetch

preferences_rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(preferences_rpc,
                         'https://preferences-service-dot-my-app.uc.r.appspot.com/preferences-service/v1/?userId=ABC123')

company_rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(company_rpc,
                         'https://company-service-dot-my-app.uc.r.appspot.com/company-service/v3/?companyId=ACME')

 ### microservice requests are now occurring in parallel

try:
  preferences_response = preferences_rpc.get_result()  # blocks until response
  if preferences_response.status_code == 200:
    # deserialize JSON, or whatever is appropriate
  else:
    # handle error
except urlfetch.DownloadError:
  # timeout, or other transient error

try:
  company_response = company_rpc.get_result()  # blocks until response
  if company_response.status_code == 200:
    # deserialize JSON, or whatever is appropriate
  else:
    # handle error
except urlfetch.DownloadError:
  # timeout, or other transient error

并行操作往往与良好的代码结构背道而驰,这是因为在现实环境中,您通常使用一个类来封装偏好设置方法,并使用另一个类封装公司方法。很难既利用异步 Urlfetch 调用,而又不破坏此封装。App Engine Python SDK 的 NDB 包提供了一个很好的解决方案:Tasklet。Tasklet 使您能够在代码中保持良好的封装,同时仍提供实现并行微服务调用的机制。请注意,Tasklet 使用 Future 而不是 RPC,但想法是类似的。

使用最短的路由

根据调用 Urlfetch 的方式,您可以让系统使用不同的基础架构和路由。为了使用性能最佳的路由,请考虑以下建议:

使用 REGION_ID.r.appspot.com,而不是自定义网域
在通过 Google 基础架构路由时,自定义网域会导致使用不同的路由。由于微服务调用在内部进行,因此使用 https://PROJECT_ID.REGION_ID.r.appspot.com 可以轻松执行操作而且性能更佳。
follow_redirects 设置为 False
调用 Urlfetch 时明确设置 follow_redirects=False,以避免使用旨在遵循重定向且权重较高的服务。您的 API 端点应该不需要重定向客户端,因为它们是您自己的微服务,并且端点应该只返回 HTTP 200、400 和 500 系列响应。
最好使用单个项目中的多项服务,而不是使用多个项目
在构建基于微服务的应用时,您有充分的理由使用多个项目,但如果性能是您的主要目标,则请使用单个项目中的多项服务。一个项目中的所有服务均托管在同一数据中心,即使 Google 数据中心之间的互联网络具有极高的吞吐量,也是本地调用的速度更快。

避免在强制执行安全机制期间使用 Chatter

如果使用涉及大量来回通信的安全机制对发起调用的 API 进行身份验证,则会对性能产生不利的影响。例如,如果您的微服务需要通过回调应用来验证来自应用的票证,那么您需要进行多次往返才能获取数据。

OAuth2 实现可以通过在 Urlfetch 调用之间使用刷新令牌以及缓存访问令牌来分摊此费用。不过,如果缓存的访问令牌存储在 Memcache 中,则提取该令牌时会产生 Memcache 开销。为了避免这种开销,您可以将访问令牌缓存在实例内存中,但是您仍会频繁遇到 OAuth2 活动,因为每个新实例都会协商访问令牌;请记住,App Engine 实例会经常启动和关闭。混合使用 Memcache 和实例缓存有助于缓解此问题,但您的解决方案会开始变得更加复杂。

另一种效果出色的方法是在微服务之间共享密钥令牌,例如,将密钥令牌作为自定义 HTTP 标头传送。在此方法中,每项微服务都可以为每个调用者提供唯一的令牌。 通常,对于安全性的实施而言,共享密钥并不是一种可靠的方案,但由于所有微服务都位于同一应用中,再考虑到可以实现的性能提升,因此这个问题并不重要。借助共享密钥,微服务只需要对传入密钥与推测的内存中字典执行字符串比较,安全方面的工作量很少。

如果您的所有微服务都在 App Engine 上,您还可以检查传入的 X-Appengine-Inbound-Appid 标头。 在向另一个 App Engine 项目发出请求时,Urlfetch 基础架构会添加此标头,并且外部方无法设置此标头。根据您的安全要求,您的微服务可以检查此传入标头,以强制执行安全政策。

跟踪微服务请求

在您构建基于微服务的应用时,会因为连续的 Urlfetch 调用而开始积累开销。发生这种情况时,您可以使用 Cloud Trace 了解正在进行的调用以及开销来源。重要的是,Cloud Trace 还可以帮助识别以串行方式调用独立微服务的位置,这样您便可以重构代码以并行执行这些提取。

当您在单个项目中使用多项服务时,Cloud Trace 的一项实用功能就会派上用场。当某个项目中的各项微服务之间发生调用时,Cloud Trace 会将所有这些调用合并到一个调用图中,以单一跟踪的形式直观呈现整个的端到端请求。

Google Cloud Trace 屏幕截图

请注意,在上面的示例中,由于是使用异步 Urlfetch 并行执行 pref-serviceuser-service 调用,因此图表中显示的 RPC 有些杂乱。 不过,这仍然是一种诊断延迟的有用工具。

后续步骤