这是我们关于将NGINX Open Source和NGINX Plus部署为API网关的系列博客文章中的第3篇。
- 第 1 部分详细介绍了将 NGINX Open Source 和 NGINX Plus 用作基于 HTTP 的 RESTful API 网关的多种用例。
- 第 2 部分在这些用例的基础上进一步扩展,并介绍可用于保护生产环境中后端 API 服务并确保其安全的一系列防护措施。
- 本文介绍如何将NGINX Open Source和NGINX Plus部署为gRPC服务的API网关。本文最初发布于2018年,现已更新,以利用NGINX Plus第23版中新增的原生gRPC健康检查协议支持。请参阅实施健康检查。
注意:除非另有说明,本文中的所有信息均适用于NGINX Plus和NGINX Open Source。为便于阅读,本文后续在相关内容同时适用于这两个版本时,将统一简称为“NGINX”。
近年来,微服务应用架构的概念和优势已有大量资料介绍,其中尤以NGINX博客最为详尽。HTTP API是微服务应用的核心,本系列的前两篇博文通过一个假设的REST API,说明NGINX如何支持此类应用。
尽管采用JSON消息格式的REST API在现代应用中广受欢迎,但这种方法并非适用于所有场景或组织。最常见的挑战包括:
- 文档标准:如果开发人员缺乏良好的规范意识,或组织未强制要求编写文档,就很容易出现多个缺少准确定义的REST API。Open API Specification已成为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。鉴于gRPC流量的默认行为和特性,在将NGINX部署为gRPC网关时,我们也采用相同的方法。虽然HTTP和gRPC流量可以共用同一主机名和端口,但出于以下几个原因,最好将二者分开:
- REST和gRPC应用的API客户端所需的错误响应格式不同
- REST与gRPC的访问日志所涉及的字段有所不同
- 由于gRPC无需处理旧版Web浏览器,因此可以采用更严格的TLS策略
为实现这种分离,我们将gRPC网关的配置放在主gRPC配置文件中单独的server{}块内。该主配置文件为grpc_gateway.conf,位于/etc/nginx/conf.d目录中。
正在加载代码示例…
首先,我们定义gRPC流量的访问日志条目格式(第1–4行)。在此示例中,我们使用JSON格式来记录每个请求中最相关的数据。例如,由于所有gRPC请求都使用POST,因此不包含HTTP方法。除HTTP状态码外,我们还会记录gRPC状态码。不过,gRPC状态码可能以不同方式生成。正常情况下,grpc-status由后端以HTTP/2尾部字段的形式返回;但在某些错误情况下,它可能由后端或NGINX本身以HTTP/2标头的形式返回。为简化访问日志,我们使用map块(第6–9行)计算新变量$grpc_status,无论gRPC状态来自何处,都能获取该状态。
此配置包含两个listen指令(第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端口。
运行示例gRPC服务
为了探索NGINX的gRPC功能,我们使用了一个简单的测试环境。该环境包含gRPC网关的关键组件,并部署了多个gRPC服务。我们采用了gRPC官方指南中的两个示例应用:helloworld(使用Go编写)和RouteGuide(使用Python编写)。RouteGuide应用尤其有用,因为它涵盖了全部4种gRPC服务方法:
- 简单RPC(单次请求-响应)
- 响应流式RPC
- 请求流式 RPC
- 双向流式 RPC
这两项gRPC服务均以Docker容器的形式安装在NGINX主机上。有关构建测试环境的完整说明,请参阅附录。

我们对NGINX进行配置,使其能够识别RouteGuide和helloworld服务,以及可用容器的地址。
正在加载代码示例…
我们为每个gRPC服务添加一个“upstream”块(第40–45行和第47–51行),并在其中填入运行gRPC服务器代码的各个容器的地址。
路由gRPC请求
在NGINX侦听gRPC的常规明文端口(50051)后,我们在配置中添加路由信息,使客户端请求能够到达正确的后端服务。但首先需要了解gRPC方法调用如何表示为HTTP/2请求。下图显示了RouteGuide服务的route_guide.proto文件的简化版本,说明NGINX所看到的URI是如何由软件包、服务和RPC方法组成的。

因此,只需匹配包名称(此处为routeguide或helloworld),即可使用HTTP/2请求中携带的信息进行路由。
正在加载代码示例…
第一个location块(第 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
{
"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方法)进行路由。
以下配置片段扩展了上一个示例,将双向流式RPC方法RouteChat路由到一个后端,并将其他所有RouteGuide方法路由到另一个后端。
正在加载代码示例…
第2个location指令(第7行)使用=(等号)修饰符,表示与RouteChat RPC方法的URI完全匹配。完全匹配先于前缀匹配处理,因此不会再考虑其他location块是否与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状态码都会使用“@”前缀传递到命名location,以便生成符合gRPC规范的响应。例如,HTTP 404响应会在内部重定向到@grpc_unimplemented location,该location将在文件后文中定义:
正在加载代码示例…
命名位置“@grpc_unimplemented”仅供NGINX内部处理使用,客户端无法直接请求,因为不存在可路由的URI。在此位置中,我们通过填充并发送必需的gRPC标头来构造gRPC响应,不包含响应正文,并使用HTTP状态码“204”(“No Content”)。
可以使用curl(1)命令模拟行为异常的gRPC客户端请求不存在的gRPC方法。但请注意,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文件还包含HTTP到gRPC的状态码映射,用于NGINX可能生成的其他错误响应,例如超时和客户端证书错误。
使用gRPC元数据对客户端进行身份验证
gRPC 元数据允许客户端在调用RPC方法时一并发送附加信息,而无需将这些数据纳入协议缓冲区规范(.proto文件)。元数据是一个简单的键值对列表,每个键值对都作为单独的HTTP/2标头传输。因此,NGINX可以轻松访问元数据。
在元数据的众多用例中,客户端身份验证是gRPC API网关最常见的用例。以下配置片段展示了NGINX Plus如何使用gRPC元数据执行JWT身份验证(JWT身份验证是NGINX Plus的专属功能)。在此示例中,JWT通过auth-token元数据发送。
正在加载代码示例…
NGINX Plus可将每个HTTP请求标头作为名为“$http_header”的变量使用。标头名称中的连字符(-)会在变量名称中转换为下划线(_),因此可通过“$http_auth_token”获取JWT(第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健康检查的更多信息,请参阅我们的博客。
应用速率限制及其他API网关控制措施
对grpc_gateway.conf中的示例配置稍作TLS相关修改后,即可用于生产环境。由于可以根据包、服务或RPC方法路由gRPC请求,因此现有NGINX功能可应用于gRPC流量,方式与应用于HTTP/REST API或常规Web流量完全相同。对于每种情况,都可以在相关的location块中添加更多配置,例如速率限制或带宽控制。
总结
在本系列关于将NGINX Open Source和NGINX Plus部署为API网关的第3篇(也是最后一篇)博客文章中,我们重点介绍了gRPC这一用于构建微服务应用的云原生技术。我们展示了NGINX如何像交付HTTP/REST API一样高效地交付gRPC应用,以及如何通过NGINX这一多用途API网关发布这两类API。
本博文所用测试环境的设置说明见下方的“附录”,所有文件均可从我们的“GitHub Gist代码库”下载。
查看本系列的其他博文:
要试用NGINX Plus作为API网关,请立即开始30天免费试用,或联系我们,讨论您的用例。试用期间,请使用我们GitHub Gist代码库中的完整配置文件集。
附录:设置测试环境
以下说明介绍如何在虚拟机上安装测试环境,以确保环境相互隔离且可重复构建。不过,也可以将其安装在物理“裸机”服务器上。
为简化测试环境,我们使用Docker容器运行gRPC服务。这样,测试环境无需使用多台主机,同时仍可让NGINX像在生产环境中一样,通过网络调用建立代理连接。
使用Docker还可以在不同端口上运行每个gRPC服务的多个实例,而无需更改代码。每个gRPC服务都在容器内侦听端口50051,该端口会映射到虚拟机上唯一的localhost端口。这样便会释放端口50051,供NGINX用作侦听端口。因此,当测试客户端使用预配置的端口50051进行连接时,会连接到NGINX。
安装NGINX Open Source或NGINX Plus
- 按照NGINX Plus管理指南中的说明安装NGINX Open Source或NGINX Plus。
- 将以下文件从GitHub Gist代码库复制到/etc/nginx/conf.d:注意:如果不使用TLS,请注释掉
ssl_*指令(位于grpc_gateway.conf中)。- grpc_gateway.conf
- errors.grpc_conf
- 启动NGINX Open Source或NGINX Plus。
$ sudo nginx安装 Docker
对于Debian和Ubuntu,请运行:
$ sudo apt-get install docker.io对于CentOS、RHEL和Oracle Linux,请运行:
$ sudo yum install docker安装RouteGuide服务容器
- 使用以下Dockerfile为RouteGuide容器构建Docker镜像。
正在加载代码示例…
可以在构建前将Dockerfile复制到本地子目录,也可以将该Dockerfile的Gist URL作为参数传递给dockerbuild命令:
$ sudo docker build -t routeguide https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/routeguide.Dockerfile- 下载并构建镜像可能需要几分钟。出现消息“
Successfullybuilt”和一个十六进制字符串(镜像 ID),表示构建已完成。 - 运行
dockerimages,确认镜像已构建。
$ sudo docker imagesREPOSITORY TAG IMAGE ID CREATED SIZE
routeguide latest 63058a1cf8ca 1 minute ago 1.31 GB
python latest 825141134528 9 days ago 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- 每条命令成功执行后,都会显示一个较长的十六进制字符串,表示正在运行的容器。
- 运行
dockerps,检查所有3个容器是否均已启动。(为便于阅读,示例输出分为多行显示。)
$ 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 服务容器
- 使用以下Dockerfile为helloworld容器构建Docker镜像。
正在加载代码示例…
可以在构建前将Dockerfile复制到本地子目录,也可以将该Dockerfile的Gist URL作为参数传递给dockerbuild命令:
$ sudo docker build -t helloworld https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/helloworld.Dockerfile- 下载并构建镜像可能需要几分钟。出现消息“
Successfullybuilt”和一个十六进制字符串(镜像ID)即表示构建完成。 - 运行
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- 启动helloworld容器。
$ sudo docker run --name hw1 -p 20001:50051 -d helloworld$ sudo docker run --name hw2 -p 20002:50051 -d helloworld- 每条命令成功执行后,都会显示一个长十六进制字符串,表示正在运行的容器。
- 运行
dockerps,检查两个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客户端应用程序
- 安装编程语言所需的必备组件,其中一些可能已安装在测试环境中。请注意,
python-pip要求启用EPEL存储库(如有必要,请先运行sudoyuminstallepel-release)。- 对于Ubuntu和Debian,请运行:
$ sudo apt-get install golang-go python3 python-pip git- 对于CentOS、RHEL和Oracle Linux,请运行:
$ sudo yum install golang python python-pip git- 下载helloworld应用程序:
$ go get google.golang.org/grpc- 下载RouteGuide应用程序:
$ git clone -b v1.14.1 https://github.com/grpc/grpc
$ pip install grpcio-tools测试设置
- 运行helloworld客户端:
$ go run 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关于作者

相关博客文章
NGINX Plus R26 正式发布
NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。
使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。
Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。
NGINX Plus R19 正式发布
NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。
NGINX现已正式加入F5
NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。
