本文是“将 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 策略
server{}
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;
POSTgrpc-status$grpc_status
http2ssl
server{}

运行示例 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,以及可用容器的地址。

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 服务添加一个 模块(第 40-45 和 47-51 行),并使用运行 gRPC 服务器代码的各个容器的地址填充它们。

路由 gRPC 请求

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

协议缓冲区 RPC method 如何转换为 HTTP/2 请求
routeguidehelloworld
# Routing
    location /routeguide. {
        grpc_pass grpc://routeguide_service;
    }
    location /helloworld. {
        grpc_pass grpc://helloworld_service;
    }
/routeguide.
grpc_passgrpc://grpcs://

运行 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)进行路由。

RouteChatRouteGuide
# 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=RouteChatRouteChatlocation

响应错误

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 中,并使用 指令(第 34 行)引用它们。与 HTTP/REST 客户端不同,gRPC 客户端应用不需要处理大量的 HTTP 状态代码。gRPC 文档指定了 NGINX 等中间代理必须如何将 HTTP 错误代码转换为 gRPC 状态代码,以便客户端始终能够接收到合适的响应。我们使用 指令来执行这个映射。

# 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;
@404@grpc_unimplemented
location @grpc_unimplemented {
    add_header grpc-status 12;
    add_header grpc-message unimplemented;
    return 204;
}
@grpc_unimplemented204NoContent
curl
$ 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 元数据验证客户端

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

auth-token
location /routeguide. {
    auth_jwt realm=routeguide token=$http_auth_token;
    auth_jwt_key_file my_idp.jwk;
    grpc_pass grpc://routeguide_service;
}
$http_header-_$http_auth_token

如果 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
    }
}
type=grpcgrpc_status=12

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

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

location

总结

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

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

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


附录:设置测试环境

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

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

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

安装 NGINX 开源版或 NGINX Plus

  • grpc_gateway.conf
  • errors.grpc_conf
ssl_*

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 镜像。
# 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"]
dockerbuild
$ sudo docker build -t routeguide https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/routeguide.Dockerfile
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
dockerps
$ 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

安装 helloworld Service 容器

  1. 通过以下 Dockerfile 为 helloworld 容器构建 Docker 镜像。
# 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"]
dockerbuild
$ sudo docker build -t helloworld https://gist.githubusercontent.com/nginx-gists/87ed942d4ee9f7e7ebb2ccf757ed90be/raw/ce090f92f3bbcb5a94bbf8ded4d597cd47b43cbe/helloworld.Dockerfil下载和构建镜像可能需要几分钟时间。出现消息 Successfully built 和一个十六进制字符串(image ID)即表示构建完成。
Successfullybuilt
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

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

dockerps
$ 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

安装 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-pipsudoyuminstallepel-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

更多资源

想要更及时全面地获取 NGINX 相关的技术干货、互动问答、系列课程、活动资源?

请前往 NGINX 开源社区: