这是我们关于部署 NGINX 开源和 NGINX Plus 作为 API 网关的系列博文的第三篇。
笔记: 除非另有说明,本帖中的所有信息都适用于 NGINX Plus 和 NGINX Open Source。 为了方便阅读,当讨论适用于两个版本时,博客的其余部分仅提及“NGINX”。
近年来,微服务应用架构的概念和优势已得到充分证明,尤其是在NGINX 博客上。 微服务应用的核心是 HTTP API,本系列的前两篇博文使用假设的 REST API 来说明 NGINX 如何解决这种风格的应用。
尽管对于现代应用来说,采用 JSON 消息格式的 REST API 非常流行,但它并不是每种场景或每个组织的理想方法。 最常见的挑战是:
近年来,gRPC 已成为构建分布式应用(尤其是微服务应用)的替代方法。 gRPC 最初由 Google 开发,于 2015 年开源,现在是云原生计算基金会的一个项目。 值得注意的是,gRPC 使用 HTTP/2 作为其传输机制,利用其二进制数据格式和多路复用流功能。
gRPC 的主要优点是:
本系列的前两篇文章描述了如何通过单个入口点(例如https://api.example.com )传递多个 API。 gRPC 流量的默认行为和特性使得我们在将 NGINX 部署为 gRPC 网关时采用相同的方法。 虽然可以在同一主机名和端口上共享 HTTP 和 gRPC 流量,但出于多种原因,最好将它们分开:
为了实现这种分离,我们将 gRPC 网关的配置放在主 gRPC 配置文件grpc_gateway.conf中其自己的server{}
块中,该文件位于/etc/nginx/conf.d目录中。
我们首先定义 gRPC 流量访问日志中条目的格式(第 1-4 行)。 在这个例子中,我们使用 JSON 格式从每个请求中捕获最相关的数据。 例如,请注意,HTTP 方法未包含在内,因为所有 gRPC 请求都使用POST
。 我们还将 gRPC 状态代码与 HTTP 状态代码一起记录。 但是,gRPC 状态代码可以通过不同的方式生成。 在正常情况下, grpc-status
作为 HTTP/2 尾部从后端返回,但对于某些错误情况,它可能会作为 HTTP/2 标头返回,由后端或 NGINX 本身返回。 为了简化访问日志,我们使用一个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)扩展。 这意味着 gRPC 网关可以与其他server{}
块中定义的虚拟服务器共享端口 443。
为了探索 NGINX 的 gRPC 功能,我们使用了一个简单的测试环境,该环境代表 gRPC 网关的关键组件,并部署了多个 gRPC 服务。 我们使用来自官方 gRPC 指南的两个示例应用: helloworld (用 Go 编写)和RouteGuide (用 Python 编写)。 RouteGuide应用特别有用,因为它包含四种 gRPC 服务方法:
这两个 gRPC 服务都作为 Docker 容器安装在我们的 NGINX 主机上。 有关构建测试环境的完整说明,请参阅附录。
我们配置 NGINX 以了解 RouteGuide 和 helloworld 服务以及可用容器的地址。
我们为每个 gRPC 服务添加一个上游
块(第 40-45 行和第 47-51 行),并用运行 gRPC 服务器代码的各个容器的地址填充它们。
通过 NGINX 监听 gRPC 的常规纯文本端口(50051),我们在配置中添加路由信息,以便客户端请求到达正确的后端服务。 但首先我们需要了解 gRPC 方法调用如何表示为 HTTP/2 请求。 下图显示了 RouteGuide 服务的route_guide.proto文件的缩写版本,说明了包、服务和 RPC 方法如何形成 NGINX 所看到的 URI。
因此,只需匹配包名称(此处为routeguide
或helloworld
),HTTP/2 请求中携带的信息便可用于路由目的。
第一个位置
块(第 26 行)没有任何修饰符,定义了一个前缀匹配,使得/routeguide.
匹配该包相应.proto文件中定义的所有服务和 RPC 方法。 因此, grpc_pass
指令(第 27 行)将来自 RouteGuide 客户端的所有请求传递到上游组routeguide_service 。 此配置(以及第 29 行和第 30 行的 helloworld 服务的并行配置)提供了 gRPC 包与其后端服务之间的简单映射。
请注意, grpc_pass
指令的参数以grpc://
方案开头,它使用纯文本 gRPC 连接代理请求。 如果后端配置了 TLS,我们可以使用grpcs://
方案通过端到端加密来保护 gRPC 连接。
运行 RouteGuide 客户端后,我们可以通过查看日志文件条目来确认路由行为。 这里我们看到 RouteChat RPC 方法被路由到在端口 10002 上运行的容器。
$ python route_guide_client.py ...$ tail -1 /var/log/nginx/grpc_log.json | jq { “时间戳”: “2021-01-20T12:17:56 + 01:00”,“客户端”: “127.0.0.1”,“uri”:“/routeguide.RouteGuide/RouteChat”,“http状态”: 200,“grpc状态”: 0,“上游”: “127.0.0.1:10002”, “接收字节数”: 161,"发送字节数": 212 }
如上所示,将多个 gRPC 服务路由到不同的后端简单、高效,并且只需要很少的几行配置。 但是,生产环境中的路由要求可能更复杂,需要基于 URI 中的其他元素(gRPC 服务甚至单个 RPC 方法)进行路由。
以下配置代码片段扩展了前面的示例,以便双向流式 RPC 方法RouteChat
被路由到一个后端,而所有其他RouteGuide
方法被路由到不同的后端。
第二个位置
指令(第 7 行)使用=
(等号)修饰符来指示这是RouteChat
RPC 方法的 URI 的完全匹配。 精确匹配在前缀匹配之前处理,这意味着不会考虑RouteChat
URI 的其他位置
块。
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 状态代码都使用@
前缀传递到命名位置,以便可以生成符合 gRPC 的响应。 例如,HTTP404
响应在内部重定向到@grpc_unimplemented
位置,该位置在文件后面定义:
@grpc_unimplemented
命名位置仅可用于内部 NGINX 处理 - 客户端无法直接请求它,因为不存在可路由的 URI。 在此位置,我们通过填充强制性 gRPC 标头并使用 HTTP 状态代码发送它们(不带响应主体)来构建 gRPC 响应204
(无内容
) 。
我们可以使用curl(1)
命令来模拟行为不良的 gRPC 客户端请求不存在的 gRPC 方法。 但请注意, curl
通常不适合作为 gRPC 测试客户端,因为协议缓冲区使用二进制数据格式。 要在命令行上测试 gRPC,请考虑使用grpc_cli
。
$ curl -i --http2 -H "Content-Type: 应用/grpc" -H "TE: trailers" -X POST https://grpc.example.com/does.Not/Exist HTTP/2 204 服务器:nginx/1.19.5 日期: 2021 年 1 月 20 日星期三 15:03:41 GMT grpc-status: 12 grpc-消息:未实现
上面引用的grpc_errors.conf文件还包含 NGINX 可能生成的其他错误响应的 HTTP 到 gRPC 状态代码映射,例如超时和客户端证书错误。
gRPC元数据允许客户端在 RPC 方法调用的同时发送附加信息,而不需要该数据成为协议缓冲区规范( .proto文件)的一部分。 元数据是键值对的简单列表,每对键值对作为单独的 HTTP/2 标头传输。 因此 NGINX 可以轻松访问元数据。
在元数据的众多用例中,客户端身份验证对于 gRPC API 网关来说是最常见的。 以下配置片段展示了 NGINX Plus 如何使用 gRPC 元数据执行JWT 身份验证(JWT 身份验证是 NGINX Plus 独有的)。 在此示例中,JWT 在auth-token
元数据中发送。
每个 HTTP 请求标头都可以作为名为$http_ header
的变量提供给 NGINX Plus。 标头名称中的连字符 ( -
) 在变量名称中转换为下划线 ( _
),因此 JWT 可作为$http_auth_token
使用(第 2 行)。
如果使用 API 密钥进行身份验证(可能与现有的 HTTP/REST API 一起使用),那么这些密钥也可以在 gRPC 元数据中携带并由 NGINX 验证。本博客系列的第 1 部分提供了 API 密钥身份验证<.htmla>的配置。
当将流量负载均衡到多个后端时,重要的是避免将请求发送到已关闭或不可用的后端。 使用 NGINX Plus,我们可以使用主动健康检查来主动向后端发送带外请求,并在它们没有按预期响应健康检查时将其从负载均衡轮换中删除。 通过这种方式,我们可以确保客户端请求永远不会到达停止服务的后端。
以下配置片段为 RouteGuide 和 helloworld gRPC 服务启用主动健康检查;为了突出显示相关配置,它省略了前面部分使用的grpc_gateway.conf文件中包含的一些指令。
对于每条路线,我们现在还指定health_check
指令(第 17 和 21 行)。 按照type=grpc
参数指定,NGINX Plus使用gRPC健康检查协议向上游组中的每个服务器发送健康检查。 但是,我们的简单 gRPC 服务没有实现 gRPC 健康检查协议,因此我们希望它们使用表示“未实现”的状态代码进行响应( grpc_status=12
)。 当他们这样做时,这足以表明我们正在与活跃的 gRPC 服务进行通信。
有了这个配置,我们可以关闭任何后端容器,而不会出现 gRPC 客户端遇到延迟或超时的情况。 主动健康检查是 NGINX Plus 独有的;请在我们的博客上阅读有关gRPC 健康检查的更多信息。
grpc_gateway.conf中的示例配置适合生产使用,并对 TLS 进行了一些小修改。 根据包、服务或 RPC 方法路由 gRPC 请求的能力意味着现有的 NGINX 功能可以以与 HTTP/REST API 完全相同的方式应用于 gRPC 流量,或者实际上像常规 Web 流量一样。 在每种情况下,相关的位置
块都可以通过进一步的配置进行扩展,例如速率限制或带宽控制。
在本系列关于部署 NGINX 开源和 NGINX Plus 作为 API 网关的第三篇也是最后一篇博客文章中,我们重点介绍了 gRPC 作为构建微服务应用的云原生技术。 我们演示了 NGINX 如何能够像 HTTP/REST API 一样有效地提供 gRPC应用,以及如何通过 NGINX 将这两种风格的 API 发布为多用途 API 网关。
有关设置本博文中使用的测试环境的说明位于下面的附录中,您可以从我们的GitHub Gist 存储库下载所有文件。
查看本系列的其他博客文章:
要尝试 NGINX Plus 作为 API 网关,请立即开始30 天免费试用或联系我们讨论您的用例。 在试用期间,请使用我们GitHub Gist 存储库中的完整配置文件。
以下说明在虚拟机上安装测试环境,以便其隔离且可重复。 但是,没有理由不能将其安装在物理的“裸机”服务器上。
为了简化测试环境,我们使用 Docker 容器来运行 gRPC 服务。 这意味着我们不需要为测试环境设置多个主机,但仍可以让 NGINX 通过网络调用建立代理连接,就像在生产环境中一样。
使用 Docker 还允许我们在不同的端口上运行每个 gRPC 服务的多个实例,而无需更改代码。 每个 gRPC 服务监听容器内的端口 50051,该端口映射到虚拟机上唯一的本地主机端口。 这反过来释放端口 50051,以便 NGINX 可以将其用作其监听端口。 因此,当测试客户端使用其预配置的 50051 端口连接时,它们就会到达 NGINX。
根据 NGINX Plus 管理指南中的说明安装NGINX Open Source或NGINX Plus 。
将以下文件从GitHub Gist 存储库复制到/etc/nginx/conf.d :
笔记: 如果不使用 TLS,请注释掉grpc_gateway.conf中的ssl_*
指令。
启动 NGINX Open Source 或 NGINX Plus。
$ sudo nginx
对于 Debian 和 Ubuntu,运行:
$ sudo apt-get 安装 docker.io
对于 CentOS、RHEL 和 Oracle Linux,运行:
$ sudo yum 安装 docker
从以下 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
下载并构建图像可能需要几分钟。 消息“成功
构建”
和十六进制字符串(图像 ID)的出现表示构建完成。
通过运行docker
images
确认映像已构建。
$ sudo docker images存储库标签图像 ID 创建大小 routeguide 最新 63058a1cf8ca 1 分钟前 1.31 GB python 最新 825141134528 9 天前 923 MB
启动 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
随着每个命令的成功执行,会出现一个长的十六进制字符串,代表正在运行的容器。
运行docker
ps
检查所有三个容器是否启动。 (为了便于阅读,示例输出分为多行。)
$ sudo docker ps容器 ID 图像命令状态... d0cdaaeddf0f routeguide "python route_g..." 上升 2 秒... c04996ca3469 routeguide "python route_g..." 领先 9 秒......
2170ddb62898 routeguide“python route_g……” 最多1分钟...... 港口名称... 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。
从以下 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
下载并构建图像可能需要几分钟。 消息“成功
构建”
和十六进制字符串(图像 ID)的出现表示构建完成。
通过运行docker
images
确认映像已构建。
$ sudo docker images存储库标签图像 ID 创建大小 helloworld 最新 e5832dc0884a 10 秒前 926MB routeguide 最新 170761fa3f03 4 分钟前 1.31GB python 最新 825141134528 9 天前 923MB golang 最新 d0e7a411e3da 3 周前 794MB
启动 helloworld 容器。
$ sudo docker run --name hw1 -p 20001:50051 -d helloworld $ sudo docker run --name hw2 -p 20002:50051 -d helloworld
随着每个命令的成功执行,会出现一个长的十六进制字符串,代表正在运行的容器。
运行docker
ps
检查两个 helloworld 容器是否启动。
$ sudo docker ps容器ID 图像命令状态... e0d204ae860a helloworld“go rungreeter……” 最多 5 秒...
66f21d89be78 helloworld“去运行欢迎程序……” 最多 9 秒... d0cdaaeddf0f routeguide “python route_g...” 最多 4 分钟... c04996ca3469 routeguide "python route_g..." 最多4分钟...
2170ddb62898 routeguide“python route_g……” 最多5分钟…… 港口名称... 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
安装编程语言必备软件,其中一些可能已经安装在测试环境中。
对于 Ubuntu 和 Debian,运行:
$ sudo apt-get 安装 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
)。
下载 helloworld应用:
$获取 google.golang.org/grpc
下载 RouteGuide应用:
$ git clone -b v1.14.1 https://github.com/grpc/grpc $ pip install grpcio-tools
运行 helloworld 客户端:
$ go 运行 go/src/google.golang.org/grpc/examples/helloworld/greeter_client/main.go
运行 RouteGuide 客户端:
$ cd grpc/examples/python/route_guide $ python route_guide_client.py
检查 NGINX 日志以确认测试环境可以运行:
$ tail /var/log/nginx/grpc_log.json
“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”