前言

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



也就是 gRPC 的负载均衡问题,因为当时的业务请求量不算大,再加上公司没有对 Istio 这类服务网格比较熟悉的大牛,所以我们也就一直拖着没有解决,依然只是使用了 kubernetes 的 service 进行负载,好在也没有出什么问题。

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

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

gRPC 负载均衡

负载不均衡

原理

先来回顾下背景,为什么会有 gRPC 负债不均衡的问题。

由于 gRPC 是基于 HTTP/2 协议的,所以客户端和服务端会保持长链接,一旦链接建立成功后就会一直使用这个连接处理后续的请求。

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

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

  • 服务端负载均衡
  • 客户端负载均衡

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

客户端负载均衡目前也有两种方案,最常见也是传统方案。



这里以 Dubbo 的调用过程为例,调用的时候需要从服务注册中心获取到提供者的节点信息,然后在客户端本地根据一定的负载均衡算法得出一个节点然后发起请求。

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

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 server
go 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.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
name, _ := os.Hostname()
// Return hostname of Server
return &pb.HelloReply{Message: fmt.Sprintf("hostname:%s, in:%s", name, in.Name)}, nil
}

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

var (
once sync.Once
c pb.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: v1
kind: Service
metadata:
name: native-tools-2
spec:
selector:
app: native-tools-2
ports:
- name: http
port: 8081
targetPort: 8081
- name: grpc
port: 50051
targetPort: 50051

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

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

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

k exec -it native-tools-65c5bd87fc-2fsmc bash
Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-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
Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-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
Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-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
Greeting: hostname:native-tools-2-d6c454689-zpwxt, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-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

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

使用 Istio

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

关于开启 namespace 的 Istio 注入会在后续更新,现在感兴趣的可以查看下官方文档:

https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/

Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-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

Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-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

Greeting: hostname:native-tools-2-5fbf46cf54-xprjz, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

Greeting: hostname:native-tools-2-5fbf46cf54-5m7dl, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

Greeting: hostname:native-tools-2-5fbf46cf54-nz8h5, in:worldistio-proxy@n:/$ curl http://127.0.0.1:8081/grpc_client?name=native-tools-2

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 也是客户端负载均衡的一种实现。



以 Istio 的架构图为例:

  • 每一个 Pod 下会新增一个 Proxycontainer,所有的流量入口和出口都会经过它。
  • 它会从控制平面 Istiod 中拿到服务的注册信息,也就是 kubernetes 中的 service。
  • 发生请求时由 proxy 容器中的 Envoy 进行最终的负载请求。

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

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

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

总结

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

  • 统一网关,处理东西、南北向流量。
  • 灰度发布
  • 流量控制
  • 接口粒度的超时配置
  • 自动重试等

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

本文相关源码:

https://github.com/crossoverJie/k8s-combat

参考链接:

在 kubernetes 环境中实现 gRPC 负载均衡的更多相关文章

  1. Kubernetes 中的 gRPC 负载均衡

    安装环境依赖 docker-desktop >= 4.1.1 kubernetes >= 1.21.5 go >= 1.17 protobuf >= 3.17.3 istioc ...

  2. 【译】gRPC负载均衡

    原文地址:https://github.com/grpc/grpc/blob/master/doc/load-balancing.md gRPC负载均衡 范围 本文档解释了gPRC的负载均衡的设计. ...

  3. gRPC负载均衡(客户端负载均衡)

    前言 上篇介绍了如何使用etcd实现服务发现,本篇将基于etcd的服务发现前提下,介绍如何实现gRPC客户端负载均衡. gRPC负载均衡 gRPC官方文档提供了关于gRPC负载均衡方案Load Bal ...

  4. gRPC负载均衡(自定义负载均衡策略)

    前言 上篇文章介绍了如何实现gRPC负载均衡,但目前官方只提供了pick_first和round_robin两种负载均衡策略,轮询法round_robin不能满足因服务器配置不同而承担不同负载量,这篇 ...

  5. 一步一步在Windows中使用MyCat负载均衡 下篇

    之前在 一步一步在Windows中使用MyCat负载均衡 上篇 中已经讲了如何配置出MyCat.下面讲其相关的使用. 五.配置MyCat-eye 对于MyCat监控官网还提供一个MyCat-eye w ...

  6. Linux中keepalived+LVS负载均衡的搭建测试

    1.1 LVS简介       LVS(Linux Virtual Server),也就是Linux虚拟服务器, 是一个自由软件项目.使用LVS技术要达到的目标是:通过LVS提供的负载均衡技术和Lin ...

  7. 一步一步在Windows中使用MyCat负载均衡

    一步一步在Windows中使用MyCat负载均衡 http://www.cnblogs.com/zhangs1986/p/6408981.html   mycat+sqlServer简单demo配置 ...

  8. LVS集群中的IP负载均衡技术

    LVS集群中的IP负载均衡技术 章文嵩 (wensong@linux-vs.org) 转自LVS官方参考资料 2002 年 4 月 本文在分析服务器集群实现虚拟网络服务的相关技术上,详细描述了LVS集 ...

  9. 一步一步在Windows中使用MyCat负载均衡 上篇

    传统关系型数据库的分布式开发通常需要自己做,不仅耗时耗力而且效果不是很理想,当想快速搭建时,最初想到的是看有没有第三方,网上牛人还是很多的,做得比较好的其中之一Mycat,它是开源的分布式数据库系统, ...

  10. Linux环境下Nginx及负载均衡

    Nginx 简介 Nginx 是一个高性能的 HTTP 和反向代理 Web 服务器,同时也提供了 IMAP/POP3/SMTP 服务.前向代理作为客户端的代理,服务端只知道代理的 IP 地址而不知道客 ...

随机推荐

  1. 如何使用libavcodec将.h264码流文件解码为.yuv图像序列?

    一.打开和关闭输入文件和输出文件 //io_data.cpp static FILE* input_file= nullptr; static FILE* output_file= nullptr; ...

  2. 获得 markdown 无序列表格式的文件目录树

    tree 命令可以获得文件目录结构,但是放在文档中时,我想用 markdown 无序列表的形式,在编辑器内还可以折叠. 完整解决方案:在 ~/.oh-my-zsh/custom 下添加下述自定义函数( ...

  3. AcWing 4489. 最长子序列题解

    思路 此题较为简单,简述一下思路. 设原始数列为 \(a\). 定义 \(dp\) 数组,初始值都为 \(1\). 遍历数组,如果 \(a[i-1]*2 \leq a[i]\) ,那么 \(dp[i] ...

  4. 【SpringBoot】WebSocket在线聊天

    先看一下页面效果,有点简单粗暴!哈哈哈哈哈,别介意. 本文参考:SpringBoot2.0集成WebSocket,实现后台向前端推送信息 新建一个springboot项目 引入相关依赖 <dep ...

  5. 【技术积累】JavaScript中的基础语法【二】

    JavaScript编写方式 JavaScript是一种脚本语言,用于为网页添加交互性和动态功能.它可以直接嵌入到HTML中,并通过浏览器解释执行.下面是一些常见的JavaScript编写方式和相应的 ...

  6. 2023年ccpc大学生程序设计竞赛-crf

    第一次参加这种大型线下程序设计比赛,心情自然是很激动,但比赛中并没有想象中那么顺利,甚至可以说有些惊险,比赛开始的时候,我们三人随便看看题,顺便等着跟榜,对于签到题我们自然是相信clk可以很快地独立完 ...

  7. hdfs小文件合并

    HDFS small file merge 1.hive Settings There are 3 settings that should be configured before archivin ...

  8. MAUI Blazor Android 输入框软键盘遮挡问题

    前言 最近才发现MAUI Blazor Android存在输入框软键盘遮挡这个问题,搜索了一番,原来这是安卓webview一个由来已久的问题,还好有大佬提出了解决方案 AndroidBug5497Wo ...

  9. golang channel 未关闭导致的内存泄漏

    现象 某一个周末我们的服务 oom了,一个比较重要的job 没有跑完,需要重跑,以为是偶然,重跑成功,因为是周末没有去定位原因 又一个工作日,它又oom了,重跑成功,持续观察,job 在oom之前竟然 ...

  10. Linux实现双击自动安装deb文件

    在Linux中,有几个常用的软件可以实现双击自动安装.deb文件的功能.以下是一些推荐的软件: 1. GDebi:GDebi是一个轻量级的图形化工具,专门用于安装.deb文件.它可以解决依赖关系,并提 ...