原文作者:Liam Crilly of F5
原文链接:将 NGINX 部署为 API 网关,第 3 部分:发布 gRPC 服务
转载来源:NGINX 官方网站
本文是“将 NGINX 开源版和 NGINX Plus 部署为 API 网关”系列博文的第三篇。
注:除非另有说明,否则本文中的所有信息都适用于 NGINX Plus 和 NGINX 开源版。为了便于阅读,当讨论内容同时适用于两个版本时,下文将它们统称为“NGINX”。
近年来,介绍微服务应用架构的概念和优势的文章非常多,其中以 NGINX 博文居首。微服务应用的核心是 HTTP API,本系列博文的前两篇文章使用了一个假设的 REST API 来说明 NGINX 如何处理此类应用。
尽管基于 JSON 消息格式的 REST API 在现代应用中非常流行,但它并不是所有场景或所有企业的理想之选。最常见的挑战是:
近年来,gRPC 已发展成为构建分布式应用,尤其是微服务应用的替代方法。gRPC 最初由 Google 开发,并于 2015 年开源,现已成为云原生计算基金会的一个项目。值得注意的是,gRPC 使用 HTTP/2 作为传输机制,并利用其二进制数据格式和多路复用流功能。
gRPC 的主要优势包括:
本系列博文的前两篇描述了如何通过单个入口点(例如 https://api.example.com)交付多个 API。当 NGINX 部署为 gRPC 网关时,gRPC 流量的默认行为和特征促使 NGINX 也要采用这种方法。虽然 NGINX 可以在同一主机名和端口上共享 HTTP 和 gRPC 流量,但最好还是将它们分开,主要有以下原因有:
为了实现这种分离,我们需要修改gRPC 网关主配置文件 grpc_gateway.conf 的 server{}
模块,它位于 /etc/nginx/conf.d 目录。
log_format grpc_json escape=json '{"timestamp":"$time_iso8601",' '"client":"$remote_addr","uri":"$uri","http-status":$status,' '"grpc-status":$grpc_status,"upstream":"$upstream_addr"' '"rx-bytes":$request_length,"tx-bytes":$bytes_sent}'; map $upstream_trailer_grpc_status $grpc_status { default $upstream_trailer_grpc_status; # grpc-status is usually a trailer '' $sent_http_grpc_status; # Else use the header, whatever its source } server { listen 50051 http2; # In production, comment out to disable plaintext port listen 443 http2 ssl; server_name grpc.example.com; access_log /var/log/nginx/grpc_log.json grpc_json; # TLS config ssl_certificate /etc/ssl/certs/grpc.example.com.crt; ssl_certificate_key /etc/ssl/private/grpc.example.com.key; ssl_session_cache shared:SSL:10m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_protocols TLSv1.2 TLSv1.3;
我们首先定义 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。
为了解 NGINX 的 gRPC 功能,我们使用了一个简单的测试环境,该环境代表了 gRPC 网关的关键组件,并部署了多个 gRPC 服务。我们使用官方 gRPC 指南中的两个示例应用: helloworld (用 Go 编写)和 RouteGuide(用 Python 编写)。RouteGuide 应用特别有用,因为它包含了四种 gRPC 服务方法:
所有 gRPC 服务都作为 Docker 容器安装在我们的 NGINX 主机上。有关构建该测试环境的完整说明,请参阅附录。
NGINX 作为 gRPC 网关的测试环境
我们配置 NGINX 以了解 RouteGuide 和 helloworld service,以及可用容器的地址。
upstream routeguide_service {
zone routeguide_service 64k;
server 127.0.0.1:10001;
server 127.0.0.1:10002;
server 127.0.0.1:10003;
}
upstream helloworld_service {
zone helloworld_service 64k;
server 127.0.0.1:20001;
server 127.0.0.1:20002;
}
我们为每个 gRPC 服务添加一个 upstream
模块(第 40-45 和 47-51 行),并使用运行 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 请求中携带的信息只需匹配包名(此处为 routeguide
或 helloworld
)即可用于路由。
# Routing
location /routeguide. {
grpc_pass grpc://routeguide_service;
}
location /helloworld. {
grpc_pass grpc://helloworld_service;
}
第一个 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
方法路由到不同的后端。
# Service-level routing
location /routeguide.RouteGuide/ {
grpc_pass grpc://routeguide_service_default;
}
# Method-level routing
location = /routeguide.RouteGuide/RouteChat {
grpc_pass grpc://routeguide_service_streaming;
}
第二个 location
指令(第 7 行)使用 “=
”(等号)来表示这是 RouteChat
RPC method 的 URI 上的精确匹配。精确匹配在前缀匹配之前进行处理,这意味着 RouteChat
URI 不会考虑其他 location
块。
gRPC 错误与传统 HTTP 流量的错误有些不同。客户端期望错误条件表示为 gRPC 响应,这使得当 NGINX 配置为 gRPC 网关时,默认的 NGINX 错误页面集(HTML 格式)将不适合使用。我们的解决方法是为 gRPC 客户端指定一组自定义的错误响应。
# Error responses
include conf.d/errors.grpc_conf; # gRPC-compliant error responses
default_type application/grpc; # Ensure gRPC for all error responses
完整的 gRPC 错误响应集是一个相对较长且大部分是静态响应的配置,因此我们将它们保存在一个单独的文件 errors.grpc_conf 中,并使用 include
指令(第 34 行)引用它们。与 HTTP/REST 客户端不同,gRPC 客户端应用不需要处理大量的 HTTP 状态代码。gRPC 文档指定了 NGINX 等中间代理必须如何将 HTTP 错误代码转换为 gRPC 状态代码,以便客户端始终能够接收到合适的响应。我们使用 error_page
指令来执行这个映射。
# Standard HTTP-to-gRPC status code mappings
# Ref: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
#
error_page 400 = @grpc_internal;
error_page 401 = @grpc_unauthenticated;
error_page 403 = @grpc_permission_denied;
error_page 404 = @grpc_unimplemented;
error_page 429 = @grpc_unavailable;
error_page 502 = @grpc_unavailable;
error_page 503 = @grpc_unavailable;
error_page 504 = @grpc_unavailable;
每个标准 HTTP 状态代码都使用 @
前缀传递到指定 location,这样就可以生成符合 gRPC 要求的响应。例如,HTTP 404
响应在内部被重定向到 @grpc_unimplemented
location,该 location 文件定义如下:
location @grpc_unimplemented {
add_header grpc-status 12;
add_header grpc-message unimplemented;
return 204;
}
@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/Exist
HTTP/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 元数据 允许客户端在 RPC method 调用的同时发送附加信息,而无需将这些数据作为协议缓冲区规范文件(.proto 文件)的一部分。元数据是一个简单的键值对(key-value)列表,每个键值对都作为单独的 HTTP/2 标头传输。因此,NGINX 访问元数据非常容易。
在元数据的众多用例中,客户端身份验证对 gRPC API 网关来说是最常见的。以下配置片段显示了 NGINX Plus 如何使用 gRPC 元数据执行 JWT 身份验证(JWT 身份验证是 NGINX Plus 的独有功能)。在此示例中,JWT 在 auth-token
元数据中发送。
location /routeguide. {
auth_jwt realm=routeguide token=$http_auth_token;
auth_jwt_key_file my_idp.jwk;
grpc_pass grpc://routeguide_service;
}
对 NGINX Plus 来说,每个 HTTP 请求标头都可作为一个名为 $http_header
的变量来使用。标头名称中的连字符 (-
) 转换为变量名称中的下划线 ( _
),因此 JWT 可用作 $http_auth_token
(第 2 行)。
如果 API 密钥用于身份验证(可能是现有的 HTTP/REST API),那么这些密钥也可以在 gRPC 元数据中携带,并由 NGINX 验证。本博客系列的第 1 部分提供了 API 密钥身份验证的配置。
当对多个后台服务器进行负载均衡时,一定要避免将请求发送到已关闭或不可用的后台服务器。借助 NGINX Plus,我们可以使用主动健康检查主动向后台服务器发送带外请求,并在它们未按预期响应健康检查时将其从负载均衡轮换中移除。通过这种方式,我们可以确保客户端请求永远不会被传输到停止服务的后台服务器。
以下配置片段为 RouteGuide 和 helloworld gRPCservice 启用了主动健康检查;为了突出显示相关配置,该片段省略了一些指令,这些指令包含在前面几节中使用的 grpc_gateway.conf 文件中。
server {
listen 50051 http2; # Plaintext
# Routing
location /routeguide. {
grpc_pass grpc://routeguide_service;
health_check type=grpc grpc_status=12; # 12=unimplemented
}
location /helloworld. {
grpc_pass grpc://helloworld_service;
health_check type=grpc grpc_status=12; # 12=unimplemented
}
}
对于每个路由,我们现在还指定 health_check
指令(第 17 和 21 行)。正如 type=grpc
参数所指定的,NGINX Plus 使用 gRPC 健康检查协议向上游 group 中的每个服务器发送健康检查。但是,我们简单的 gRPC 服务没有实现 gRPC 健康检查协议,因此我们希望它们使用表示“unimplemented”(grpc_status=12
) 的状态代码进行响应。当它们使用这种状态代码进行响应时,就足以表明我们正在与一个活动的 gRPC 服务进行通信。
有了这个配置,我们可以关闭任何后端容器,且 gRPC 客户端不会出现延迟或超时。主动健康检查是 NGINX Plus 的独有功能;有关 gRPC 健康检查的更多信息,请阅读我们的博客。
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 存储库中下载所有文件。
查看本系列博文的其他文章:
如欲试用作为 API 网关 的 NGINX Plus ,请立即下载 30 天免费试用版 ,或与我们联系以讨论您的用例。在试用期间,您可以使用位于我们的 GitHub Gist 存储库的完整配置文件集。
以下说明将测试环境安装在一个虚拟机上,方便隔离和重复使用。当然也如果有条件也可以安装在物理服务器上。
为了简化测试环境,我们使用 Docker 容器来运行 gRPC 服务。这么做的的好处是我们不需要在测试环境中使用多个主机,但仍然可以像在生产环境中一样,让 NGINX 通过网络调用建立代理连接。
Docker 还支持我们在不同的端口上运行每个 gRPC 服务的多个实例,而无需修改代码。每个 gRPC 服务监听容器内的端口 50051,该端口映射到虚拟机上唯一的 localhost 端口。这反过来释放了端口 50051,NGINX 可以将其用作监听端口。因此,当测试客户端使用其预配置的端口 50051 连接时,它们会连接到 NGINX。
注意:如果未使用 TLS,则注释掉 grpc_gateway.conf 中的 ssl_*
指令。
3. 启动 NGINX 开源版或 NGINX Plus。
$ sudo nginx
对于 Debian 和 Ubuntu,运行:
$ sudo apt-get install docker.io
对于 CentOS、RHEL 和 Oracle Linux,运行:
$ sudo yum install docker
# This Dockerfile runs the RouteGuide server from
# https://grpc.io/docs/tutorials/basic/python.html
FROM python
RUN pip install grpcio-tools
RUN git clone -b v1.14.x https://github.com/grpc/grpc
WORKDIR grpc/examples/python/route_guide
EXPOSE 50051
CMD ["python", "route_guide_server.py"]
您可以在构建之前将 Dockerfile 复制到本地子目录,也可以将 Dockerfile 的 Gist 的 URL 指定为dockerbuild
命令的参数:
$ sudo docker build -t routeguide https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/routeguide.Dockerfile
2. 确认镜像是通过运行dockerimages
构建的。
$ 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 ps
CONTAINER 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。
# This Dockerfile runs the helloworld server from
# https://grpc.io/docs/quickstart/go.html
FROM golang
RUN go get -u google.golang.org/grpc
WORKDIR $GOPATH/src/google.golang.org/grpc/examples/helloworld
EXPOSE 50051
CMD ["go", "run", "greeter_server/main.go"]
您可以在构建之前将 Dockerfile 复制到本地子目录,也可以将 Dockerfile 的 Gist 的 URL 指定为dockerbuild
命令的参数:
$ sudo docker build -t helloworld https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/helloworld.Dockerfil下载和构建镜像可能需要几分钟时间。出现消息 Successfully built 和一个十六进制字符串(image ID)即表示构建完成。
下载和构建镜像可能需要几分钟时间。出现消息Successfullybuilt
和一个十六进制字符串(image ID)即表示构建完成。
2. 确认镜像是通过运行dockerimages
构建的。
$ 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. 运行dockerps
,检查两个 helloworld 容器是否都已启动。
$ sudo docker ps
CONTAINER 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
$ sudo apt-get install golang-go python3 python-pip git
$ sudo yum install golang python python-pip git
请注意,python-pip
需要启用 EPEL 存储库(根据需要先运行sudoyuminstallepel-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
$ 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
想要更及时全面地获取 NGINX 相关的技术干货、互动问答、系列课程、活动资源?
请前往 NGINX 开源社区:
|