BLOG | NGINX

将 NGINX 部署为 API 网关,第 3 部分:发布 gRPC 服务

NGINX-Part-of-F5-horiz-black-type-RGB
Liam Crilly 缩略图
Liam Crilly
Published January 20, 2021

本文是“将 NGINX 开源版和 NGINX Plus 部署为 API 网关”系列博文的第三篇。

注:除非另有说明,否则本文中的所有信息都适用于 NGINX Plus 和 NGINX 开源版。为了便于阅读,当讨论内容同时适用于两个版本时,下文将它们统称为“NGINX”。

近年来,介绍微服务应用架构的概念和优势的文章非常多,其中以 NGINX 博文居首。微服务应用的核心是 HTTP API,本系列博文的前两篇文章使用了一个假设的 REST API 来说明 NGINX 如何处理此类应用。

尽管基于 JSON 消息格式的 REST API 在现代应用中非常流行,但它并不是所有场景或所有企业的理想之选。最常见的挑战是:

  • 文档标准 —— 如果没有良好的开发者制度或强制性的文档要求,最后很容易产生大量缺乏准确定义的 REST API。Open API 规范 已成为 REST API 的通用接口描述语言,但其使用却不是强制性的,需要开发组织内部的有力治理。
  • 事件和长连接 —— REST API 以及它们使用 HTTP 传输,几乎决定了所有 API 调用都是请求 – 响应模式。当客户端应用需要服务器反馈消息时,使用 HTTP 长轮询和 WebSocket 等解决方案会有所帮助,但使用此类解决方案最终都需要构建一个单独、相邻的 API。
  • 复杂事务 —— REST API 是围绕唯一资源的概念构建的,每个资源都由一个 URI 表示。当应用需要调用多个资源更新时,要么需要多个 API 调用(效率低下),要么必须在后端实现复杂的事务(与 REST 的核心原则相悖)。

近年来,gRPC 已发展成为构建分布式应用,尤其是微服务应用的替代方法。gRPC 最初由 Google 开发,并于 2015 年开源,现已成为云原生计算基金会的一个项目。值得注意的是,gRPC 使用 HTTP/2 作为传输机制,并利用其二进制数据格式和多路复用流功能。

