本文介绍gRPC程序健康检查+Kubernetes部署+负载均衡

参考文档:

健康检查相关

我们都知道Kubernetes的健康检查(存活探针和就绪探针)可以使您的应用程序在睡眠时保持可用状态。当检测到没有回应的 Pod 时,会将其标记为不健康,并使这些 Pod 重新启动或重新安排。

在老版本的 Kubernetes 原生并不支持 gRPC 健康检查。gRPC 的开发人员在 Kubernetes 中部署时可以采用以下三种方法:
health

  1. httpGet prob: 不能与 gRPC 一起使用。您需要重构您的应用程序,必须同时支持 gRPC 和 HTTP/1.1 协议(在不同的端口号上)。
  2. tcpSocket probe: 打开 gRPC 服务器的 Socket 是没有意义的,因为它无法读取响应主体。
  3. exec probe: 将定期调用容器生态系统中的程序。对于 gRPC,这意味着开发人员要自己实现健康 RPC,然后使用容器编写并交付客户端工具。

grpc-health-probe 解决方案

gRPC有一个标准的健康检查协议。它可以从任何语言轻松使用。生成的代码和用于设置运行状况的实用程序几乎都在gRPC的所有语言实现中提供。

在 gRPC 应用程序中实现了此健康检查协议,那么可以使用标准或通用工具调用Check()方法来确定服务器状态。

grpc-health-probe

使用此工具,您可以在所有 gRPC 应用程序中使用相同的健康检查配置。这种方法有以下要求:

  • 用您喜欢的语言找到 gRPC 的 “健康” 模块并开始使用它例如 Golang
  • 将二进制文件 grpc_health_probe 送到容器中。
  • 配置 Kubernetes 的 “exec” 检查模块来调用容器中的 “grpc_health_probe” 工具。在这种情况下,执行 “grpc_health_probe” 将通过 localhost 调用您的 gRPC 服务器,因为它们位于同一个容器中。

以下内容译自grpc-health-probe官方文档
1.grpc_health_probe实用程序允许您查询 gRPC 服务的健康状况,这些服务通过gRPC 健康检查协议公开服务的状态。
2.grpc_health_probe是指用于在健康检查GRPC应用 Kubernetes,使用EXEC探针执行。
3.⚠️ Kubernetes v1.23 现在引入了内置的 gRPC 健康检查 功能作为 alpha 功能。因此,您可能不再需要使用此工具,而是使用原生 Kubernetes 功能。
4.如果您使用的是旧版本的 Kubernetes,或者使用高级配置(例如自定义元数据、TLS 或更精细的超时调整),或者根本不使用 Kubernetes,这个工具仍然很有用。
5.此命令行实用程序生成 RPC 到/grpc.health.v1.Health/Check. 如果它以SERVING状态响应,grpc_health_probe它将成功退出,否则它将以非零退出代码退出(如下所述)。

1
2
3
4
5
6
7
8
#成功
$ grpc_health_probe -addr=localhost:5000
healthy: SERVING

#失败
$ grpc_health_probe -addr=localhost:9999 -connect-timeout 250ms -rpc-timeout 100ms
failed to connect service at "localhost:9999": context deadline exceeded
exit status 2

Dockerfile_Yaml编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM registry.cn-shanghai.aliyuncs.com/xxxx/base:alpine-glibc-Shanghai

ENV GRPC_HEALTH_PROBE_VERSION=v0.4.6

WORKDIR /app

COPY . .
RUN chmod +x ip138_updating_interface && \
wget -qO /bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-arm64 && \
chmod +x /bin/grpc_health_probe

EXPOSE 8031

