前面已经介绍了几种 gRPC 的进阶特性,这篇文章再来看看 gRPC 的:
- 多路复用
- 元数据
- 负载均衡
我把前面的 Order 服务再复制一份,作为本篇文章的代码演示。
多路复用
首先来看看多路复用,在前面的代码演示中,服务器端其实只有一个 gRPC 服务,但是 gRPC 还支持在一个服务端运行多个 gRPC 服务。
我们有一个现成的订单 Order 服务,这里我再新增一个 问候服务 Greeter,还是在 OrderInfo.proto 文件中,添加如下代码:
// 问候服务 service GreeterService { rpc sayHello(google.protobuf.StringValue) returns (google.protobuf.StringValue); }
然后在服务端的 main.go 中注册服务:
s := grpc.NewServer() //注册订单服务 order.RegisterOrderManagementServer(s, server) //注册问候服务 order.RegisterGreeterServiceServer(s, &GreeterServer{})
在客户端这边,调用的时候,可以创建对应的 Client,然后调用相应的方法即可:
// 问候服务客户端 greeterClient := order.NewGreeterServiceClient(conn) _, err = greeterClient.SayHello(ctx, &wrappers.StringValue{Value: "roseduan"}) if err != nil { log.Println("call greeter server [say hello] err.", err) }
元数据
在多个微服务的调用当中,信息交换常常是使用方法之间的参数传递的方式,但是在有些场景下,一些信息可能和 RPC 方法的业务参数没有直接的关联,所以不能作为参数的一部分,在 gRPC 中,可以使用元数据来存储这类信息。
首先来看一下一个简单的元数据的发送和接收的例子,首先在客户端定义并在创建订单的时候发送元数据信息:
client := order.NewOrderManagementClient(conn) md := metadata.Pairs( "timestamp", time.Now().Format(time.RFC3339), "test-key", "val1", "test-key", "val2", ) //使用元数据context mdCtx := metadata.NewOutgoingContext(context.Background(), md) fmt.Println("----------------use metadata----------------") testOrder := &order.Order{Destination: "beijing", Items: []string{"book1", "book2"}, Price: 123.232} _, err = client.AddOrder(mdCtx, testOrder)
然后就可以在服务端的 AddOrder 方法中,获取到设置的 metadata 信息了:
//获取元数据 if md, ok := metadata.FromIncomingContext(ctx); !ok { log.Println("failed to get metadata") } else { log.Printf("metadata from client : %+v\n", md) }
当然,在服务器也可以发送元数据到客户端当中,比如下面的这个例子:
在服务端的方法中定义:
//发送一个header元信息 md := metadata.New(map[string]string{"location": "San Jose", "timestamp": time.Now().Format(time.StampNano)}) err = grpc.SendHeader(ctx, md) if err != nil { log.Println("send header err") }
在服务端中的接收该信息:
//接收从服务端发送过来的metadata信息 var header metadata.MD _, err = client.AddOrder(mdCtx, testOrder, grpc.Header(&header)) log.Printf("metadata from server : %+v\n", header)
负载均衡
负载均衡策略一般是服务端的负载均衡和客户端的负载均衡,针对服务端代理的负载均衡,一般可采用 Nginx 或者 Envoy 来实现,让他们来提供不同的负载均衡算法,示意图如下:
客户端的负载均衡,指的是在请求发送的时候,在客户端进行服务的选择,从而实现负载均衡。
接下来看一个客户端负载均衡的代码示例:
假设 gRPC 服务端有一个服务,运行在了两个端口上,分别为 50051 和 50052,下面是服务端的代码:
type Server struct { addr string } func (s *Server) SayHello(ctx context.Context, req *wrappers.StringValue) (resp *wrappers.StringValue, err error) { resp = &wrappers.StringValue{} log.Println("the server port is ", s.addr) return } func startServer(addr string) { listener, err := net.Listen("tcp", addr) if err != nil { log.Println("tcp listen err.", err) return } s := grpc.NewServer() load_balance_demo.RegisterEchoServiceServer(s, &Server{addr}) log.Printf("serving on %s\n", addr) if err := s.Serve(listener); err == nil { log.Fatalf("failed to serve: %v", err) } } func main() { var wg sync.WaitGroup for _, addr := range addrs { wg.Add(1) go func(val string) { defer wg.Done() startServer(val) }(addr) } wg.Wait() }
在客户端呢,需要自定义负载均衡策略,我们选择最简单的 round_robin 算法。
下面是客户端的代码:
var addrs = []string{"localhost:50051", "localhost:50052"} const ( exampleScheme = "example" exampleServiceName = "lb.example.com" ) func main() { conn, _ := grpc.Dial( fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName), grpc.WithBalancerName(roundrobin.Name), grpc.WithInsecure(), ) defer conn.Close() makeRPCs(conn, 10) } func makeRPCs(cc *grpc.ClientConn, n int) { client := load_balance_demo.NewEchoServiceClient(cc) for i := 0; i < n; i++ { callUnaryEcho(client, "test for load balance") } } func callUnaryEcho(c load_balance_demo.EchoServiceClient, message string) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &wrappers.StringValue{Value: message}) if err != nil { log.Fatalf("could not greet: %v", err) } fmt.Println(r.Value) }
由于服务端分别在两个端口上,因此这里我们可以使用命名解析器,将一个测试的域名指向 gRPC 的两个服务,命名解析器的代码如下:
type exampleResolverBuilder struct {} type exampleResolver struct { target resolver.Target cc resolver.ClientConn addrsStore map[string][]string } func (*exampleResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { r := &exampleResolver{ target: target, cc: cc, addrsStore: map[string][]string{ exampleServiceName: addrs, }, } r.start() return r, nil } func (*exampleResolverBuilder) Scheme() string {return exampleScheme} func (r *exampleResolver) start() { addrStrs := r.addrsStore[r.target.Endpoint] addrs := make([]resolver.Address, len(addrStrs)) for i, s := range addrStrs { addrs[i] = resolver.Address{Addr: s} } r.cc.UpdateState(resolver.State{Addresses: addrs}) } func (*exampleResolver) ResolveNow(o resolver.ResolveNowOptions){} func (*exampleResolver) Close() {} func init() { resolver.Register(&exampleResolverBuilder{}) }
然后运行客户端和服务端的代码,会发现调用了 10 次服务端的方法,轮询调用每个服务,这正是 round_robin 的负载均衡逻辑。
如果将代码中的负载均衡逻辑改为 pick_first,那么则会一直调用第一个服务。
conn, _ := grpc.Dial( fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName), grpc.WithBalancerName(grpc.PickFirstBalancerName), grpc.WithInsecure(),
你可以在服务端方法中打上日志,来验证是否运行正确。