在 Kubernetes 环境中实现 gRPC 负载均衡

前言前段时间写过一篇 gRPC 的入门文章,在最后还留了一个坑没有填:

在 Kubernetes 环境中实现 gRPC 负载均衡

文章插图
图片
也就是 gRPC 的负载均衡问题 , 因为当时的业务请求量不算大 , 再加上公司没有对 Istio 这类服务网格比较熟悉的大牛,所以我们也就一直拖着没有解决,依然只是使用了 kube.NETes 的 service 进行负载,好在也没有出什么问题 。
由于现在换了公司后也需要维护公司的服务网格服务,结合公司内部对 Istio 的使用现在终于不再停留在理论阶段了 。
所以也终有机会将这个坑填了 。
gRPC 负载均衡负载不均衡原理先来回顾下背景,为什么会有 gRPC 负债不均衡的问题 。由于 gRPC 是基于 HTTP/2 协议的 , 所以客户端和服务端会保持长链接 , 一旦链接建立成功后就会一直使用这个连接处理后续的请求 。
在 Kubernetes 环境中实现 gRPC 负载均衡

文章插图
图片
除非我们每次请求之后都新建一个连接,这显然是不合理的 。
所以要解决 gRPC 的负载均衡通常有两种方案:
  • 服务端负载均衡
  • 客户端负载均衡 在 gRPC 这个场景服务端负载均衡不是很合适 , 所有的请求都需要经过一个负载均衡器,这样它就成为整个系统的瓶颈,所以更推荐使用客户端负载均衡 。
客户端负载均衡目前也有两种方案,最常见也是传统方案 。
在 Kubernetes 环境中实现 gRPC 负载均衡

文章插图
图片
这里以 Dubbo 的调用过程为例,调用的时候需要从服务注册中心获取到提供者的节点信息,然后在客户端本地根据一定的负载均衡算法得出一个节点然后发起请求 。
换成 gRPC 也是类似的,这里以 go-zero 负载均衡的原理为例:
在 Kubernetes 环境中实现 gRPC 负载均衡

文章插图
图片
gRPC 官方库也提供了对应的负载均衡接口,但我们依然需要自己维护服务列表然后在客户端编写负载均衡算法,这里有个官方 demo:
https://Github.com/grpc/grpc-go/blob/87eb5b7502493f758e76c4d09430c0049a81a557/examples/features/load_balancing/client/mAIn.go
但切换到 kubernetes 环境中时再使用以上的方式就不够优雅了,因为我们使用 kubernetes 的目的就是不想再额外的维护这个客户端包 , 这部分能力最好是由 kubernetes 自己就能提供 。
但遗憾的是 kubernetes 提供的 service 只是基于 L4 的负载,所以我们每次请求的时候都只能将请求发往同一个 Provider 节点 。
测试这里我写了一个小程序来验证负债不均衡的示例:
// Create gRPC servergo func() {var port = ":50051"lis, err := net.Listen("tcp", port)if err != nil {log.Fatalf("failed to listen: %v", err)}s := grpc.NewServer()pb.RegisterGreeterServer(s, &server{})if err := s.Serve(lis); err != nil {log.Fatalf("failed to serve: %v", err)} else {log.Printf("served on %s n", port)}}()// server is used to implement helloworld.GreeterServer.type server struct {pb.UnimplementedGreeterServer}// SayHello implements helloworld.GreeterServerfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {log.Printf("Received: %v", in.GetName())name, _ := os.Hostname()// Return hostname of Serverreturn &pb.HelloReply{Message: fmt.Sprintf("hostname:%s, in:%s", name, in.Name)}, nil}使用同一个 gRPC 连接发起一次 gRPC 请求 , 服务端会返回它的 hostname
var (once sync.Oncecpb.GreeterClient)http.HandleFunc("/grpc_client", func(w http.ResponseWriter, r *http.Request) {once.Do(func() {service := r.URL.Query().Get("name")conn, err := grpc.Dial(fmt.Sprintf("%s:50051", service), grpc.WithInsecure(), grpc.WithBlock())if err != nil {log.Fatalf("did not connect: %v", err)}c = pb.NewGreeterClient(conn)})// Contact the server and print out its response.name := "world"ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()g, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})if err != nil {log.Fatalf("could not greet: %v", err)}fmt.Fprint(w, fmt.Sprintf("Greeting: %s", g.GetMessage()))})创建一个 service 用于给 gRPC 提供域名:
apiVersion: v1kind: Servicemetadata:name: native-tools-2spec:selector:App: native-tools-2ports:- name: httpport: 8081targetPort: 8081- name: grpcport: 50051targetPort: 50051


推荐阅读