CMD ["/app/ip138_updating_interface"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
containers:
- name: server
image: "[YOUR-DOCKER-IMAGE]"
ports:
- containerPort: 5000
readinessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:5000"]
initialDelaySeconds: 5
livenessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:5000"]
initialDelaySeconds: 10

负载均衡相关

随着微服务的增长,gRPC在这些较小的服务之间的相互通信中获得了很大的普及,在后台,gRPC使用http/2在同一连接和双工流中复用许多请求。

使用具有结构化数据的快速,轻便的二进制协议作为服务之间的通信介质确实很有吸引力,但是使用gRPC时需要考虑一些因素,最重要的是如何处理负载均衡。

gRPC使用粘性连接

gRPC连接是粘性的。这意味着当从客户端到服务器建立连接时,相同的连接将被尽可能长时间地用于许多请求(多路复用)。这样做是为了避免所有最初的时间和资源花费在TCP握手上。因此,当客户端获取与服务器实例的连接时,它将保持连接。

现在,当同一客户端开始发送大量请求时,它们都将转到同一服务器实例。而这正是问题所在,将没有机会将负载分配给其他实例。他们都去同一个实例。这就是为什么粘性连接会使负载平衡变得非常困难。

可以实现gRPC负载的方式有很多种如DNS服务发现客户端实现等等,生产环境中为了不给开发人员添加负担,我这里只说一下服务端通过7层负载实现gRPC负载均衡的过程
四层负载
上图种中间的LB工作在OSI参考模型第4层。因此,它非常快,可以处理更多的连接。当出现新的TCP通信连接时,负载均衡器将选择一个实例,并且在连接有效期内将连接路由到该单个实例。

然而如上面所说gRPC连接是粘性的和持久的,因此它会在负载均衡器后面的客户端和同一服务端实例之间保持长时间相同的连接,只要服务端不挂。

粘性连接和k8s的弹性伸缩

如果单个POD上的负载(内存或cpu)高于自动伸缩策略,则将导致在该目标工作负载中启动一个新的副本POD。
但是,目标工作负载中的新POD将无济于事。为什么? 同样是因为gRPC连接是持久的且具有粘性。正在发送大量请求的客户端,将继续将它们发送到与其连接的同一POD上。

因此,新的POD就算被启动,也没有请求过载将流向新的实例。利用率高的同一POD仍在接收来自客户端的请求负载(因为客户端一直在重用相同的连接)。

HPA策略可能会不断触发并向目标工作负载添加新POD(因为单个POD的cpu /内存过载)。但是这些新POD接收的流量几乎为零。自动缩放策略可能会继续触发并可能最大化目标工作负载中允许的POD,而实际上的新建的POD并没有参与到流量负载分担。

负载均衡解决办法

综上所述就意味着我们需要一个 7 层负载均衡,而 K8s 的 Service 核心使用的是 kube proxy,这是一个 4 层负载均衡,所以不能满足我们的要求。

gRPC 官方博客的文章 gRPC Load Balancing 也给出了 gRPC 负载均衡的思路:

  • 客户端型负载均衡(ZooKeeper/Etcd/Consul/DNS 等),适用于流量较高的场景
  • 代理式的负载均衡(Haproxy/Nginx/Traefik/Envoy/Linkerd 等),适用于服务器需要对外提供统一的入口场景,通常我们说的 ServiceMesh 就是这种方式

以下内容译自gRPC官方博客
In L7 (application level) load balancing, the LB terminates and parses the HTTP/2 protocol. The LB can inspect each request and assign a backend based on the request contents. For example, a session cookie sent as part of HTTP header can be used to associate with a specific backend, so all requests for that session are served by the same backend. Once the LB has chosen an appropriate backend, it creates a new HTTP/2 connection to that backend. It then forwards the HTTP/2 streams received from the client to the backend(s) of choice. With HTTP/2, LB can distribute the streams from one client among multiple backends.
-
在 L7(应用层)负载均衡中,LB 终止并解析 HTTP/2 协议。LB 可以检查每个请求并根据请求内容分配后端。例如,作为 HTTP 标头的一部分发送的会话 cookie 可用于与特定后端关联,因此该会话的所有请求都由同一后端提供服务。一旦 LB 选择了合适的后端,它就会创建一个到该后端的新 HTTP/2 连接。然后它将从客户端接收到的 HTTP/2 流转发到选择的后端。使用 HTTP/2,LB 可以将来自一个客户端的流分发到多个后端。

四层负载

Ingress Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@node001 ingress]# cat svc1.yaml 
apiVersion: v1
kind: Service
metadata:
name: grpchttps
namespace: ingress-nginx
spec:
type: NodePort
ports:
- name: https
port: 8844
targetPort: 443
protocol: TCP
nodePort: 8844
selector:
app.kubernetes.io/name: ingress-nginx

Ingress nginx gRPC默认跑在https所在端口,这里将443端口映射为8844。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kind: Ingress
metadata:
name: grpc-ingress
annotations:
# 注意这里:必须要配置以指明后端服务为gRPC服务
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
spec:
rules:
# gRPC服务域名
- host: grpc-service.fxeyeinterface.com
http:
paths:
- path: /
pathType: Prefix
backend:
# gRPC服务
serviceName: grpc-service
servicePort: 50051
tls:
- hosts:
- grpc-service.fxeyeinterface.com
secretName: grpc-service.fxeyeinterface.com #可以随便写一个名字,使用ingress默认颁发的证书

grpcurl
查看访问日志:
nginx

Ingress Kong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#添加KONG http2 所需端口
- name: KONG_PROXY_LISTEN
value: 0.0.0.0:8000, 0.0.0.0:8443 ssl, 0.0.0.0:8888 http2, 0.0.0.0:8844 ssl http2

#创建service
curl -XPOST http://kong-proxy.kong:8001/services \
--data name=service-as-grpc \
--data protocol=grpc \
--data host=grpc-service.default.svc.cluster.local \
--data port=50051

curl -XPOST http://kong-proxy.kong:8001/services/service-as-grpc/routes \
--data protocols=grpc \
--data "hosts=grpc-service.fxeyeinterface.com" \
--data name=service-ar-grpc \
--data paths=/

# 其中,kong-proxy.kong:8001 为kong in k8s的管理端口,paths对应的具体的grpc后端
# 其它的grpc后端按上新建即可

grpcurl
查看访问日志:
kong