当前位置: 首页 > news >正文

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,或者基于软件如 LVS,HAproxy等实现。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),我们只需实现它提供的BuilderResolver接口,就能完成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.md

typeResolverinterface{ResolveNow(ResolveNowOption)Close()}

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

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

packageetcdv3import("context""log""sync""time""github.com/coreos/etcd/mvcc/mvccpb""go.etcd.io/etcd/clientv3""google.golang.org/grpc/resolver")constschema="grpclb"//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)}return&ServiceDiscovery{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.cc=cc s.serverList=make(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{addrs=append(addrs,v)}returnaddrs}

代码主要修改以下地方:

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

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

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

packageetcdv3import("context""log""time""go.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}//注册服务并绑定租约_,err=s.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.leaseID=resp.ID s.keepAliveChan=leaseRespChan 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连接grpcClient=pb.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() 被调用err=grpcServer.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:8001localhost: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/1190000008672912

  • https://github.com/wothing/wonaming

http://www.cnnetsun.cn/news/4041551.html

相关文章:

  • 低压变频器哪个好——2025年主流品牌选型深度解析
  • 几百页扫描PDF不能搜索复制?Umi-OCR双层PDF实测记录
  • LosslessCut 无损剪辑指南:3分钟完成传统软件半小时的切割活
  • SQL注入高级技巧:布尔盲注与WAF绕过实战解析
  • AI Agent 面试题 393:如何设计Agent的记忆重要性评分机制?
  • 技术拆解(五):残差连接到底在“残”什么?HC、MHC和注意力残差,一张图根治困惑
  • 数据的存储
  • 免费开源的桌面围栏工具NoFences:3分钟让图标各归其位
  • 微信聊天记录存哪里才安心?WeChatMsg 本地留档实操手册
  • 面试官:Workflow 和 Agent 有什么区别?如何选型?
  • 一篇搞定 OneNote 笔记迁移:onenote-md-exporter 完整使用指南
  • AIGC无限画布前端实现:从Canvas架构到性能优化全解析
  • QQ空间备份三步走:用QQ空间导出助手免费永久保存十年青春
  • OpenClaw:AI Agent如何重塑测试自动化,从脚本执行到智能决策
  • 利用GPT-5.6模拟国自然评审的评分思路:把淘汰项改成加分项
  • 三分钟装好开源番茄钟:把macOS菜单栏变成你的专注控制台
  • 02.01.02.泛微OA Ecology10 (开发环境搭建idea安装配置)
  • Dell XPS 13 (2026)评测:预算Windows笔记本新标杆,能否击败MacBook Neo?
  • 暗黑2角色存档修改完整教程:用Diablo Edit2免费修改角色、装备与任务进度
  • 米哈游扫码登录器上手指南:如何用 3 分钟搭好你的自动抢码工具
  • 满足各类环境测试需求大阵列芯片测试座-测试良率高
  • OpenProject容器化部署完整实操:排掉三个高频坑,半小时把项目管理平台跑起来
  • Agent 能不能上线,关键看评估能不能真正控制业务流程
  • Knowledge Graph Augmented Large Language Models for Disease Prediction
  • AgentScope 2.0:专为托管AI智能体打造的企业级云原生平台
  • ColabFold 批量处理实战:一次跑完几百条序列的蛋白质结构预测完整流程
  • 微信公众号数据采集完整指南:3个实战场景玩转搜狗微信搜索爬虫
  • JPEXS Free Flash Decompiler 实战指南:一条命令跑通 SWF 反编译、修复与资源提取全流程
  • ARM架构KVM虚拟化支持现状分析
  • 单片机常用型号参考