服务器端负载均衡选项: MaxConnectionAge
默认情况下,gRPC 客户端与服务器间存在一条 HTTP/2 长连接,当服务器端添加了新结点时,客户端是无感知的,新上线的结点将不会有请求进来。~MaxConnectionAge~ 指定长连接的存活时间,当客户端检测到 gRPC 长连接断开时,会解析服务器域名,重新建立连接,从而为负载均衡提供了基础。
import "google.golang.org/grpc"
import "google.golang.org/grpc/keepalive"
server = grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{MaxConnectionAge: 2 * time.Minute}),
)
客户端负载均衡器: Pick First
适用于终端用户与服务间通信,由于存在大量终端用户,服务器端的负载均衡不成问题。
Pick First 是 golang gRPC 默认的负载均衡器,采用 “一服务一连接” 模型,客户端与服务间只有一条长连接。
它从服务器域名解析出 IP 列表后,依次尝试建立一条长连接,直到成功为止,当长连接断开时,重复以上过程。
客户端通过 服务器域名 建立 gRPC 连接来启用 Pick First 负载均衡器。
import "google.golang.org/grpc"
import pb "google.golang.org/grpc/examples/helloworld/helloworld"
conn, err := grpc.Dial(
"greeter_server",
grpc.WithInsecure(),
)
c := pb.NewGreeterClient(conn)
客户端负载均衡器: Round Robin
适用于内部服务间通信,在 K8S 中配合 Headless Services 非常容易部署。
Round Robin 是 golang gRPC 内置支持的负载均衡器,采用 “一服务器 IP 一连接” 模型,客户端与服务器的每一个 IP 间都有一条长连接。
它从服务器域名解析出 IP 列表后,对每一个 IP 确保存在一条长连接,当有长连接断开时,重复以上过程。
客户端通过 “dns:///服务器域名” 建立 gRPC 连接并指定 “roundrobin” 启用 Round Robin 负载均衡器。
import "google.golang.org/grpc"
import "google.golang.org/grpc/balancer/roundrobin"
import pb "google.golang.org/grpc/examples/helloworld/helloworld"
conn, err := grpc.Dial(
"dns:///greeter_server",
grpc.WithInsecure(),
grpc.WithBalancerName(roundrobin.Name),
)
c := pb.NewGreeterClient(conn)
客户端负载均衡器: Pool
适用于外部服务间通信,在 K8S 中以 SLB 方式暴露的服务对外表现为一个 IP,此时采用连接池可以确保负载均衡。
最新的 go-grpc 库(v1.28+)提供了 ClientConnInterface 接口,通过该接口可以很方便地实现 gRPC 连接池,参考 googleapis 。
采用 “一服务器 IP N 连接” 模型,客户端与服务器的每一个 IP 间都有 N 条长连接,各连接间的负载均衡采用 Round Robin 算法。
它从服务器域名解析出 IP 列表后,对每一个 IP 确保存在 N 条长连接,当有长连接断开时,重复以上过程。
conn_pool.go
package main
import (
"sync/atomic"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/balancer/roundrobin"
)
type ConnPool struct {
conns []*grpc.ClientConn
idx uint32
}
func (p *ConnPool) Num() int {
return len(p.conns)
}
func (p *ConnPool) Conn() *grpc.ClientConn {
i := atomic.AddUint32(&p.idx, 1)
return p.conns[i%uint32(len(p.conns))]
}
func (p *ConnPool) Close() error {
var firstError error
for _, conn := range p.conns {
if err := conn.Close(); err != nil {
if firstError != nil {
firstError = err
}
}
}
return firstError
}
func (p *ConnPool) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
return p.Conn().Invoke(ctx, method, args, reply, opts...)
}
func (p *ConnPool) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return p.Conn().NewStream(ctx, desc, method, opts...)
}
func NewConnPool(ctx context.Context, address string, poolSize int) (*ConnPool, error) {
pool := &ConnPool{}
for i := 0; i < poolSize; i++ {
conn, err := grpc.Dial(
address,
grpc.WithInsecure(),
grpc.WithBalancerName(roundrobin.Name),
)
if err != nil {
defer pool.Close() // NOTE: error from Close is ignored.
return nil, err
}
pool.conns = append(pool.conns, conn)
}
return pool, nil
}
使用连接池
import "golang.org/x/net/context"
import pb "google.golang.org/grpc/examples/helloworld/helloworld"
conn, err := NewConnPool(
context.Background(),
"dns:///greeter_server",
10,
)
c := pb.NewGreeterClient(conn)