• 在 kubernetes 环境中实现 gRPC 负载均衡


    47a08b18462fde5b530886e50bb9298d.png


    前言

    前段时间写过一篇 gRPC 的入门文章,在最后还留了一个坑没有填:7bf721b0a67dd5bc9b9d51d5413e9ac2.png也就是 gRPC 的负载均衡问题,因为当时的业务请求量不算大,再加上公司没有对 Istio 这类服务网格比较熟悉的大牛,所以我们也就一直拖着没有解决,依然只是使用了 kubernetes 的 service 进行负载,好在也没有出什么问题。

    由于现在换了公司后也需要维护公司的服务网格服务,结合公司内部对 Istio 的使用现在终于不再停留在理论阶段了。

    所以也终有机会将这个坑填了。

    gRPC 负载均衡

    负载不均衡

    原理

    先来回顾下背景,为什么会有 gRPC 负债不均衡的问题。由于 gRPC 是基于 HTTP/2 协议的,所以客户端和服务端会保持长链接,一旦链接建立成功后就会一直使用这个连接处理后续的请求。

    f92fe31943a228a184698ddc8c90dd05.png
    image.png

    除非我们每次请求之后都新建一个连接,这显然是不合理的。

    所以要解决 gRPC 的负载均衡通常有两种方案:

    • 服务端负载均衡

    • 客户端负载均衡 在 gRPC 这个场景服务端负载均衡不是很合适,所有的请求都需要经过一个负载均衡器,这样它就成为整个系统的瓶颈,所以更推荐使用客户端负载均衡。

    客户端负载均衡目前也有两种方案,最常见也是传统方案。5bd8ab87a167dc108cfd86ad691e4042.png这里以 Dubbo 的调用过程为例,调用的时候需要从服务注册中心获取到提供者的节点信息,然后在客户端本地根据一定的负载均衡算法得出一个节点然后发起请求。

    换成 gRPC 也是类似的,这里以 go-zero 负载均衡的原理为例:92436dbf3fdaba70424113735a824331.png

    gRPC 官方库也提供了对应的负载均衡接口,但我们依然需要自己维护服务列表然后在客户端编写负载均衡算法,这里有个官方 demo:

    https://github.com/grpc/grpc-go/blob/87eb5b7502493f758e76c4d09430c0049a81a557/examples/features/load_balancing/client/main.go

    但切换到 kubernetes 环境中时再使用以上的方式就不够优雅了,因为我们使用 kubernetes 的目的就是不想再额外的维护这个客户端包,这部分能力最好是由 kubernetes 自己就能提供。

    但遗憾的是 kubernetes 提供的 service 只是基于 L4 的负载,所以我们每次请求的时候都只能将请求发往同一个 Provider 节点。

    测试

    这里我写了一个小程序来验证负载不均衡的示例:

    1. // Create gRPC server
    2. go func() {  
    3.    var port = ":50051"  
    4.    lis, err := net.Listen("tcp", port)  
    5.    if err != nil {  
    6.       log.Fatalf("failed to listen: %v", err)  
    7.    }  
    8.    s := grpc.NewServer()  
    9.    pb.RegisterGreeterServer(s, &server{})  
    10.    if err := s.Serve(lis); err != nil {  
    11.       log.Fatalf("failed to serve: %v", err)  
    12.    } else {  
    13.       log.Printf("served on %s \n", port)  
    14.    }  
    15. }()
    16. // server is used to implement helloworld.GreeterServer.  
    17. type server struct {  
    18.    pb.UnimplementedGreeterServer  
    19. }  
    20.   
    21. // SayHello implements helloworld.GreeterServer  
    22. func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {  
    23.    log.Printf("Received: %v", in.GetName())  
    24.    name, _ := os.Hostname()  
    25.    // Return hostname of Server
    26.    return &pb.HelloReply{Message: fmt.Sprintf("hostname:%s, in:%s", name, in.Name)}, nil  
    27. }

    使用同一个 gRPC 连接发起一次 gRPC 请求,服务端会返回它的 hostname

    1. var (  
    2.    once sync.Once  
    3.    c    pb.GreeterClient  
    4. )  
    5. http.HandleFunc("/grpc_client"func(w http.ResponseWriter, r *http.Request) {  
    6.    once.Do(func() {  
    7.       service := r.URL.Query().Get("name")  
    8.       conn, err := grpc.Dial(fmt.Sprintf("%s:50051", service), grpc.WithInsecure(), grpc.WithBlock())  
    9.       if err != nil {  
    10.          log.Fatalf("did not connect: %v", err)  
    11.       }  
    12.       c = pb.NewGreeterClient(conn)  
    13.    })  
    14.   
    15.    // Contact the server and print out its response.  
    16.    name := "world"  
    17.    ctx, cancel := context.WithTimeout(context.Background(), time.Second)  
    18.    defer cancel()  
    19.    g, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})  
    20.    if err != nil {  
    21.       log.Fatalf("could not greet: %v", err)  
    22.    }  
    23.    fmt.Fprint(w, fmt.Sprintf("Greeting: %s", g.GetMessage()))  
    24. })

    创建一个 service 用于给 gRPC 提供域名:

    1. apiVersion: v1  
    2. kind: Service  
    3. metadata:  
    4.   name: native-tools-2
    5. spec:  
    6.   selector:  
    7.     app: native-tools-2
    8.   ports:  
    9.     - name: http  
    10.       port: 8081  
    11.       targetPort: 8081  
    12.     - name: grpc  
    13.       port: 50051  
    14.       targetPort: 50051

    同时将我们的 gRPC server 部署三个节点,再部署了一个客户端节点:

    1. ❯ k get pod
    2. NAME                                READY   STATUS    RESTARTS
    3. native-tools-2-d6c454689-52wgd      1/1     Running   0              
    4. native-tools-2-d6c454689-67rx4      1/1     Running   0              
    5. native-tools-2-d6c454689-zpwxt      1/1     Running   0              
    6. native-tools-65c5bd87fc-2fsmc       2/2     Running   0

    我们进入客户端节点执行多次 grpc 请求:

    1. k exec -it native-tools-65c5bd87fc-2fsmc bash
    2. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    3. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    4. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    5. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    6. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    7. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    8. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    9. Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

    会发现每次请求的都是同一个节点 native-tools-2-d6c454689-zpwxt,这也就证明了在 kubernetes 中直接使用 gRPC 负载是不均衡的,一旦连接建立后就只能将请求发往那个节点。

    使用 Istio

    Istio 可以拿来解决这个问题,我们换到一个注入了 Istio 的 namespace 下还是同样的 代码,同样的 service 资源进行测试。

    关于开启 namespace 的 Istio 注入会在后续更新,现在感兴趣的可以查看下官方文档:https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/

    1. Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    2. Greeting: hostname:native-tools-2-5fbf46cf54-xprjz, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    3. Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    4. Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    5. Greeting: hostname:native-tools-2-5fbf46cf54-xprjz, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    6. Greeting: hostname:native-tools-2-5fbf46cf54-xprjz, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    7. Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    8. Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    9. Greeting: hostname:native-tools-2-5fbf46cf54-nz8h5, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2
    10. Greeting: hostname:native-tools-2-5fbf46cf54-nz8h5, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

    可以发现同样的请求已经被负载到了多个 server 后端,这样我们就可以不再单独维护一个客户端 SDK 的情况下实现了负载均衡。

    原理

    其实本质上 Istio 也是客户端负载均衡的一种实现。7d038133ae565abc275a3dbb7a8dc0dc.png以 Istio 的架构图为例:

    • 每一个 Pod 下会新增一个 Proxycontainer,所有的流量入口和出口都会经过它。

    • 它会从控制平面 Istiod 中拿到服务的注册信息,也就是 kubernetes 中的 service。

    • 发生请求时由 proxy 容器中的 Envoy 进行最终的负载请求。

    可以在使用了 Istio 的 Pod 中查看到具体的容器:

    1. ❯ k get pod native-tools-2-5fbf46cf54-5m7dl -n istio-test-2 -o json | jq '.spec.containers[].name'
    2. "istio-proxy"
    3. "native-tools-2"

    可以发现这里存在一个 istio-proxy 的容器,也就是我们常说的 sidecar,这样我们就可以把原本的 SDK 里的功能全部交给 Istio 去处理。

    总结

    当然 Istio 的功能远不止于此,比如:

    • 统一网关,处理东西、南北向流量。

    • 灰度发布

    • 流量控制

    • 接口粒度的超时配置

    • 自动重试等

    这次只是一个开胃菜,更多关于 Istio 的内容会在后续更新,比如会从如何在 kubernetes 集群中安装 Istio 讲起,带大家一步步使用好 Istio

    本文相关源码:https://github.com/crossoverJie/k8s-combat

    参考链接:

    • https://istio.io/latest/docs/setup/getting-started/

    • https://segmentfault.com/a/1190000042295402

    • https://go-zero.dev/docs/tutorials/service/governance/lb

    往期推荐

    技术阅读周刊第一期

    使用 Helm 管理应用的一些 Tips

    五分钟k8s入门到实战-应用配置

    Go 语言史诗级更新-循环Bug修复

    d441225aba632f0cec584c1b4f09fb5b.gif

    点分享

    4b286770dff67cdd6747a7babaa8df35.gif

    点收藏

    bdfb6ce7284815d874de4f42be1ed33c.gif

    点点赞

    251f3a83cd1a7a0036e8c2b7ed3f9440.gif

    点在看

  • 相关阅读:
    c语言-8皇后问题
    前端项目部署
    摩莎(MOXA)NPort 5110串口转网口设定
    leetcode 145.二叉树的后序遍历
    MASA Framework 事件总线 - 跨进程事件总线
    [ruby on rails]部署时候产生ActiveRecord::PreparedStatementCacheExpired错误的原因及解决方法
    Maven环境搭建
    vue2给对象新增、删除属性,界面不刷新 。通过数组下标修改数组界面不会刷新
    【AGC】构建服务1-云函数示例
    【我的前端】面向 JavaScript 开发:前端必学的4种函数式编程技术
  • 原文地址:https://blog.csdn.net/qq_18661793/article/details/133896894