k8s gRPC负载均衡问题
在 K8S 下部署服务,缺省情况下会被分配一个地址(也就是 ClusterIP),客户端的请求会发送给它,然后再通过负载均衡转发给后端某个 pod:
grpc http2.0链接复用问题
HTTP/1.1 不是实现了基于 KeepAlive 的连接复用么?为什么 HTTP/1.1 的复用没问题,而 HTTP/2 的复用就有问题?
答案是 HTTP/1.1 的 复用是串行的,当请求到达的时候,如果没有空闲连接那么就新创建一个连接,如果有空闲连接那么就可以复用,同一个时间点,连接里最多只能承载一个请求,结果是 HTTP/1.1 可以连接多个 pod;
而 HTTP/2 的复用是并行的,当请求到达的时候,如果没有连接那么就创建连接,如果有连接,那么不管其是否空闲都可以复用,同一个时间点,连接里可以承载多个请求,结果是 HTTP/2 仅仅连接了一个 pod。
如果默认使用直链方式也就是交给k8s自己去处理负载均衡,在http1.x短连接是没有问题的,grpc是使用http2实现的,它是长链接多路复用,后续发送的请求也都会打到相同pod,造成负载不均衡
问题复现
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
pb "codeup.aliyun.com/rpc/proto/test"
"google.golang.org/grpc"
)
var (
client pb.TestSrvServiceClient
GrpcAddr = "svc cluster_ip:8351"
)
func init() {
conn, err := grpc.Dial(GrpcAddr, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
client = pb.NewTestSrvServiceClient(conn)
}
func getAliyunServiceClient() pb.TestSrvServiceClient {
return client
}
func main() {
c := getAliyunServiceClient()
wg := &sync.WaitGroup{}
for i := 1; i < 200; i++ {
wg.Add(1)
go func(c pb.TestSrvServiceClient, i int, wg *sync.WaitGroup) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := c.GetGroupResult(ctx, &pb.GetGroupResultRequest{
ExperimentId: int64(i),
})
if err != nil {
log.Println("err", err)
} else {
fmt.Println("res", res)
}
time.Sleep(time.Millisecond * 100)
wg.Done()
}(c, i, wg)
}
wg.Wait()
}
查看日志,发现3个pod的服务大部分的请求都打在了一个pod上面
解决方案:
Proxy负载均衡
在 Proxy 中实现负载均衡:采用 Envoy 做代理,和每台后端服务器保持长连接,当客户端请求到达时,代理服务器依照规则转发请求给后端服务器,从而实现负载均衡。
Client负载均衡
在 Client 中实现负载均衡:把服务部署成 headless service,这样服务就有了一个域名,然后客户端通过域名访问 gRPC 服务,DNS resolver 会通过 DNS 查询后端多个服务器地址,然后通过算法来实现负载均衡。
如服务发现consul等,则不存在此问题
K8S headless service服务详解
为什么需要无头服务?
客户端想要和指定的的Pod直接通信
并不是随机选择
开发人员希望自己控制负载均衡的策略,不使用Service提供的默认的负载均衡的功能,或者应用程序希望知道属于同组服务的其它实例。