使用 Nginx 进行 gRPC 服务的负载均衡
到目前为止,我们已经学习了很多关于如何使用 gRPC 开发后端 Web 服务的知识。在部署方面,我们需要考虑的一个重要因素是负载均衡。
大规模 gRPC 部署通常包含多个相同的后端服务器和多个客户端。负载均衡用于将客户端的负载最优地分配到各个可用服务器上。
以下是YouTube 上完整的 gRPC 课程播放列表链接,以及
GitLab 仓库:pcbook-go和pcbook-java
目录
负载均衡的类型
gRPC负载均衡主要有两种方案:服务器端负载均衡和客户端负载均衡。选择使用哪一种方案是架构设计上的首要选择。
服务器端负载均衡
在服务器端负载均衡中,客户端向负载均衡器或代理(例如Nginx或)发出 RPC 请求Envoy。负载均衡器将 RPC 调用分发到可用的后端服务器之一。
它还会跟踪每台服务器的负载,并实现负载均衡的算法。客户端本身并不知道后端服务器的情况。
客户端负载均衡
在客户端负载均衡中,客户端知道有多个后端服务器,并为每个 RPC 请求选择一个服务器。通常,后端服务器会向服务发现基础设施(例如 Service Discovery 或 Service Discovery)注册自身Consul。Etcd然后,客户端与该基础设施通信以获取服务器地址。
胖客户端自行实现负载均衡算法。例如,在不考虑服务器负载的简单配置中,客户端可以直接在可用服务器之间轮询连接。
另一种方法是使用旁路负载均衡器,其中负载均衡的智能功能在一个专门的负载均衡服务器上实现。客户端查询旁路负载均衡器以获取最佳服务器。维护服务器状态、服务发现和负载均衡算法的实现等繁重工作都集中在旁路负载均衡器中。
优点和缺点
服务器端负载均衡的优势之一是客户端实现简单。客户端只需知道代理的地址,无需编写其他代码。这种方法即使对于不受信任的客户端也有效,这意味着 gRPC 服务可以面向公共互联网上的所有人开放。
然而,它的缺点是会增加一次额外的跃点。所有 RPC 请求都必须先经过代理才能到达后端服务器,从而导致更高的延迟。因此,这种服务器端负载均衡适用于有很多客户端(可能来自不可信的互联网客户端)想要连接到我们数据中心内 gRPC 服务器的情况。
另一方面,客户端负载均衡不会增加调用的额外跃点,因此通常能带来更高的性能。然而,客户端的实现会变得复杂,尤其是在采用胖客户端架构时。因此,它应该只用于受信任的客户端,或者我们需要使用旁路负载均衡器来保护信任边界网络。客户端负载均衡通常用于高流量系统和微服务架构。
在本文中,我们将学习如何使用 为我们的gRPC服务设置服务器端负载均衡Nginx。
代码重构
因为我将向您展示Nginx在服务器和客户端上启用或禁用 TLS 的不同配置,所以让我们稍微更新一下代码,以便接受一个新的命令行参数。
更新服务器
在服务器端,我们添加一个新的布尔标志enableTLS,它将告诉我们是否要在 gRPC 服务器上启用 TLS。它的默认值为 false false。
func main() {
port := flag.Int("port", 0, "the server port")
enableTLS := flag.Bool("tls", false, "enable SSL/TLS")
flag.Parse()
log.Printf("start server on port %d, TLS = %t", *port, *enableTLS)
...
}
然后,我们将拦截器提取到一个单独的serverOptions变量中。我们检查该enableTLS标志。只有当该标志启用时,我们才加载 TLS 凭据,并将这些凭据添加到服务器选项切片中。最后,我们将服务器选项传递给grpc.NewServer()函数调用。
func main() {
...
interceptor := service.NewAuthInterceptor(jwtManager, accessibleRoles())
serverOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(interceptor.Unary()),
grpc.StreamInterceptor(interceptor.Stream()),
}
if *enableTLS {
tlsCredentials, err := loadTLSCredentials()
if err != nil {
log.Fatal("cannot load TLS credentials: ", err)
}
serverOptions = append(serverOptions, grpc.Creds(tlsCredentials))
}
grpcServer := grpc.NewServer(serverOptions...)
...
}
服务器端就完成了。接下来我们对客户端也做类似的操作!
更新客户端
首先,我们将enableTLS标志添加到命令行参数中。然后,我们定义一个transportOption具有默认值的变量grpc.WithInsecure()。
func main() {
serverAddress := flag.String("address", "", "the server address")
enableTLS := flag.Bool("tls", false, "enable SSL/TLS")
flag.Parse()
log.Printf("dial server %s, TLS = %t", *serverAddress, *enableTLS)
transportOption := grpc.WithInsecure()
if *enableTLS {
tlsCredentials, err := loadTLSCredentials()
if err != nil {
log.Fatal("cannot load TLS credentials: ", err)
}
transportOption = grpc.WithTransportCredentials(tlsCredentials)
}
cc1, err := grpc.Dial(*serverAddress, transportOption)
if err != nil {
log.Fatal("cannot dial server: ", err)
}
authClient := client.NewAuthClient(cc1, username, password)
interceptor, err := client.NewAuthInterceptor(authClient, authMethods(), refreshDuration)
if err != nil {
log.Fatal("cannot create auth interceptor: ", err)
}
cc2, err := grpc.Dial(
*serverAddress,
transportOption,
grpc.WithUnaryInterceptor(interceptor.Unary()),
grpc.WithStreamInterceptor(interceptor.Stream()),
)
if err != nil {
log.Fatal("cannot dial server: ", err)
}
laptopClient := client.NewLaptopClient(cc2)
testRateLaptop(laptopClient)
}
只有当enableTLS标志值为真时true,我们才会从 PEM 文件加载 TLS 凭据并将其更改transportOption为真grpc.WithTransportCredentials(tlsCredentials)。最后,我们将凭据传递transportOption给 gRPC 连接。客户端就此完成。
测试新标志
现在如果我们运行命令make server,可以看到服务器在禁用 TLS 的情况下运行。
如果我们运行 make client 命令,它也是在不使用 TLS 的情况下运行的,并且所有 RPC 调用都成功了。
如果我们-tls在命令中添加标志make server并重新启动它,TLS 将被启用。
...
server:
go run cmd/server/main.go -port 8080 -tls
...
如果现在运行make client,请求将会失败:
我们还需要在客户端启用 TLS,方法是在命令-tls中添加标志make client。
...
client:
go run cmd/client/main.go -address 0.0.0.0:8080 -tls
...
现在我们可以看到请求再次成功了。
更新 Makefile
好了,现在 TLS 标志按预期工作了。在添加之前Nginx,我们先Makefile稍微更新一下代码,以便能够轻松地运行多个服务器和客户端实例,无论是否启用 TLS。
我将移除这些-tls标志,以便make server命令make client在不使用 TLS 的情况下运行。我还会添加两条 make 命令,在不同的端口上运行两个服务器实例。假设第一个服务器运行在端口 1 50051,第二个服务器运行在端口 2 50052。
...
server:
go run cmd/server/main.go -port 8080
client:
go run cmd/client/main.go -address 0.0.0.0:8080
server1:
go run cmd/server/main.go -port 50051
server2:
go run cmd/server/main.go -port 50052
...
我们再添加 3 个 make 命令来启动客户端和支持 TLS 的服务器。第一个client-tls命令将启动支持 TLS 的客户端。第二个make server1-tls命令将在端口 上启动一个 TLS 服务器50051,第三个make server2-tls命令将在端口 上启动另一个 TLS 服务器50052。
...
client-tls:
go run cmd/client/main.go -address 0.0.0.0:8080 -tls
server1-tls:
go run cmd/server/main.go -port 50051 -tls
server2-tls:
go run cmd/server/main.go -port 50052 -tls
...
安装 Nginx
接下来我们需要做的就是安装Nginx。因为我用的是Mac,所以我可以直接使用Homebrew:
❯ brew install nginx
安装完nginx后,我们可以进入这个usr/local/etc/nginx文件夹进行配置。让我们nginx.conf用Visual Studio Code打开这个文件。
❯ cd /usr/local/etc/nginx
❯ code nginx.conf
这是默认配置:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
include servers/*;
}
本教程中有些内容我们不需要关心,所以我们来更新这个配置文件。
配置 Nginx 以应对不安全的 gRPC
首先,我们删除用户配置,取消注释错误日志,删除日志级别和进程 ID 的配置,假设我们现在只需要 10 个工作连接。
我们需要做的一件重要事情是配置存储错误日志和访问日志文件的正确位置。就我而言,Homebrew 已经创建了一个日志文件夹Nginx,/usr/local/var/log/nginx所以我直接在错误/访问日志设置中使用它即可。
worker_processes 1;
error_log /usr/local/var/log/nginx/error.log;
events {
worker_connections 10;
}
http {
access_log /usr/local/var/log/nginx/access.log;
server {
listen 8080 http2;
location / {
}
}
}
现在在服务器代码块中,我们有一个listen命令用于监听来自客户端端口的请求8080。这是普通 HTTP 服务器的默认配置。由于 gRPC 使用,我们需要在该命令的末尾HTTP/2添加。http2
让我们移除服务器名称和字符集,因为我们现在不需要它们。访问日志也一样,因为我们已经在前面定义过了。此外,让我们删除默认根 HTML 文件的配置以及该location代码块之后的所有内容,因为我们暂时不需要它们。
好的,现在我们要对两个服务器实例的传入请求进行负载均衡。所以我们需要upstream为它们定义一个负载均衡器。我把它叫做 `<server_instance>` upstream pcbook_services。在这个代码块中,我们使用 ` server<server_instance>` 关键字来声明一个服务器实例。第一个实例运行在localhost端口 1 50051,第二个实例运行在端口 2 50052。
worker_processes 1;
error_log /usr/local/var/log/nginx/error.log;
events {
worker_connections 10;
}
http {
access_log /usr/local/var/log/nginx/access.log;
upstream pcbook_services {
server 0.0.0.0:50051;
server 0.0.0.0:50052;
}
server {
listen 8080 http2;
location / {
grpc_pass grpc://pcbook_services;
}
}
}
然后,为了将所有 RPC 调用路由到上游,在location代码块中,我们使用grpc_pass关键字,后跟grpc://方案和上游的名称,即pcbook_services。
就这样!我们不安全的gRPC服务器的负载均衡就完成了。
让我们在终端运行 nginx 来启动它。
❯ nginx
❯ ps aux | grep nginx
quangpham 9013 0.0 0.0 4408572 800 s000 S+ 6:13PM 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox nginx
quangpham 9007 0.0 0.0 4562704 1124 ?? S 6:12PM 0:00.00 nginx: worker process
quangpham 9006 0.0 0.0 4422416 612 ?? Ss 6:12PM 0:00.00 nginx: master process nginx
ps我们可以使用 ` and`命令来检查它是否正在运行grep。让我们查看一下日志文件夹:
❯ cd /usr/local/var/log/nginx
❯ ls -l
total 0
-rw-r--r-- 1 quangpham admin 0 Oct 11 18:12 access.log
-rw-r--r-- 1 quangpham admin 0 Oct 11 18:12 error.log
如您所见,系统生成了 2 个日志文件:access.log和error.log。目前它们是空的,因为我们还没有发送任何请求。
现在我们运行命令make server1启动第一个服务器,端口号50051为TLS = false。然后在另一个标签页中运行命令make server2启动第二个服务器,端口号为50052,同样禁用 TLS。最后,我们make client在另一个新标签页中运行命令。
看起来不错。所有 RPC 调用都成功了。我们来检查一下服务器日志。
接收server2到 2 个创建笔记本电脑的请求。
系统server1收到 1 个登录请求和 1 个创建笔记本电脑请求。太棒了!
过了一会儿,服务器又收到了另一个登录请求。这是因为我们的客户端仍在运行,它会定期调用登录函数来刷新令牌。
希望你们还记得我们在gRPC 拦截器讲座中编写的代码。
好的,现在我们来看一下nginx访问日志文件。
你可以看到,首先是一个登录调用,然后是 3 个创建笔记本电脑的调用,最后又是一个登录调用。所以一切都按预期运行。
接下来我将向您展示如何SSL/TLS启用Nginx。
配置 Nginx 以使用 TLS 进行 gRPC 通信。
在典型的部署中,gRPC 服务器已经在受信任的网络内运行,只有负载均衡器(Nginx在本例中)暴露在公共互联网上。因此,我们可以让 gRPC 服务器TLS像以前一样运行,只需添加TLS即可Nginx。
在 Nginx 上启用 TLS,但保持 gRPC 服务器不安全
为此,我们需要将 3 个 pem 文件复制到 nginx 配置文件夹中:
- 服务器的证书
- 服务器的私钥
- 如果使用双向 TLS,则还需要签署客户端证书的 CA 证书。
好的,现在我要cd进入该/usr/local/etc/nginx文件夹并创建一个新cert文件夹。然后我会将pcbook项目中的这3个pem文件复制到这个文件夹中。
❯ cd /usr/local/etc/nginx
❯ mkdir cert
❯ cp ~/Projects/techschool/pcbook-go/cert/server-cert.pem cert
❯ cp ~/Projects/techschool/pcbook-go/cert/server-key.pem cert
❯ cp ~/Projects/techschool/pcbook-go/cert/ca-cert.pem cert
好了,现在所有证书和密钥文件都已准备就绪。让我们回到nginx配置文件。
要启用 TLS,我们首先需要ssl在listen命令中添加相应参数。然后,我们使用该ssl_certificate命令指定Nginx服务器证书文件的位置。最后,我们使用另一个ssl_certificate_key命令指定服务器私钥文件的位置。
...
server {
listen 8080 ssl http2;
ssl_certificate cert/server-cert.pem;
ssl_certificate_key cert/server-key.pem;
ssl_client_certificate cert/ca-cert.pem;
ssl_verify_client on;
location / {
grpc_pass grpc://pcbook_services;
}
}
...
由于我们使用的是双向 TLS,因此还需要使用命令ssl_client_certificate告知 nginx 客户端 CA 证书文件的位置。最后,我们设置ssl_verify_clientnginxon以验证客户端发送的证书的真实性。
好了,完成了。现在重启nginx。我们nginx -s stop先运行命令停止它,然后再用nginx命令启动它。
❯ nginx -s stop
❯ nginx
我们的服务器已经运行了,现在让我们运行客户端吧!
如果我们直接运行make client,它将在没有 TLS 的情况下运行,因此请求将失败,因为Nginx现在正在启用 TLS 的情况下运行。
现在我们开始跑吧make client-tls。
这次客户端启用了 TLS,所有请求都成功了。
请注意,我们的服务器目前仍在未启用 TLS 的情况下运行。因此,实际情况是:只有客户端与服务器之间的连接Nginx是安全的,而客户端Nginx与后端服务器之间的连接则是通过另一个不安全的连接进行的。
一旦Nginx收到客户端发送的加密数据,服务器会先解密数据,然后再将其转发到后端服务器。因此,只有当服务器Nginx和后端服务器位于同一可信网络中时,才应使用此方法。
好的,但如果它们不在同一个可信网络中呢?那么,在这种情况下,我们别无选择,只能在后端服务器上也启用 TLS,并配置 nginx 以使其与之兼容。
在 Nginx 和 gRPC 服务器上启用 TLS
让我们停止当前的连接server1,server2然后使用 TLS 重新启动它们。
❯ make server1-tls
❯ make server2-tls
如果我们现在make client-tls立即运行,请求将会失败。
原因是,虽然客户端与后端服务器之间的 TLS 握手Nginx成功,但客户端与后端服务器之间的 TLS 握手Nginx失败,因为后端服务器现在期望的是安全的 TLS 连接,而Nginx客户端在连接到后端服务器时仍然使用不安全的连接。
从错误日志中可以看到,故障发生在Nginx与上游服务器通信时。
为了在 nginx 和上游之间启用安全的 TLS 连接nginx.conf,我们需要在文件grpc中将协议更改为grpcs。
...
server {
...
location / {
grpc_pass grpcs://pcbook_services;
}
}
...
如果我们只使用服务器端 TLS,这应该就足够了。但是,在这种情况下,我们使用的是双向 TLS,所以如果我们Nginx现在重启并重新运行make client-tls,请求仍然会失败,因为Nginx尚未配置为将证书发送到上游服务器。
bad certificate正如您在日志中看到的,我们遇到了错误。
让我们看看如果转到服务器代码cmd/server/main.go并将ClientAuth字段从tls.RequireAndVerifyClientCert更改为会发生什么tls.NoClientCert,这意味着我们将只使用服务器端 TLS。
func loadTLSCredentials() (credentials.TransportCredentials, error) {
...
// Create the credentials and return it
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert,
ClientCAs: certPool,
}
return credentials.NewTLS(config), nil
}
然后重启server1-tls并再次server2-tls运行make client-tls。
❯ make server1-tls
❯ make server2-tls
❯ make client-tls
这次所有请求都成功了。这正是我们所预期的!
好的,如果我们真的需要在nginx和上游服务器之间建立双向TLS连接该怎么办?
让我们把字段改ClientAuth回原样tls.RequireAndVerifyClientCert,重启 2 个 TLS 后端服务器,然后回到我们的nginx.conf文件。
这次,我们需要指示Nginx后端服务器进行双向 TLS 连接,方法是提供证书和私钥的位置。我们使用grpc_ssl_certificate关键字指定证书,使用grpc_ssl_certificate_key关键字指定私钥。
您可以根据需要生成不同的证书和私钥对Nginx。这里我直接使用了服务器上的同一对证书和私钥。
worker_processes 1;
error_log /usr/local/var/log/nginx/error.log;
events {
worker_connections 10;
}
http {
access_log /usr/local/var/log/nginx/access.log;
upstream pcbook_services {
server 0.0.0.0:50051;
server 0.0.0.0:50052;
}
server {
listen 8080 ssl http2;
# Mutual TLS between gRPC client and nginx
ssl_certificate cert/server-cert.pem;
ssl_certificate_key cert/server-key.pem;
ssl_client_certificate cert/ca-cert.pem;
ssl_verify_client on;
location / {
grpc_pass grpcs://pcbook_services;
# Mutual TLS between nginx and gRPC server
grpc_ssl_certificate cert/server-cert.pem;
grpc_ssl_certificate_key cert/server-key.pem;
}
}
}
好的,我们来试试。
首先停止当前的nginx进程,然后启动一个新的进程。make client-tls再次运行。
❯ nginx -s stop
❯ nginx
❯ make client-tls
这次所有请求都成功了。完美!
多个路由位置
在结束之前,我还有一件事想给你们看。
正如您所见,登录和创建笔记本电脑的请求现在均匀地分配到我们的两个后端服务器上。但有时,我们可能需要将身份验证服务和业务逻辑服务分开。
例如,假设我们希望所有登录请求都发送到服务器 1,所有其他请求都发送到服务器 2。在这种情况下,我们还可以指示服务器Nginx根据请求路径来路由请求。
worker_processes 1;
error_log /usr/local/var/log/nginx/error.log;
events {
worker_connections 10;
}
http {
access_log /usr/local/var/log/nginx/access.log;
upstream auth_services {
server 0.0.0.0:50051;
server 0.0.0.0:50052;
}
upstream laptop_services {
server 0.0.0.0:50051;
server 0.0.0.0:50052;
}
server {
listen 8080 ssl http2;
# Mutual TLS between gRPC client and nginx
ssl_certificate cert/server-cert.pem;
ssl_certificate_key cert/server-key.pem;
ssl_client_certificate cert/ca-cert.pem;
ssl_verify_client on;
location /techschool.pcbook.AuthService {
grpc_pass grpcs://auth_services;
# Mutual TLS between nginx and gRPC server
grpc_ssl_certificate cert/server-cert.pem;
grpc_ssl_certificate_key cert/server-key.pem;
}
location /techschool.pcbook.LaptopService {
grpc_pass grpcs://laptop_services;
# Mutual TLS between nginx and gRPC server
grpc_ssl_certificate cert/server-cert.pem;
grpc_ssl_certificate_key cert/server-key.pem;
}
}
}
我在这里复制路径/techschool.pcbook.AuthService并将AuthService其粘贴到此位置。然后我将上游名称更改为auth_services。它应该只连接到server1端口50051。
然后我为该上游添加了另一个服务器laptop_services,并使其仅连接到server2端口50052。然后复制 location 块,将上游名称更改为laptop_services,并将路径更新为techschool.pcbook.LaptopService。
好的,我们来试试!只需要重启Nginx并运行make client-tls。
现在我们可以看到只有登录请求发送到了server1。
所有其他创建笔记本电脑的请求都会发送到这里server2。即使我们多次运行此 make client-tls 命令也是如此。
所以它奏效了!关于使用 gRPC 进行负载均衡的讲解就到此结束了Nginx。
我将把这个 nginx 配置文件推送到pcbook-go 仓库,这样如果你喜欢的话,就可以下载并试用它。
非常感谢您的阅读和学习。祝您编程愉快,我们下期再见!
如果您喜欢这篇文章,请订阅我们的 YouTube 频道并在 Twitter 上关注我们,以便将来获取更多教程。
如果你想加入我在Voodoo的优秀团队,请点击此处查看我们的招聘信息。可远程办公,也可在巴黎/阿姆斯特丹/伦敦/柏林/巴塞罗那现场办公,公司提供签证担保。
文章来源:https://dev.to/techschoolguru/load-balancing-grpc-service-with-nginx-3fio






















