gRPC有一个关键的特性是负载均衡。允许客户端请求多个服务端,能够防止任何一个服务端过载和允许系统拓展多个服务。
gRPC 负载均衡策略是通过域名解析到一系列的服务 ip 地址,这个策略的职责是维护服务器的链接和当一个 RPC 发送数据时,选取一个链接。
设置 DNS 解析
func (*HelloResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &HelloResolver{target: target, cc: cc, addrStore: map[string][]string{exampleServiceName: addrlist}}
r.start()
return r, nil
}
设置负载均衡策略
roundrobinConn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`), // This sets the initial balancing policy.
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
代码实践
开发环境:
golang 1.20.1
protoc libprotoc 3.20.3
目录结构
.
├── com
│ └── example
│ ├── hello
│ │ ├── hello.pb.go
│ │ ├── hello.proto
│ │ └── hello_grpc.pb.go
│ └── loadbalance
│ ├── client.go
│ └── server.go
├── go.mod
└── go.sum
hello.proto
syntax = "proto3";
package hello;
option go_package = "com/example/hello";
service Greeting {
rpc SayHello(HelloRequest) returns (HelloReply){}
}
message HelloRequest{
string name = 1;
}
message HelloReply{
string message = 1;
}
server.go
package main
import (
"context"
"example.com/com/example/hello"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"log"
"net"
"sync"
)
type server struct {
hello.UnimplementedGreetingServer
addr string
}
// 开启2个端口作为服务端
var (
addrs = []string{":9090", ":9091"}
)
func (s *server) SayHello(ctx context.Context, request *hello.HelloRequest) (*hello.HelloReply, error) {
if request.Name == "" {
return nil, status.Errorf(codes.InvalidArgument, "request missing required field: Name")
}
return &hello.HelloReply{
Message: "Hello," + request.Name + "(" + s.addr + ")",
}, nil
}
func StartServer(addr string) {
listen, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal("failed to listen " + addr)
}
s := grpc.NewServer()
hello.RegisterGreetingServer(s, &server{addr: addr})
log.Printf("serving on %s\n", addr)
if err := s.Serve(listen); err != nil {
log.Fatalf("failed to serve %v", err)
}
}
func main() {
var wg sync.WaitGroup
// 启动2个服务
for _, addr := range addrs {
wg.Add(1)
go func(addr string) {
StartServer(addr)
}(addr)
}
log.Println("all server start...")
wg.Wait()
}
client.go
package main
import (
"context"
"example.com/com/example/hello"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/status"
"log"
"time"
)
const (
exampleScheme = "example"
exampleServiceName = "lb.example.grpc.io"
)
var addrlist = []string{"localhost:9090", "localhost:9091"}
func callSayHello(c hello.GreetingClient, name string) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
r, err := c.SayHello(ctx, &hello.HelloRequest{Name: name})
if err != nil {
if status.Code(err) != codes.InvalidArgument {
log.Println("Received unexpected loadbalance:", err)
return
}
log.Println("Received loadbalance:", err)
return
}
log.Println("Received message ", r.Message)
}
func bulkRPC(conn *grpc.ClientConn) {
c := hello.NewGreetingClient(conn)
for i := 0; i < 5; i++ {
callSayHello(c, "zhang san")
}
}
func main() {
conn, err := grpc.Dial(fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("client connection err %v", err)
}
defer conn.Close()
fmt.Println("--- calling helloworld.Greeter/SayHello with pick_first ---")
bulkRPC(conn)
log.Println()
// 为链接设置负载均衡的策略
roundrobinConn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName),
// 设置负载均衡的策略
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer roundrobinConn.Close()
fmt.Println("--- calling helloworld.Greeter/SayHello with round_robin ---")
bulkRPC(roundrobinConn)
}
type HelloResolverBuilder struct{}
func (*HelloResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &HelloResolver{target: target, cc: cc, addrStore: map[string][]string{exampleServiceName: addrlist}}
r.start()
return r, nil
}
func (*HelloResolverBuilder) Scheme() string {
return exampleScheme
}
type HelloResolver struct {
target resolver.Target
cc resolver.ClientConn
addrStore map[string][]string
}
func (r *HelloResolver) start() {
addreStrs := r.addrStore[r.target.Endpoint()]
address := make([]resolver.Address, len(addreStrs))
for i, s := range addreStrs {
address[i] = resolver.Address{Addr: s}
}
r.cc.UpdateState(resolver.State{Addresses: address})
}
func (*HelloResolver) ResolveNow(o resolver.ResolveNowOptions) {}
func (*HelloResolver) Close() {}
func init() {
resolver.Register(&HelloResolverBuilder{})
}
客户端执行的效果:
--- calling helloworld.Greeter/SayHello with pick_first ---
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30
--- calling helloworld.Greeter/SayHello with round_robin ---
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30 Received message Hello,zhang san(:9091)
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
2023/11/04 15:00:30 Received message Hello,zhang san(:9091)
2023/11/04 15:00:30 Received message Hello,zhang san(:9090)
总结
使用的 DNS 解析到服务器地址进行负载均衡,实际运用中不会再代码里面写死ip地址,需要借助中间件的类似 etcd
将服务器的地址上报,可以动态的监听服务的地址变化,这种方式都是客户端负载,另外还有一个是服务端服务在 k8s 环境可以通过 ingress
实现服务端负载。