gRPC 的主要优势包括:

  • 紧耦合的接口定义语言(协议缓冲区
  • 对流数据的原生支持(双向)
  • 高效的二进制数据格式
  • 自动生成多语言的代码,支持真正的多语言开发环境,且不会产生互操作性问题

 

定义 gRPC 网关

本系列博文的前两篇描述了如何通过单个入口点(例如 https://api.example.com)交付多个 API。当 NGINX 部署为 gRPC 网关时,gRPC 流量的默认行为和特征促使 NGINX 也要采用这种方法。虽然 NGINX 可以在同一主机名和端口上共享 HTTP 和 gRPC 流量,但最好还是将它们分开,主要有以下原因有:

  • REST 和 gRPC 应用的 API 客户端需要不同格式的错误响应
  • REST 和 gRPC 访问日志的相关字段有所不同
  • 因为 gRPC 不涉及旧版 Web 浏览器,因此它可以实施更严格的 TLS 策略

为了实现这种分离,我们需要修改 gRPC 网关主配置文件 grpc_gateway.confserver{} 模块,它位于 /etc/nginx/conf.d 目录。

我们首先定义 gRPC 流量访问日志中的条目格式(第 1-4 行)。在本例中,我们使用 JSON 格式从每个请求中捕获最相关的数据。请注意,HTTP method 不包括在内,因为所有 gRPC 请求都使用 POST。我们还记录了 gRPC 状态代码和 HTTP 状态代码。然而,gRPC 状态代码可通过不同的方式生成。在正常情况下,grpc-status 从后端返回 HTTP/2 消息头,但在一些错误情况下,它可能会被后端或 NGINX 自己返回 HTTP/2 消息头。为了简化访问日志,我们使用 map 块(第 6-9 行)来评估新变量 $grpc_status 并从产生该变量的地方获取 gRPC 状态。

此配置包含两个监听指令(第 12 行和第 13 行),所以我们可以测试明文(端口 50051)和受 TLS 保护的(端口 443)流量。http2 参数将 NGINX 配置为接受 HTTP/2 连接 —— 请注意,这与 ssl 参数无关。另请注意,端口 50051 是 gRPC 的常规明文端口,但不推荐在生产环境中使用。

TLS 配置是常规配置,但 ssl_protocols 指令(第 23 行)除外,该指令将 TLS 1.2 指定为最弱的可接受协议。HTTP/2 规范要求使用 TLS 1.2(或更高版本),以保证所有客户端都支持对 TLS 的 SNI (Server Name Indication) 扩展。这意味着 gRPC 网关可以与其他 server{} 模块中定义的虚拟服务器共享端口 443。

 

运行示例 gRPC 服务

为了解 NGINX 的 gRPC 功能,我们使用了一个简单的测试环境,该环境代表了 gRPC 网关的关键组件,并部署了多个 gRPC 服务。我们使用官方 gRPC 指南中的两个示例应用: helloworld (用 Go 编写)和 RouteGuide(用 Python 编写)。RouteGuide 应用特别有用,因为它包含了四种 gRPC 服务方法:

  • 简单 RPC(单一请求 – 响应)
  • 响应流 RPC
  • 请求流 RPC
  • 双向流 RPC

所有 gRPC 服务都作为 Docker 容器安装在我们的 NGINX 主机上。有关构建该测试环境的完整说明,请参阅附录

NGINX 作为 gRPC 网关的测试环境

我们配置 NGINX 以了解 RouteGuide 和 helloworld service,以及可用容器的地址。

我们为每个 gRPC 服务添加一个 upstream 模块(第 40-45 和 47-51 行),并使用运行 gRPC 服务器代码的各个容器的地址填充它们。

路由 gRPC 请求

通过 NGINX 监听 gRPC 的常规明文端口 (50051) ,我们将路由信息添加到配置中,以便客户端请求能够到达正确的后端 service 。但首先我们需要了解 gRPC method 调用如何表示为 HTTP/2 请求。下图为 RouteGuide service 的 route_guide.proto 文件的缩略版,说明了 package、service 和 RPC method 如何形成 URI,如 NGINX 所见。

协议缓冲区 RPC method 如何转换为 HTTP/2 请求

因此,HTTP/2 请求中携带的信息只需匹配包名(此处为 routeguidehelloworld)即可用于路由。

第一个 location 模块(第 26 行),不包含任何修饰符,定义了一个前缀匹配,以便 /routeguide.匹配该包对应的 .proto 文件中定义的所有 service 和 RPC method。因此,grpc_pass 指令(第 27 行)将来自 RouteGuide 客户端的所有请求传递给上游 group routeguide_service。该配置(以及第 29 行和第 30 行的 helloworld 服务的并行配置)提供了 gRPC 包与其后端 service 之间的简单映射。

请注意,grpc_pass 指令的参数以 grpc://方式请求,该请求方式使用明文 gRPC 连接代理请求。如果后端配置了 TLS,我们可以使用 grpcs:// 通过端到端加密来保护 gRPC 连接。

运行 RouteGuide 客户端后,我们可以通过查看日志文件来确认路由行为。此处,我们看到 RouteChat RPC method 被路由到在端口 10002 上运行的容器。

$ python route_guide_client.py...
$ tail -1 /var/log/nginx/grpc_log.json | jq
{
  "timestamp": "2021-01-20T12:17:56+01:00",
  "client": "127.0.0.1",
  "uri": "/routeguide.RouteGuide/RouteChat",
  "http-status": 200,
  "grpc-status": 0,
  "upstream": "127.0.0.1:10002",
  "rx-bytes": 161,
  "tx-bytes": 212
}

精确路由

如上所示,将多个 gRPC 服务简单、高效的路由到不同后端,只需要少数几行配置。然而,生产环境中的路由要求可能更加复杂,需要基于 URI 中的其他元素(gRPC 服务甚至单个 RPC method)进行路由。

以下配置片段扩展了前面的示例,以便将双向流式 RPC method RouteChat 路由到同一个后端,而将其他所有 RouteGuide 方法路由到不同的后端。

第二个 location 指令(第 7 行)使用 “=”(等号)来表示这是 RouteChat RPC method 的 URI 上的精确匹配。精确匹配在前缀匹配之前进行处理,这意味着 RouteChat URI 不会考虑其他 location 块。

 

响应错误

gRPC 错误与传统 HTTP 流量的错误有些不同。客户端期望错误条件表示为 gRPC 响应,这使得当 NGINX 配置为 gRPC 网关时,默认的 NGINX 错误页面集(HTML 格式)将不适合使用。我们的解决方法是为 gRPC 客户端指定一组自定义的错误响应。

完整的 gRPC 错误响应集是一个相对较长且大部分是静态响应的配置,因此我们将它们保存在一个单独的文件 errors.grpc_conf 中,并使用 include 指令(第 34 行)引用它们。与 HTTP/REST 客户端不同,gRPC 客户端应用不需要处理大量的 HTTP 状态代码。gRPC 文档指定了 NGINX 等中间代理必须如何将 HTTP 错误代码转换为 gRPC 状态代码,以便客户端始终能够接收到合适的响应。我们使用 error_page 指令来执行这个映射。

每个标准 HTTP 状态代码都使用 @ 前缀传递到指定 location,这样就可以生成符合 gRPC 要求的响应。例如,HTTP 404 响应在内部被重定向到 @grpc_unimplemented location,该 location 文件定义如下:

@grpc_unimplemented 命名 location 仅可用于内部 NGINX 处理 —— 由于没有可路由的 URI,客户端无法直接请求该 location。在 location 中,我们填充强制性 gRPC 标头并使用 HTTP 状态代码 204 (No Content) 发送它们(不包含响应正文),从而构造 gRPC 响应。

我们可以使用 curl(1) 命令模拟一个行为不端的 gRPC 客户端去请求一个不存在的 gRPC method。但是请注意,由于协议缓冲区使用二进制数据格式,curl 通常不适合作为 gRPC 测试客户端。要在命令行上测试 gRPC,可考虑使用 grpc_cli

$ curl -i --http2 -H "Content-Type: application/grpc" -H "TE: trailers" -X POST https://grpc.example.com/does.Not/ExistHTTP/2 204 
server: nginx/1.19.5
date: Wed, 20 Jan 2021 15:03:41 GMT
grpc-status: 12
grpc-message: unimplemented

上面引用的 grpc_errors.conf 文件还包含 NGINX 可能生成的其他错误响应的 HTTP 到 gRPC 状态代码映射,例如超时和客户端证书错误。

 

使用 gRPC 元数据验证客户端

gRPC 元数据允许客户端在 RPC method 调用的同时发送附加信息,而无需将这些数据作为协议缓冲区规范文件(.proto 文件)的一部分。元数据是一个简单的键值对(key-value)列表,每个键值对都作为单独的 HTTP/2 标头传输。因此,NGINX 访问元数据非常容易。

在元数据的众多用例中,客户端身份验证对 gRPC API 网关来说是最常见的。以下配置片段显示了 NGINX Plus 如何使用 gRPC 元数据执行 JWT 身份验证(JWT 身份验证是 NGINX Plus 的独有功能)。在此示例中,JWT 在 auth-token 元数据中发送。

对 NGINX Plus 来说,每个 HTTP 请求标头都可作为一个名为 $http_header 的变量来使用。标头名称中的连字符 (-) 转换为变量名称中的下划线 ( _ ),因此 JWT 可用作 $http_auth_token (第 2 行)。

如果 API 密钥用于身份验证(可能是现有的 HTTP/REST API),那么这些密钥也可以在 gRPC 元数据中携带,并由 NGINX 验证。本博客系列的第 1 部分提供了 API 密钥身份验证<.htmla>的配置。

 

实施健康检查

当对多个后台服务器进行负载均衡时,一定要避免将请求发送到已关闭或不可用的后台服务器。借助 NGINX Plus,我们可以使用主动健康检查主动向后台服务器发送带外请求,并在它们未按预期响应健康检查时将其从负载均衡轮换中移除。通过这种方式,我们可以确保客户端请求永远不会被传输到停止服务的后台服务器。

以下配置片段为 RouteGuide 和 helloworld gRPCservice 启用了主动健康检查;为了突出显示相关配置,该片段省略了一些指令,这些指令包含在前面几节中使用的 grpc_gateway.conf 文件中。

对于每个路由,我们现在还指定 health_check 指令(第 17 和 21 行)。正如 type=grpc 参数所指定的,NGINX Plus 使用 gRPC 健康检查协议向上游 group 中的每个服务器发送健康检查。但是,我们简单的 gRPC 服务没有实现 gRPC 健康检查协议,因此我们希望它们使用表示“unimplemented”(grpc_status=12) 的状态代码进行响应。当它们使用这种状态代码进行响应时,就足以表明我们正在与一个活动的 gRPC 服务进行通信。

有了这个配置,我们可以关闭任何后端容器,且 gRPC 客户端不会出现延迟或超时。主动健康检查是 NGINX Plus 的独有功能;有关 gRPC 健康检查的更多信息,请阅读我们的博客。

 

应用速率限制和其他 API 网关控制

grpc_gateway.conf 中的示例配置适合生产环境使用,其中对 TLS 进行了一些小的修改。基于package、 service 或 RPC method 路由 gRPC 请求的能力表明现有的 NGINX 功能可以以HTTP/REST API 或常规 Web 流量完全相同的方式应用于 gRPC 流量。在每种情况下,相关的 location 模块都可以通过进一步的配置(例如速率限制或带宽控制)进行扩展。

 

总结

在关于将 NGINX 开源版和 NGINX Plus 部署为 API 网关系列博文的第三篇也是最后一篇博文中,我们重点介绍了将 gRPC 作为构建微服务应用的云原生技术。我们展示了 NGINX 如何能够像交付 HTTP/REST API 一样有效地交付 gRPC 应用,以及如何通过 NGINX 作为多用途 API 网关发布这两种 API。

有关本文使用的测试环境的说明位于下面的附录中,您可以从我们的 GitHub Gist 存储库中下载所有文件。

查看本系列博文的其他文章:

  • 第 1 部分解释了如何在一些基于 HTTP 的基本的 API 网关用例中配置 NGINX。
  • 第 2 部分探讨了保护后端服务免受恶意或不良客户端攻击的更高级用例。

如欲试用作为 API 网关 的 NGINX Plus ,请立即下载 30 天免费试用版 ,或与我们联系以讨论您的用例。在试用期间,您可以使用位于我们的 GitHub Gist 存储库的完整配置文件集。


附录:设置测试环境

以下说明将测试环境安装在一个虚拟机上,方便隔离和重复使用。当然也如果有条件也可以安装在物理服务器上。

为了简化测试环境,我们使用 Docker 容器来运行 gRPC 服务。这么做的的好处是我们不需要在测试环境中使用多个主机,但仍然可以像在生产环境中一样,让 NGINX 通过网络调用建立代理连接。

Docker 还支持我们在不同的端口上运行每个 gRPC 服务的多个实例,而无需修改代码。每个 gRPC 服务监听容器内的端口 50051,该端口映射到虚拟机上唯一的 localhost 端口。这反过来释放了端口 50051,NGINX 可以将其用作监听端口。因此,当测试客户端使用其预配置的端口 50051 连接时,它们会连接到 NGINX。

安装 NGINX 开源版或 NGINX Plus

  1. 根据 NGINX Plus 管理员指南中的说明安装 NGINX 开源版NGINX Plus

  2. 将以下文件从 GitHub Gist 存储库复制到 /etc/nginx/conf.d:

    • grpc_gateway.conf
    • errors.grpc_conf

    注意:如果未使用 TLS,则注释掉 grpc_gateway.conf 中的 ssl_*指令。

  3. 3.启动 NGINX 开源版或 NGINX Plus。

    $ sudo nginx

安装 Docker

对于 Debian 和 Ubuntu,运行:

$ sudo apt-get install docker.io

对于 CentOS、RHEL 和 Oracle Linux,运行:

$ sudo yum install docker

安装 RouteGuide 服务容器

  1. 通过以下 Dockerfile 为 RouteGuide 容器构建 Docker 镜像。

    您可以在构建之前将 Dockerfile 复制到本地子目录,也可以将 Dockerfile 的 Gist 的 URL 指定为 docker build 命令的参数:

    $ sudo docker build -t routeguide https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/routeguide.Dockerfile

    下载和构建镜像可能需要几分钟时间。出现消息 Successfully built 和一个十六进制字符串(image ID)即表示构建完成。

  2. 确认镜像是通过运行 docker images 构建的。

    $ sudo docker images
    REPOSITORY     TAG          IMAGE ID          CREATED         SIZE
    routeguide     latest       63058a1cf8ca      1 minute ago    1.31 GB
    python         latest       825141134528      9 days ago      923 MB
  3. 启动 RouteGuide 容器。

    $ sudo docker run --name rg1 -p 10001:50051 -d routeguide$ sudo docker run --name rg2 -p 10002:50051 -d routeguide
    $ sudo docker run --name rg3 -p 10003:50051 -d routeguide

    每个命令执行成功时,都会出现一个长的十六进制字符串,代表正在运行的容器。

  4. 运行 docker ps,检查三个容器是否都已启动。(为了便于阅读,我们将示例输出拆分成了多行。)

    $ sudo docker psCONTAINER ID  IMAGE       COMMAND              STATUS        ...
    d0cdaaeddf0f  routeguide  "python route_g..."  Up 2 seconds  ...
    c04996ca3469  routeguide  "python route_g..."  Up 9 seconds  ...
    2170ddb62898  routeguide  "python route_g..."  Up 1 minute   ...
    
          ... PORTS                     NAMES
          ... 0.0.0.0:10003->50051/tcp  rg3
          ... 0.0.0.0:10002->50051/tcp  rg2
          ... 0.0.0.0:10001->50051/tcp  rg1

    输出中的 PORTS 列显示了每个容器如何将不同的本地端口映射到容器内的端口 50051。

安装 helloworld Service 容器

  1. 通过以下 Dockerfile 为 helloworld 容器构建 Docker 镜像。

    您可以在构建之前将 Dockerfile 复制到本地子目录,也可以将 Dockerfile 的 Gist 的 URL 指定为 docker build 命令的参数:

    $ sudo docker build -t helloworld https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/helloworld.Dockerfile

    下载和构建镜像可能需要几分钟时间。出现消息 Successfully built 和一个十六进制字符串(image ID)即表示构建完成。

  2. 确认镜像是通过运行 docker images 构建的。

    $ sudo docker images
    REPOSITORY     TAG          IMAGE ID          CREATED           SIZE
    helloworld     latest       e5832dc0884a      10 seconds ago    926MB
    routeguide     latest       170761fa3f03      4 minutes ago     1.31GB
    python         latest       825141134528      9 days ago        923MB
    golang         latest       d0e7a411e3da      3 weeks ago       794MB
  3. 启动 helloworld 容器。

    $ sudo docker run --name hw1 -p 20001:50051 -d helloworld$ sudo docker run --name hw2 -p 20002:50051 -d helloworld

    每个命令执行成功时,都会出现一个长的十六进制字符串,代表正在运行的容器。

  4. 运行 docker ps,检查两个 helloworld 容器是否都已启动。

    $ sudo docker psCONTAINER ID  IMAGE       COMMAND              STATUS        ... 
    e0d204ae860a  helloworld  "go run greeter..."  Up 5 seconds  ... 
    66f21d89be78  helloworld  "go run greeter..."  Up 9 seconds  ... 
    d0cdaaeddf0f  routeguide  "python route_g..."  Up 4 minutes  ... 
    c04996ca3469  routeguide  "python route_g..."  Up 4 minutes  ... 
    2170ddb62898  routeguide  "python route_g..."  Up 5 minutes  ... 
    
          ... PORTS                     NAMES
          ... 0.0.0.0:20002->50051/tcp  hw2
          ... 0.0.0.0:20001->50051/tcp  hw1
          ... 0.0.0.0:10003->50051/tcp  rg3
          ... 0.0.0.0:10002->50051/tcp  rg2
          ... 0.0.0.0:10001->50051/tcp  rg1

安装 gRPC 客户端应用

  1. 安装编程语言的先决条件,其中一些可能已安装在测试环境中。

    • 对于 Ubuntu 和 Debian,运行:

      $ sudo apt-get install golang-go python3 python-pip git
    • 对于 CentOS、RHEL 和 Oracle Linux,运行:

      $ sudo yum install golang python python-pip git

    请注意,python-pip 需要启用 EPEL 存储库(根据需要先运行 sudo yum install epel-release)。

  2. 下载 helloworld 应用:

    $ go get google.golang.org/grpc
  3. 下载 RouteGuide 应用:

    $ git clone -b v1.14.1 https://github.com/grpc/grpc
    $ pip install grpcio-tools

测试设置

  1. 运行 helloworld 客户端:

    $ go run go/src/google.golang.org/grpc/examples/helloworld/greeter_client/main.go
  2. 运行 RouteGuide 客户端:

    $ cd grpc/examples/python/route_guide
    $ python route_guide_client.py
  3. 检查 NGINX 日志,确认测试环境可正常运行:

    $ tail /var/log/nginx/grpc_log.json

"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."