八、Golang gRPC 自定义负载均衡

228 阅读3分钟

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 实现服务端负载。