上篇介绍了如何使用etcd实现服务发现,本篇将基于etcd的服务发现前提下,介绍如何实现gRPC客户端负载均衡。

gRPC官方文档提供了关于gRPC负载均衡方案Load Balancing in gRPC,此方案是为gRPC设计的,下面我们对此进行分析。

gRPC中的负载平衡是以每次调用为基础,而不是以每个连接为基础。换句话说,即使所有的请求都来自一个客户端,我们仍希望它们在所有的服务器上实现负载平衡。

  • 集中式(Proxy Model)

在服务消费者和服务提供者之间有一个独立的负载均衡(LB),通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。

该方案主要问题:服务消费方、提供方之间增加了一级,有一定性能开销,请求量大时,效率较低。

可能有读者会认为集中式负载均衡存在这样的问题,一旦负载均衡服务挂掉,那整个系统将不能使用。
解决方案:可以对负载均衡服务进行DNS负载均衡,通过对一个域名设置多个IP地址,每次DNS解析时轮询返回负载均衡服务地址,从而实现简单的DNS负载均衡。

  • 客户端负载(Balancing-aware Client)

针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。

该方案主要问题:要用多种语言、多个版本的客户端编写和维护负载均衡策略,使客户端的代码大大复杂化。

  • 独立LB服务(External Load Balancing Service)

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。

不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库。

本篇将介绍第二种负载均衡方法,客户端负载均衡。

gRPC已提供了简单的负载均衡策略(如:Round Robin),我们只需实现它提供的BuilderResolver接口,就能完成gRPC客户端负载均衡。

  1. type Builder interface {
  2. Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
  3. Scheme() string
  4. }

Builder接口:创建一个resolver(本文称之服务发现),用于监视名称解析更新。
Build方法:为给定目标创建一个新的resolver,当调用grpc.Dial()时执行。
Scheme方法:返回此resolver支持的方案,Scheme定义可参考:https://github.com/grpc/grpc/blob/master/doc/naming.md

  1. type Resolver interface {
  2. ResolveNow(ResolveNowOption)
  3. Close()
  4. }

Resolver接口:监视指定目标的更新,包括地址更新和服务配置更新。
ResolveNow方法:被 gRPC 调用,以尝试再次解析目标名称。只用于提示,可忽略该方法。
Close方法:关闭resolver

根据以上两个接口,我们把服务发现的功能写在Build方法中,把获取到的负载均衡服务地址返回到客户端,并监视服务更新情况,以修改客户端连接。
修改服务发现代码,discovery.go

  1. package etcdv3
  2. import (
  3. "context"
  4. "log"
  5. "sync"
  6. "time"
  7. "github.com/coreos/etcd/mvcc/mvccpb"
  8. "go.etcd.io/etcd/clientv3"
  9. "google.golang.org/grpc/resolver"
  10. )
  11. const schema = "grpclb"
  12. //ServiceDiscovery 服务发现
  13. type ServiceDiscovery struct {
  14. cli *clientv3.Client //etcd client
  15. cc resolver.ClientConn
  16. serverList map[string]resolver.Address //服务列表
  17. lock sync.Mutex
  18. }
  19. //NewServiceDiscovery 新建发现服务
  20. func NewServiceDiscovery(endpoints []string) resolver.Builder {
  21. cli, err := clientv3.New(clientv3.Config{
  22. Endpoints: endpoints,
  23. DialTimeout: 5 * time.Second,
  24. })
  25. if err != nil {
  26. log.Fatal(err)
  27. }
  28. return &ServiceDiscovery{
  29. cli: cli,
  30. }
  31. }
  32. //Build 为给定目标创建一个新的`resolver`,当调用`grpc.Dial()`时执行
  33. func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
  34. log.Println("Build")
  35. s.cc = cc
  36. s.serverList = make(map[string]resolver.Address)
  37. prefix := "/" + target.Scheme + "/" + target.Endpoint + "/"
  38. //根据前缀获取现有的key
  39. resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
  40. if err != nil {
  41. return nil, err
  42. }
  43. for _, ev := range resp.Kvs {
  44. s.SetServiceList(string(ev.Key), string(ev.Value))
  45. }
  46. s.cc.NewAddress(s.getServices())
  47. //监视前缀,修改变更的server
  48. go s.watcher(prefix)
  49. return s, nil
  50. }
  51. // ResolveNow 监视目标更新
  52. func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
  53. log.Println("ResolveNow")
  54. }
  55. //Scheme return schema
  56. func (s *ServiceDiscovery) Scheme() string {
  57. return schema
  58. }
  59. //Close 关闭
  60. func (s *ServiceDiscovery) Close() {
  61. log.Println("Close")
  62. s.cli.Close()
  63. }
  64. //watcher 监听前缀
  65. func (s *ServiceDiscovery) watcher(prefix string) {
  66. rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
  67. log.Printf("watching prefix:%s now...", prefix)
  68. for wresp := range rch {
  69. for _, ev := range wresp.Events {
  70. switch ev.Type {
  71. case mvccpb.PUT: //新增或修改
  72. s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
  73. case mvccpb.DELETE: //删除
  74. s.DelServiceList(string(ev.Kv.Key))
  75. }
  76. }
  77. }
  78. }
  79. //SetServiceList 新增服务地址
  80. func (s *ServiceDiscovery) SetServiceList(key, val string) {
  81. s.lock.Lock()
  82. defer s.lock.Unlock()
  83. s.serverList[key] = resolver.Address{Addr: val}
  84. s.cc.NewAddress(s.getServices())
  85. log.Println("put key :", key, "val:", val)
  86. }
  87. //DelServiceList 删除服务地址
  88. func (s *ServiceDiscovery) DelServiceList(key string) {
  89. s.lock.Lock()
  90. defer s.lock.Unlock()
  91. delete(s.serverList, key)
  92. s.cc.NewAddress(s.getServices())
  93. log.Println("del key:", key)
  94. }
  95. //GetServices 获取服务地址
  96. func (s *ServiceDiscovery) getServices() []resolver.Address {
  97. addrs := make([]resolver.Address, 0, len(s.serverList))
  98. for _, v := range s.serverList {
  99. addrs = append(addrs, v)
  100. }
  101. return addrs
  102. }

代码主要修改以下地方:

  1. 把获取的服务地址转成resolver.Address,供gRPC客户端连接。

  2. 根据schema的定义规则,修改key格式。

服务注册主要修改key存储格式,register.go

  1. package etcdv3
  2. import (
  3. "context"
  4. "log"
  5. "time"
  6. "go.etcd.io/etcd/clientv3"
  7. )
  8. //ServiceRegister 创建租约注册服务
  9. type ServiceRegister struct {
  10. cli *clientv3.Client //etcd client
  11. leaseID clientv3.LeaseID //租约ID
  12. //租约keepalieve相应chan
  13. keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
  14. key string //key
  15. val string //value
  16. }
  17. //NewServiceRegister 新建注册服务
  18. func NewServiceRegister(endpoints []string, serName, addr string, lease int64) (*ServiceRegister, error) {
  19. cli, err := clientv3.New(clientv3.Config{
  20. Endpoints: endpoints,
  21. DialTimeout: 5 * time.Second,
  22. })
  23. if err != nil {
  24. log.Fatal(err)
  25. }
  26. ser := &ServiceRegister{
  27. cli: cli,
  28. key: "/" + schema + "/" + serName + "/" + addr,
  29. val: addr,
  30. }
  31. //申请租约设置时间keepalive
  32. if err := ser.putKeyWithLease(lease); err != nil {
  33. return nil, err
  34. }
  35. return ser, nil
  36. }
  37. //设置租约
  38. func (s *ServiceRegister) putKeyWithLease(lease int64) error {
  39. //设置租约时间
  40. resp, err := s.cli.Grant(context.Background(), lease)
  41. if err != nil {
  42. return err
  43. }
  44. //注册服务并绑定租约
  45. _, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
  46. if err != nil {
  47. return err
  48. }
  49. //设置续租 定期发送需求请求
  50. leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
  51. if err != nil {
  52. return err
  53. }
  54. s.leaseID = resp.ID
  55. s.keepAliveChan = leaseRespChan
  56. log.Printf("Put key:%s val:%s success!", s.key, s.val)
  57. return nil
  58. }
  59. //ListenLeaseRespChan 监听 续租情况
  60. func (s *ServiceRegister) ListenLeaseRespChan() {
  61. for leaseKeepResp := range s.keepAliveChan {
  62. log.Println("续约成功", leaseKeepResp)
  63. }
  64. log.Println("关闭续租")
  65. }
  66. // Close 注销服务
  67. func (s *ServiceRegister) Close() error {
  68. //撤销租约
  69. if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
  70. return err
  71. }
  72. log.Println("撤销租约")
  73. return s.cli.Close()
  74. }

客户端修改gRPC连接服务的部分代码即可:

  1. func main() {
  2. r := etcdv3.NewServiceDiscovery(EtcdEndpoints)
  3. resolver.Register(r)
  4. // 连接服务器
  5. conn, err := grpc.Dial(r.Scheme()+"://8.8.8.8/simple_grpc", grpc.WithBalancerName("round_robin"), grpc.WithInsecure())
  6. if err != nil {
  7. log.Fatalf("net.Connect err: %v", err)
  8. }
  9. defer conn.Close()
  10. // 建立gRPC连接
  11. grpcClient = pb.NewSimpleClient(conn)

gRPC内置了简单的负载均衡策略round_robin,根据负载均衡地址,以轮询的方式进行调用服务。

服务端启动时,把服务地址注册到etcd中即可:

  1. func main() {
  2. // 监听本地端口
  3. listener, err := net.Listen(Network, Address)
  4. if err != nil {
  5. log.Fatalf("net.Listen err: %v", err)
  6. }
  7. log.Println(Address + " net.Listing...")
  8. // 新建gRPC服务器实例
  9. grpcServer := grpc.NewServer()
  10. // 在gRPC服务器注册我们的服务
  11. pb.RegisterSimpleServer(grpcServer, &SimpleService{})
  12. //把服务注册到etcd
  13. ser, err := etcdv3.NewServiceRegister(EtcdEndpoints, SerName, Address, 5)
  14. if err != nil {
  15. log.Fatalf("register service err: %v", err)
  16. }
  17. defer ser.Close()
  18. //用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
  19. err = grpcServer.Serve(listener)
  20. if err != nil {
  21. log.Fatalf("grpcServer.Serve err: %v", err)
  22. }
  23. }

我们先启动并注册三个服务

然后客户端进行调用

看服务端接收到的请求

关闭localhost:8000服务,剩余localhost:8001localhost:8002服务接收请求

重新打开localhost:8000服务

可以看到,gRPC客户端负载均衡运行良好。

本文介绍了gRPC客户端负载均衡的实现,它简单实现了gRPC负载均衡的功能。但在对接其他语言时候比较麻烦,需要每种语言都实现一套服务发现和负载策略。

目前官方只提供了取第一个地址pick_first和轮询round_robin两种负载均衡策略。下篇将介绍如何自定义负载均衡策略。

有兴趣了解第三种负载均衡方法External Load Balancing Service的,可以参考这个项目:https://github.com/bsm/grpclb

源码地址:https://github.com/Bingjian-Zhu/etcd-example

参考:

版权声明:本文为FireworksEasyCool原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/FireworksEasyCool/p/12912839.html