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

NGINX | 2021年1月20日

这是我们关于将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作为gRPC网关的测试环境

我们对NGINX进行配置,使其能够识别RouteGuide和helloworld服务,以及可用容器的地址。

正在加载代码示例…

我们为每个gRPC服务添加一个“upstream”块(第40–45行和第47–51行),并在其中填入运行gRPC服务器代码的各个容器的地址。

路由gRPC请求

在NGINX侦听gRPC的常规明文端口(50051)后,我们在配置中添加路由信息,使客户端请求能够到达正确的后端服务。但首先需要了解gRPC方法调用如何表示为HTTP/2请求。下图显示了RouteGuide服务的route_guide.proto文件的简化版本,说明NGINX所看到的URI是如何由软件包、服务和RPC方法组成的。

Protocol Buffer RPC方法如何转换为HTTP/2请求

因此,只需匹配包名称(此处为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代码库”下载。

查看本系列的其他博文:

  • 第 1 部分介绍了如何针对一些基本的基于 HTTP 的 API 网关用例配置 NGINX。
  • 第 2 部分探讨了更高级的用例,介绍如何保护后端服务免受恶意客户端或行为异常客户端的影响。

要试用NGINX Plus作为API网关,请立即开始30天免费试用,或联系我们,讨论您的用例。试用期间,请使用我们GitHub Gist代码库中的完整配置文件集。

附录:设置测试环境

以下说明介绍如何在虚拟机上安装测试环境,以确保环境相互隔离且可重复构建。不过,也可以将其安装在物理“裸机”服务器上。

为简化测试环境,我们使用Docker容器运行gRPC服务。这样,测试环境无需使用多台主机,同时仍可让NGINX像在生产环境中一样,通过网络调用建立代理连接。

使用Docker还可以在不同端口上运行每个gRPC服务的多个实例,而无需更改代码。每个gRPC服务都在容器内侦听端口50051,该端口会映射到虚拟机上唯一的localhost端口。这样便会释放端口50051,供NGINX用作侦听端口。因此,当测试客户端使用预配置的端口50051进行连接时,会连接到NGINX。

安装NGINX Open Source或NGINX Plus

  1. 按照NGINX Plus管理指南中的说明安装NGINX Open Source或NGINX Plus。
  2. 将以下文件从GitHub Gist代码库复制到/etc/nginx/conf.d:注意:如果不使用TLS,请注释掉ssl_*指令(位于grpc_gateway.conf中)。
    • grpc_gateway.conf
    • errors.grpc_conf
  3. 启动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服务容器

  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
  1. 下载并构建镜像可能需要几分钟。出现消息“Successfully built”和一个十六进制字符串(镜像 ID),表示构建已完成。
  2. 运行docker images,确认镜像已构建。
$ 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
  1. 启动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
  1. 每条命令成功执行后,都会显示一个较长的十六进制字符串,表示正在运行的容器。
  2. 运行docker ps,检查所有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
  1. 输出中的“PORTS”列显示了每个容器如何将不同的本地端口映射到容器内的端口 50051。

安装 helloworld 服务容器

  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
  1. 下载并构建镜像可能需要几分钟。出现消息“Successfully built”和一个十六进制字符串(镜像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
  1. 启动helloworld容器。
$ sudo docker run --name hw1 -p 20001:50051 -d helloworld$ sudo docker run --name hw2 -p 20002:50051 -d helloworld
  1. 每条命令成功执行后,都会显示一个长十六进制字符串,表示正在运行的容器。
  2. 运行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. 安装编程语言所需的必备组件,其中一些可能已安装在测试环境中。请注意,python-pip要求启用EPEL存储库(如有必要,请先运行sudo yum install epel-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
  1. 下载helloworld应用程序:
$ go get google.golang.org/grpc
  1. 下载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
  1. 运行RouteGuide客户端:
$ cd grpc/examples/python/route_guide
$ python route_guide_client.py
  1. 检查NGINX日志,确认测试环境运行正常:
$ tail /var/log/nginx/grpc_log.json


分享
标签: 2021, Tech, F5 NGINX

关于作者

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。