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

相关新闻

Git仓库完整迁移指南:镜像克隆、裸仓库与远程地址修改三种方法详解

Git仓库完整迁移指南:镜像克隆、裸仓库与远程地址修改三种方法详解

1. 项目缘起:为什么我们需要“完整”迁移Git仓库?最近在帮团队做基础设施的国产化适配,其中一个绕不开的环节就是代码仓库的迁移。这听起来简单,不就是把代码从一个地方复制到另一个地方吗?但实际操作起来,…

2026/10/9 1:14:37 阅读更多 →
2026百元档随身WiFi避坑指南:100元以内,便宜和靠谱怎么兼得?

2026百元档随身WiFi避坑指南:100元以内,便宜和靠谱怎么兼得?

开学季的宿舍、刚搬的出租屋、临时出差的高铁上,很多人第一次考虑随身WiFi时预算都不高,核心问题就一句话:100元以内有没有靠谱的选择?先把结论放前面:预算100元以内,飞猫M1是目前公开参数较完整的机型之一…

2026/10/13 9:55:12 阅读更多 →
IDEA Git集成实战:从基础配置到高效团队协作全指南

IDEA Git集成实战:从基础配置到高效团队协作全指南

1. 项目概述:为什么IDEA里的Git值得你花时间? 如果你是一名Java开发者,或者正在使用IntelliJ IDEA进行任何语言的开发,那么“在IDEA中使用Git”这个标题对你来说,可能意味着一个从“能用”到“高效用”的质变过程。很多…

2026/10/9 7:07:35 阅读更多 →

最新新闻

R语言回归实战:首尔自行车需求预测与Cubist模型调优

R语言回归实战:首尔自行车需求预测与Cubist模型调优

简介:这份资源面向具备一定R语言基础、希望上手真实回归建模的数据分析学习者与从业者,围绕首尔自行车共享系统的每小时租赁需求预测展开。数据源自首尔自行车共享计划,核心目标是借助温度、时间、假期、季节等变量,预测每小时所需…

2026/10/13 21:12:02 阅读更多 →
全自动代码演进架构:从AI辅助生成到验证闭环的工程实践

全自动代码演进架构:从AI辅助生成到验证闭环的工程实践

1. 从“帮我写”到“替我做”:AI辅助编程的能级跃迁这两年,我身边的开发团队几乎没有一个不用AI代码助手的。我自己也天天在编辑器里跟它打交道:补全函数、生成单测、写文档注释、改老接口……说实话,效率提升是实打实的&#xff…

2026/10/13 21:12:02 阅读更多 →
电影院会员管理系统开发实战:从需求边界到部署上线的完整复盘

电影院会员管理系统开发实战:从需求边界到部署上线的完整复盘

电影院会员管理系统这种项目,技术栈看起来不算新,但真到动手做的时候,"能跑"和"能交付"之间隔着很远的路。这篇文章我把整个项目的开发过程拆开讲,从需求边界、技术选型,到后端表设计和前端页面实…

2026/10/13 21:12:02 阅读更多 →
国产个人AI助手十天实测:端侧轻量化模型如何实现生活级嵌入

国产个人AI助手十天实测:端侧轻量化模型如何实现生活级嵌入

1. 项目概述:这不是一个“AI工具测评”,而是一次真实生活嵌入实验“我使用国产「个人 AI 助手」的十天|AI 上新”——这个标题里藏着三个关键信号:国产、个人、十天。它不是在比参数,不是在跑分,更不是照着…

2026/10/13 21:12:02 阅读更多 →
Java接口深度解析:从抽象类区别到JVM调用与动态代理设计

Java接口深度解析:从抽象类区别到JVM调用与动态代理设计

不少刚学Java的人第一次看到interface这个关键字,都会下意识把它当成“抽象类的平替”。毕竟两者都能声明抽象方法,都不能直接new,看起来确实像兄弟俩。但等我做项目做到后面才彻底明白,接口在Java里承担的工作,远比“…

2026/10/13 21:12:01 阅读更多 →
基于Django与大数据的药材销售预测及可视化系统全解析

基于Django与大数据的药材销售预测及可视化系统全解析

做中药材销售类项目这么大半年,最常被问的一句话是“这行业的数据分析到底该从哪下手”。说实话,中药材销售和普通快消品差挺远的,品类复杂、产地分散、价格波动受天气和行情影响大,很多团队连一套能用的预测系统都搭不出来。今天…

2026/10/13 21:11:00 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →