gRPC负载均衡(客户端负载均衡)

发布时间:2026/8/15 14:07:12
gRPC负载均衡(客户端负载均衡) gRPC负载均衡客户端负载均衡前言上篇介绍了如何使用etcd实现服务发现本篇将基于etcd的服务发现前提下介绍如何实现gRPC客户端负载均衡。gRPC负载均衡gRPC官方文档提供了关于gRPC负载均衡方案Load Balancing in gRPC此方案是为gRPC设计的下面我们对此进行分析。1、对每次调用进行负载均衡gRPC中的负载平衡是以每次调用为基础而不是以每个连接为基础。换句话说即使所有的请求都来自一个客户端我们仍希望它们在所有的服务器上实现负载平衡。2、负载均衡的方法集中式Proxy Model[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-akL52HNn-1613979911428)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518153536494-684598725.png)]在服务消费者和服务提供者之间有一个独立的负载均衡LB通常是专门的硬件设备如 F5或者基于软件如 LVSHAproxy等实现。LB上有所有服务的地址映射表通常由运维配置注册当服务消费方调用某个目标服务时它向LB发起请求由LB以某种策略比如轮询Round-Robin做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力能自动摘除不健康的服务实例。该方案主要问题服务消费方、提供方之间增加了一级有一定性能开销请求量大时效率较低。可能有读者会认为集中式负载均衡存在这样的问题一旦负载均衡服务挂掉那整个系统将不能使用。解决方案可以对负载均衡服务进行DNS负载均衡通过对一个域名设置多个IP地址每次DNS解析时轮询返回负载均衡服务地址从而实现简单的DNS负载均衡。客户端负载Balancing-aware Client[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z422EtFU-1613979911430)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518155900462-1370526164.png)]针对第一个方案的不足此方案将LB的功能集成到服务消费方进程里也被称为软负载或者客户端负载方案。服务提供方启动时首先将服务地址注册到服务注册表同时定期报心跳到服务注册表以表明服务的存活状态相当于健康检查服务消费方要访问某个服务时它通过内置的LB组件向服务注册表查询同时缓存并定期刷新目标服务地址列表然后以某种负载均衡策略选择一个目标服务地址最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部同时服务消费方和服务提供方之间是直接调用没有额外开销性能比较好。该方案主要问题要用多种语言、多个版本的客户端编写和维护负载均衡策略使客户端的代码大大复杂化。独立LB服务External Load Balancing Service[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W9r95C1t-1613979911431)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518170636421-1833253282.png)]该方案是针对第二种方案的不足而提出的一种折中方案原理和第二种方案基本类似。不同之处是将LB和服务发现功能从进程内移出来变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题服务调用方和LB之间是进程内调用性能好同时该方案还简化了服务调用方不需要为不同语言开发客户库。本篇将介绍第二种负载均衡方法客户端负载均衡。实现gRPC客户端负载均衡gRPC已提供了简单的负载均衡策略如Round Robin我们只需实现它提供的Builder和Resolver接口就能完成gRPC客户端负载均衡。typeBuilderinterface{Build(target Target,cc ClientConn,opts BuildOption)(Resolver,error)Scheme()string}Builder接口创建一个resolver本文称之服务发现用于监视名称解析更新。Build方法为给定目标创建一个新的resolver当调用grpc.Dial()时执行。Scheme方法返回此resolver支持的方案Scheme定义可参考https://github.com/grpc/grpc/blob/master/doc/naming.mdtypeResolverinterface{ResolveNow(ResolveNowOption)Close()}Resolver接口监视指定目标的更新包括地址更新和服务配置更新。ResolveNow方法被 gRPC 调用以尝试再次解析目标名称。只用于提示可忽略该方法。Close方法关闭resolver根据以上两个接口我们把服务发现的功能写在Build方法中把获取到的负载均衡服务地址返回到客户端并监视服务更新情况以修改客户端连接。修改服务发现代码discovery.gopackageetcdv3import(contextlogsynctimegithub.com/coreos/etcd/mvcc/mvccpbgo.etcd.io/etcd/clientv3google.golang.org/grpc/resolver)constschemagrpclb//ServiceDiscovery 服务发现typeServiceDiscoverystruct{cli*clientv3.Client//etcd clientcc resolver.ClientConn serverListmap[string]resolver.Address//服务列表lock sync.Mutex}//NewServiceDiscovery 新建发现服务funcNewServiceDiscovery(endpoints[]string)resolver.Builder{cli,err:clientv3.New(clientv3.Config{Endpoints:endpoints,DialTimeout:5*time.Second,})iferr!nil{log.Fatal(err)}returnServiceDiscovery{cli:cli,}}//Build 为给定目标创建一个新的resolver当调用grpc.Dial()时执行func(s*ServiceDiscovery)Build(target resolver.Target,cc resolver.ClientConn,opts resolver.BuildOption)(resolver.Resolver,error){log.Println(Build)s.cccc s.serverListmake(map[string]resolver.Address)prefix:/target.Scheme/target.Endpoint///根据前缀获取现有的keyresp,err:s.cli.Get(context.Background(),prefix,clientv3.WithPrefix())iferr!nil{returnnil,err}for_,ev:rangeresp.Kvs{s.SetServiceList(string(ev.Key),string(ev.Value))}s.cc.NewAddress(s.getServices())//监视前缀修改变更的servergos.watcher(prefix)returns,nil}// ResolveNow 监视目标更新func(s*ServiceDiscovery)ResolveNow(rn resolver.ResolveNowOption){log.Println(ResolveNow)}//Scheme return schemafunc(s*ServiceDiscovery)Scheme()string{returnschema}//Close 关闭func(s*ServiceDiscovery)Close(){log.Println(Close)s.cli.Close()}//watcher 监听前缀func(s*ServiceDiscovery)watcher(prefixstring){rch:s.cli.Watch(context.Background(),prefix,clientv3.WithPrefix())log.Printf(watching prefix:%s now...,prefix)forwresp:rangerch{for_,ev:rangewresp.Events{switchev.Type{casemvccpb.PUT://新增或修改s.SetServiceList(string(ev.Kv.Key),string(ev.Kv.Value))casemvccpb.DELETE://删除s.DelServiceList(string(ev.Kv.Key))}}}}//SetServiceList 新增服务地址func(s*ServiceDiscovery)SetServiceList(key,valstring){s.lock.Lock()defers.lock.Unlock()s.serverList[key]resolver.Address{Addr:val}s.cc.NewAddress(s.getServices())log.Println(put key :,key,val:,val)}//DelServiceList 删除服务地址func(s*ServiceDiscovery)DelServiceList(keystring){s.lock.Lock()defers.lock.Unlock()delete(s.serverList,key)s.cc.NewAddress(s.getServices())log.Println(del key:,key)}//GetServices 获取服务地址func(s*ServiceDiscovery)getServices()[]resolver.Address{addrs:make([]resolver.Address,0,len(s.serverList))for_,v:ranges.serverList{addrsappend(addrs,v)}returnaddrs}代码主要修改以下地方把获取的服务地址转成resolver.Address供gRPC客户端连接。根据schema的定义规则修改key格式。服务注册主要修改key存储格式register.gopackageetcdv3import(contextlogtimego.etcd.io/etcd/clientv3)//ServiceRegister 创建租约注册服务typeServiceRegisterstruct{cli*clientv3.Client//etcd clientleaseID clientv3.LeaseID//租约ID//租约keepalieve相应chankeepAliveChan-chan*clientv3.LeaseKeepAliveResponse keystring//keyvalstring//value}//NewServiceRegister 新建注册服务funcNewServiceRegister(endpoints[]string,serName,addrstring,leaseint64)(*ServiceRegister,error){cli,err:clientv3.New(clientv3.Config{Endpoints:endpoints,DialTimeout:5*time.Second,})iferr!nil{log.Fatal(err)}ser:ServiceRegister{cli:cli,key:/schema/serName/addr,val:addr,}//申请租约设置时间keepaliveiferr:ser.putKeyWithLease(lease);err!nil{returnnil,err}returnser,nil}//设置租约func(s*ServiceRegister)putKeyWithLease(leaseint64)error{//设置租约时间resp,err:s.cli.Grant(context.Background(),lease)iferr!nil{returnerr}//注册服务并绑定租约_,errs.cli.Put(context.Background(),s.key,s.val,clientv3.WithLease(resp.ID))iferr!nil{returnerr}//设置续租 定期发送需求请求leaseRespChan,err:s.cli.KeepAlive(context.Background(),resp.ID)iferr!nil{returnerr}s.leaseIDresp.ID s.keepAliveChanleaseRespChan log.Printf(Put key:%s val:%s success!,s.key,s.val)returnnil}//ListenLeaseRespChan 监听 续租情况func(s*ServiceRegister)ListenLeaseRespChan(){forleaseKeepResp:ranges.keepAliveChan{log.Println(续约成功,leaseKeepResp)}log.Println(关闭续租)}// Close 注销服务func(s*ServiceRegister)Close()error{//撤销租约if_,err:s.cli.Revoke(context.Background(),s.leaseID);err!nil{returnerr}log.Println(撤销租约)returns.cli.Close()}客户端修改gRPC连接服务的部分代码即可funcmain(){r:etcdv3.NewServiceDiscovery(EtcdEndpoints)resolver.Register(r)// 连接服务器conn,err:grpc.Dial(r.Scheme()://8.8.8.8/simple_grpc,grpc.WithBalancerName(round_robin),grpc.WithInsecure())iferr!nil{log.Fatalf(net.Connect err: %v,err)}deferconn.Close()// 建立gRPC连接grpcClientpb.NewSimpleClient(conn)gRPC内置了简单的负载均衡策略round_robin根据负载均衡地址以轮询的方式进行调用服务。服务端启动时把服务地址注册到etcd中即可funcmain(){// 监听本地端口listener,err:net.Listen(Network,Address)iferr!nil{log.Fatalf(net.Listen err: %v,err)}log.Println(Address net.Listing...)// 新建gRPC服务器实例grpcServer:grpc.NewServer()// 在gRPC服务器注册我们的服务pb.RegisterSimpleServer(grpcServer,SimpleService{})//把服务注册到etcdser,err:etcdv3.NewServiceRegister(EtcdEndpoints,SerName,Address,5)iferr!nil{log.Fatalf(register service err: %v,err)}deferser.Close()//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待直到进程被杀死或者 Stop() 被调用errgrpcServer.Serve(listener)iferr!nil{log.Fatalf(grpcServer.Serve err: %v,err)}}运行效果我们先启动并注册三个服务[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CcBjY60C-1613979911433)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518201520301-2141314089.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yEDYzKtW-1613979911434)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518201526062-1105611810.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPyNZUfz-1613979911435)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518201529806-1864982377.png)]然后客户端进行调用[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uf2751dH-1613979911436)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518201645385-8940133.png)]看服务端接收到的请求[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0oh1VkkQ-1613979911437)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518201925163-1429636105.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNdAKdPX-1613979911438)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518201929077-822092499.png)]关闭localhost:8000服务剩余localhost:8001和localhost:8002服务接收请求[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IsnUNpt2-1613979911438)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518202359155-2143850614.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5NgMmsA6-1613979911439)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518202405272-1990664274.png)]重新打开localhost:8000服务[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DxIslk4E-1613979911440)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518202655967-135791051.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WY5uMrJI-1613979911440)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518202700598-298101288.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oLUQPK6o-1613979911441)(https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200518202703530-602882933.png)]可以看到gRPC客户端负载均衡运行良好。总结本文介绍了gRPC客户端负载均衡的实现它简单实现了gRPC负载均衡的功能。但在对接其他语言时候比较麻烦需要每种语言都实现一套服务发现和负载策略且如果要较为复杂的负载策略需要修改客户端代码才能完成。下篇将介绍如何实现官方推荐的负载均衡策略External Load Balancing Service。源码地址https://github.com/Bingjian-Zhu/etcd-example参考https://segmentfault.com/a/1190000008672912https://github.com/wothing/wonaming