Ⅰ 微服务架构 | *3.5 Nacos 服务注册与发现的源码分析
参考资料 :
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服务原理与实战》
《B站 尚硅谷 SpringCloud 框架开发教程 周阳》
为方便理解与表达,这里把 Nacos 控制台和 Nacos 注册中心称为 Nacos 服务器(就是 web 界面那个),我们编写的业务服务称为 Nacso 客户端;
Nacos 客户端将自己注册进 Nacos 服务器。《1. 服务如何注册进 Nacos 注册中心》主要从 Nacos 客户端角度解释如何发送信息给 Nacos 服务器;《2. Nacos 服务器注册服务》主要从 Nacos 服务器角度解释注册原理;
《3. 客户端查询所有服务实例》将从服务消费者和提供者的角度,解释服务消费者如何获取提供者的所有实例。服务消费者和提供者都是 Nacos 的客户端;
《4. 客户端监听 Nacos 服务器以动态获取服务实例》从消费者客户端角度出发监听 Nacos 服务器,以动态获知提供者的变化;
Ⅱ 闲聊注册中心——ZK、Eureka、Sofa-Registry
最开始服务之间的调用借助的是域名,域名其实是个好东西,使用起来很方便,但所有调用请求都得走域名解析和负载均衡,相对来说性能就差一点,而且这些夹在中间的零部件会演变成性能瓶颈和潜在的单点风险,后来大家一比较发现还不如直接端到端调用,那么就需要一个东西把调用链的两端连起来,这就是注册中心。
注册中心提供服务的注册发现,用来连接调用链路的 Provider 和 Consumer 这两个端点。一个注册中心得管理好服务的扩缩容、故障转移、流量分配这类的核心功能,自身同样需要高可用,妥善解决跨地域部署情况下注册中心节点之间的数据一致性,因此我更倾向于认为注册中心是一个偏向体系化的产品,它的价值依赖于与之相匹配的服务规模和治理体系,它的思想虽然产生得很早,但是规模化地落地并不容易,偏小的服务规模本身对于注册中心的依赖并不明显,实现的手段也没有定式;大型服务又会对注册中心的定位、功能提出更苛刻的需求。
究竟是选择强一致性还是高可用性,当下的主流技术选型倾向于后者,但这并不是说明 CP 组件没有存在的意义,而是 AP 组件更契合互联网对于注册中心的定位。
互联网系统高可用永远是摆在首位的东西,因为系统不可用的时间通常跟财产损失直接挂钩。服务列表在短时间少了一两个节点、多了一两个节点,这点流量倾斜对于大多数服务来说感知并不明显,只需要注册中心在某个短暂的时间窗口内重新达到一致性的状态就可以,注册中心不应该在运行时影响到处于正常调用的两个端点之间的连通性。
CP 的代表就是 zookeeper ,至今 zookeeper 也是非常流行的用于注册中心的组件,这在一定程度上要感谢 bbo 。 bbo 在 zookeeper 中是用一个树形结构的两个节点,分别维护服务的 Provider 和 Comsumer 的,列表注册在这两个节点下面,服务的健康检查依赖 zookeeper 提供的临时节点特性,实际上跟 session 的生命周期绑定在一起,但临时节点的问题就是数据易失,一旦集群故障节点重启丢失服务列表,可能造成服务大面积瘫痪,电商新贵 PDD 就曾经出现过这个问题。
CP 最关键的问题在于无法支持机房容灾,例如 ABC 三个机房,机房 C 和其他两个机房产生了网络隔离,部署在机房 C 的 zookeeper 节点是无法提供写入的,这意味着机房 C 部署的 Provider 不能扩缩容、不能重启,最好在故障期间一直保持原样,否则 Provider 任何操作都会是高危的,会直接影响到部署在机房 C 的 Comsumer 。
顺带说一句,我认为 zookeeper 真挺好的,它本身的定位就是个分布式协调服务,并不是专门为注册中心设计的,你不能拿评价注册中心的眼光去评价它,你要的功能 zookeeper 基本上都有,借助 Curator 拿来就可以用,提供了 election ,提供了 ZAB , TPS 低了点(其实也可以了)但是也满足大部分服务规模。
Eureka —— 奈飞的网红组件, AP 注册中心的代表。 Eureka 1.x 今天回头看来是个很牵强的实现,因为它的架构从诞生之日起,就意味着将来数据规模增长之后大概率会出问题,集群整体可伸缩性很低,每扩容一台 Eureka 节点意味着整体点对点之间需要多一份数据流,而这种 Peer 点对点机制的数据流很容易打满网卡,造成服务端压力。根据公开的资料, Eureka 在实例规模五千左右时就会出现明显的性能瓶颈,甚至是服务不可用的情况。
但是从另一个方面来说, Eureka 的架构清晰、运维部署都很方便,而且 Eureka 作为 SpringCloud 推荐的注册中心实现,国内用户数目也相当可观,可见在服务规模恰当的情况下其性能并没有问题,从携程的分享资料也可以看出,其内部的注册中心也是类似于 Eureka 的实现,可见没有绝对完美的架构设计,适合自己、满足业务需求才是最主要的。
Eureka 服务端多节点架构其实有点去中心化的意思,有意保持了每个节点的无状态特性,但是代价就是每个节点都持有全量数据,新增的数据可以往任意一个节点写入,然后由这个节点向其他节点广播,最终达到一致性。
数据都没有持久化,仅保存在内存中,带来了更好的读写性能、更短的响应时间,但是无法处理数据瓶颈,大节点扩容造成的数据同步存在打满网卡的风险,这点跟 redis 集群很像。客户端 30s 定期上报心跳、客户端缓存服务列表这些都没什么好谈的, Eureka 有个很有意思的点是,它的集群节点实现了一个自我保护机制,用来预测到底是集群出问题还是客户端出问题,实现很简单但是思想挺实用的。
Eureka 2.0 的设计文档上体现了读写分离集群的思想,本质上是为了提升集群性能和容量,但是非常遗憾,目前 2.0 的状态是 Discontinued ,短时间应该不会有成熟的产品出现。
虽然没有 Eureka 2.0 ,但是可以从蚂蚁开源的 Sofa-Registry 中了解类似思想的实现,Sofa-Registry 脱胎于阿里的 ConfigServer ,从公开的资料显示,国内阿里应该是最早做注册中心读写分离实践的。
这种架构带来的直接好处就是可以支持海量数据,第一层的 session 集群可以无限水平扩展,彼此之间完全不需要通信, session 集群在内存中维护了注册中心需要的一切拓扑关系,客户端仅仅连接一部分 session 集群中的机器,如果某台 session 机器挂了客户端会选择另一台重连;背后的 data 集群则通过分片存储所有的源数据,分片之间通过主从副本保证高可用,再将源数据推送到 session 集群保存下来,供客户端读取。
缺点也很明显,这种架构人力投入、运维成本肯定高于 Eureka 。
这几乎是注册中心不可避免的问题,高可用、机房容灾、网络延迟等都会催生出要求注册中心节点能够跨地域部署,这会引申出另一个问题就是如何做数据同步。
这种情况下已经不可能通过客户端注册或者是集群节点数据流复制来保证异地注册中心集群之间的数据一致,而是会研发单独的数据同步组件,通过跨地域专线实现地域间注册中心的数据同步,如果专线断开,此时各自地域的注册中心依旧可以独立服务于本地域内服务的调用,等到专线恢复,二者的数据继续进行同步合并纠正,最终达到一致性。对这个知识点感兴趣的可以了解下 Nacos-Sync 组件,是 Nacos 项目专门推出的开源数据同步服务。
文章略过了一些注册中心通用的概念,例如数据模型的分层、服务列表本地缓存、健康检查的方式、推拉权衡等,我认为叙述这些细节的意义并不大。
很多时候,理解某个组件我认为最重要的是理清它的架构演进过程,这是宝贵的经验财富,你可以抓住并学习每次架构演进的方向和原因。
Ⅲ “SpringCloud原理”Ribbon核心组件以及运行原理万字源码剖析
大家好,本文我将继续来剖析SpringCloud中负载均衡组件Ribbon的源码。本来我是打算接着OpenFeign动态代理生成文章直接讲Feign是如何整合Ribbon的,但是文章写了一半发现,如果不把Ribbon好好讲清楚,那么有些Ribbon的细节理解起来就很困难,所以我还是打算单独写一篇文章来剖析Ribbon的源码,这样在讲Feign整合Ribbon的时候,我就不再赘述这些细节了。好了,话不多说,直接进入主题。
这是个很简单的东西,就是服务实例数据的封装,里面封装了服务实例的ip和端口之类的,一个服务有很多台机器,那就有很多个Server对象。
ServerList是个接口,泛型是Server,提供了两个方法,都是获取服务实例列表的,这两个方法其实在很多实现类中实现是一样的,没什么区别。这个接口很重要,因为这个接口就是Ribbon获取服务数据的来源接口,Ribbon进行负载均衡的服务列表就是通过这个接口来的,那么可以想一想是不是只要实现这个接口就可以给Ribbon提供服务数据了?事实的确如此,在SpringCloud中,eureka、nacos等注册中心都实现了这个接口,都将注册中心的服务实例数据提供给Ribbon,供Ribbon来进行负载均衡。
通过名字也可以知道,是用来更新服务注册表的数据,他有唯一的实现,就是PollingServerListUpdater,这个类有一个核心的方法,就是start,我们来看一下start的实现。
通过这段方法我们可以看出,首先通过isActive.compareAndSet(false, true)来保证这个方法只会被调用一下,然后封装了一个Runnable,这个Runnable干了一件核心的事,就是调用传入的updateAction的doUpdate方法,然后将Runnable扔到了带定时调度功能的线程池,经过initialDelayMs(默认1s)时间后,会调用一次,之后都是每隔refreshIntervalMs(默认30s)调用一次Runnable的run方法,也就是调用updateAction的doUpdate方法。
所以这个类的核心作用就是每隔30s会调用一次传入的updateAction的doUpdate方法的实现,记住这个结论。
IRule是负责负载均衡的算法的,也就是真正实现负载均衡获取一个服务实例就是这个接口的实现。比如说实现类RandomRule,就是从一堆服务实例中随机选取一个服务实例。
就是一个配置接口,有个默认的实现DefaultClientConfigImpl,通过这个可以获取到一些配置Ribbon的一些配置。
这个接口的作用,对外主要提供了获取服务实例列表和选择服务实例的功能。虽然对外主要提供获取服务的功能,但是在实现的时候,主要是用来协调上面提到的各个核心组件的,使得他们能够协调工作,从而实现对外提供获取服务实例的功能。
这个接口的实现有好几个实现类,但是我讲两个比较重要的。
BaseLoadBalancer
核心属性
allServerList:缓存了所有的服务实例数据
upServerList:缓存了能够使用的服务实例数据。
rule:负载均衡算法组件,默认是RoundRobinRule
核心方法
setRule:这个方法是设置负载均衡算法的,并将当前这个ILoadBalancer对象设置给IRule,从这可以得出一个结论,IRule进行负载均衡的服务实例列表是通过ILoadBalancer获取的,也就是 IRule 和 ILoadBalancer相互引用。setRule(rule)一般是在构造对象的时候会调用。
chooseServer:就是选择一个服务实例,是委派给IRule的choose方法来实现服务实例的选择。
BaseLoadBalancer这个实现类总体来说,已经实现了ILoadBalancer的功能的,所以这个已经基本满足使用了。
说完BaseLoadBalancer这个实现类,接下来说一下DynamicServerListLoadBalancer实现类。DynamicServerListLoadBalancer继承自BaseLoadBalancer,DynamicServerListLoadBalancer主要是对BaseLoadBalancer功能进行扩展。
DynamicServerListLoadBalancer
成员变量
serverListImpl:上面说过,通过这个接口获取服务列表
filter:起到过滤的作用,一般不care
updateAction:是个匿名内部类,实现了doUpdate方法,会调用updateListOfServers方法
serverListUpdater:上面说到过,默认就是唯一的实现类PollingServerListUpdater,也就是每个30s就会调用传入的updateAction的doUpdate方法。
这不是巧了么,serverListUpdater的start方法需要一个updateAction,刚刚好成员变量有个updateAction的匿名内部类的实现,所以serverListUpdater的start方法传入的updateAction的实现其实就是这个匿名内部类。
那么哪里调用了serverListUpdater的start方法传入了updateAction呢?是在构造的时候调用的,具体的调用链路是调用 restOfInit -> (),这里就不贴源码了
所以,其实DynamicServerListLoadBalancer在构造完成之后,默认每隔30s中,就会调用updateAction的匿名内部类的doUpdate方法,从而会调用updateListOfServers。所以我们来看一看 updateListOfServers 方法干了什么。
这个方法实现很简单,就是通过调用 ServerList 的getUpdatedListOfServers获取到一批服务实例数据,然后过滤一下,最后调用updateAllServerList方法,进入updateAllServerList方法。
其实很简单,就是调用每个服务实例的setAlive方法,将isAliveFlag设置成true,然后调用setServersList。setServersList这个方法的主要作用是将服务实例更新到内部的缓存中,也就是上面提到的allServerList和upServerList,这里就不贴源码了。
其实分析完updateListOfServers方法之后,再结合上面源码的分析,我们可以清楚的得出一个结论,那就是默认每隔30s都会重新通过ServerList组件获取到服务实例数据,然后更新到BaseLoadBalancer缓存中,IRule的负载均衡所需的服务实例数据,就是这个内部缓存。
从DynamicServerListLoadBalancer的命名也可以看出,他相对于父类BaseLoadBalancer而言,提供了动态更新内部服务实例列表的功能。
为了便于大家记忆,我画一张图来描述这些组件的关系以及是如何运作的。
说完一些核心的组件,以及他们跟ILoadBalancer的关系之后,接下来就来分析一下,ILoadBalancer是在ribbon中是如何使用的。
ILoadBalancer是一个可以获取到服务实例数据的组件,那么服务实例跟什么有关,那么肯定是跟请求有关,所以在Ribbon中有这么一个抽象类,,这个是用来执行请求的,我们来看一下这个类的构造。
通过上面可以看出,在构造的时候需要传入一个ILoadBalancer。
中有一个方法executeWithLoadBalancer,这个是用来执行传入的请求,以负载均衡的方式。
这个方法构建了一个LoadBalancerCommand,随后调用了submit方法,传入了一个匿名内部类,这个匿名内部类中有这么一行代码很重要。
这行代码是根据给定的一个Server重构了URI,这是什么意思呢?举个例子,在OpenFeign那一篇文章我说过,会根据服务名拼接出类似 http:// ServerA 的地址,那时是没有服务器的ip地址的,只有服务名,假设请求的地址是 http:// ServerA/api/sayHello ,那么reconstructURIWithServer干的一件事就是将ServerA服务名替换成真正的服务所在的机器的ip和端口,假设ServerA所在的一台机器(Server里面封装了某台机器的ip和端口)是192.168.1.101:8088,那么重构后的地址就变成 http:// 192.168.1.101:8088/api/ sayHello ,这样就能发送http请求到ServerA服务所对应的一台服务器了。
之后根据新的地址,调用这个类中的execute方法来执行请求,execute方法是个抽象方法,也就是交给子类实现,子类就可以通过实现这个方法,来发送http请求,实现rpc调用。
那么这台Server是从获取的呢?其实猜猜也知道,肯定是通过ILoadBalancer获取的,因为submit方法比较长,这里我直接贴出submit方法中核心的一部分代码
就是通过selectServer来选择一个Server的,selectServer我就不翻源码了,其实最终还是调用ILoadBalancer的方法chooseServer方法来获取一个服务,之后就会调用上面的说的匿名内部类的方法,重构URI,然后再交由子类的execut方法来实现发送http请求。
所以,通过对的executeWithLoadBalancer方法,我们可以知道,这个抽象类的主要作用就是通过负载均衡算法,找到一个合适的Server,然后将你传入的请求路径 http:// ServerA/api/sayHello 重新构建成类似 http:// 192.168.1.101:8088/api/ sayHello 这样,之后调用子类实现的execut方法,来发送http请求,就是这么简单。
到这里其实Ribbon核心组件和执行原理我就已经说的差不多了,再来画一张图总结一下
说完了Ribbon的一些核心组件和执行原理之后,我们再来看一下在SpringCloud环境下,这些组件到底是用的哪些实现,毕竟有写时接口,有的是抽象类。
Ribbon的自动装配类:RibbonAutoConfiguration,我拎出了核心的源码
RibbonAutoConfiguration配置类上有个@RibbonClients注解,接下来讲解一下这个注解的作用
SpringClientFactory是不是感觉跟OpenFeign中的FeignContext很像,其实两个的作用是一样的,SpringClientFactory也继承了NamedContextFactory,实现了配置隔离,同时也在构造方法中传入了每个容器默认的配置类RibbonClientConfiguration。至于什么是配置隔离,我在OpenFeign那篇文章说过,不清楚的小伙伴可以后台回复feign01即可获得文章链接。
配置优先级问题
优先级最低的就是FeignContext和SpringClientFactory构造时传入的配置类
至于优先级怎么来的,其实是在NamedContextFactory中createContext方法中构建时按照配置的优先级一个一个传进去的。
RibbonClientConfiguration提供的默认的bean
接下来我们看一下RibbonClientConfiguration都提供了哪些默认的bean
配置类对应的bean,这里设置了ConnectTimeout和ReadTimeout都是1s中。
IRule,默认是ZoneAvoidanceRule,这个Rule带有过滤的功能,过滤哪些不可用的分区的服务(这个过滤可以不用care),过滤成功之后,继续采用线性轮询的方式从过滤结果中选择一个出来。至于这个propertiesFactory,可以不用管,这个是默认读配置文件的中的配置,一般不设置,后面看到都不用care。
至于为什么容器选择NacosServerList而不是ConfigurationBasedServerList,主要是因为这个配置类是通过@RibbonClients导入的,也就是比SpringClientFactory导入的RibbonClientConfiguration配置类优先级高。
ServerListUpdater,就是我们剖析的PollingServerListUpdater,默认30s更新一次BaseLoadBalancer内部服务的缓存。
那么在springcloud中,上图就可以加上注册中心。
三、总结
本文剖析了Ribbon这个负载均衡组件中的一些核心组件的源码,并且将这些组件之间的关系一一描述清楚,同时也剖析了在发送请求的时候是如何通过ILoadBalancer获取到一个服务实例,重构URI的过程。希望本篇文章能够让你知道Ribbon是如何工作的。
Ⅳ 百度AI人脸注册源码详解
1、起点 AppConfig.client.addUser(image, imageType, groupId, userId, options) 调用人脸注册api,从addUser进入
2、那就要看一下_request请求是怎么发送的,点进去看
3、至此调用基本结束,接下来看里面用到的几个方法:
Ⅳ Dubbo2.7.8服务启动过程源码分析
可以从 git clone,导入intellij idea,子模块bbo-demo可以调试。
也可以新建Maven工程,自写demo。 推荐(要动手) 。
本文以新建为例,并 推荐 引入nacos做为注册中心(可选其他注册中心)。
新建接口:
新建接口实现类AnnotationProviderServiceImpl:
新建启动类AnnotationProviderBootstrap.
bbo-provider.properties配置文件如下:
整体目录结构:
上述工程是基于annotation方式创建的,所以启动类为
设置断点,并启动。
AnnotatedBeanDefinitionReader的190-192行:
回到84行:
AbstractApplicationContext的524行:
上述是初始化各种Processor,会做两部分操作,可实时查看控制台输出日志。
在的50行设置断点。
回到546行:
来到DubboBootstrap的893行start方法, 核心 所在:
initialize();主要是一些初始化操作。
重点关注exportServices
在exportServices中:
ServiceConfig被翻译成:
<bbo:service beanName="ServiceBean:com.zp.bbo.api.HelloService" />
sc.export()过程:
见ServiceConfig183行:
init serviceMetadata之后真正执行doExport动作,最终来到ServiceConfig304行:doExportUrls
这里做了几件事:
1.registerProvider:将HelloService放入 providers 和providersWithoutGroup。
2.获取注册中心地址:registryURLs
3.doExportUrlsFor1Protocol
4.exportLocal
这里根据不同的Protocol启动不同的Server。可调试进入DubboProtocol的export方法。
5.在RegistryProtocol中注册到注册中心
调试至FailbackRegistry240行:
进入NacosRegistry152行,进行注册。
进入NacosNamingService139行:registerInstance
最终NamingProxy168行registerService完成注册。
6.exported() 分发ServiceConfigExportedEvent事件
那么到此,bbo的服务提供者注册过程分析就已经全部完成。
接下来将进行bbo其他源码分析,包括负载均衡,路由等等。
Ⅵ Eureka源码浅读---自我保护机制
Eureka源码采用1.7.2版本
本人小白,此文为本人阅读源码笔记,如果您读到本文,您需要自己甄别是否正确,文中的说明只代表本人理解,不一定是正确的!!!
自我保护机制设计的初衷是防止服务注册服务因为本地网络故障,长时间未接受到心跳请求,造成错误的移除大量服务实例,其实调用服务还是可用的
自我保护机制是和自动故障移除联系在一起的,针对的移除实例也是自动故障移除
在服务故障移除的方法中有这样一个判断,当返回false时候,直接返回,不进行故障实例的摘除
进入该方法
关于获取上一分钟心跳总数,Eureka Server内部采用的是定时线程进行统计,使用两个AtomicLong进行保存当前和上一分钟的心跳总数
该方法初始化了运行了定时调度的线程进行统计,默认执行间隔为1min,执行流程:
那么当前的心跳总数是怎么计算的呢,直接看心跳的renew()方法,是否嵌入了计数器累计操作
如上所示,当接收到心跳时,当前心跳计数器进行了递增操作
而getNumOfRenewsInLastMin()获取上一分钟心跳总数就是获取lastBucket数量,再找下该定时任务启动的入口
和自动故障移除的定时同时启动的,那么lastBucket代表了上一分钟的心跳总数
接下来,我们需要看看期望每分钟最小心跳总数的由来:
numberOfRenewsPerMinThreshold最开始的初始化计算是在Eureka Server初始化计算的,使用当前Server拉取到的服务实例总数 * 0.85
在openForTraffic()方法中使用初始化拉取的服务实例总数作为基数标准进行计算,(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold()) -> count * 2 * 0.85,
集群模式下,count为其他节点中已注册的服务实例总数,单节点就为0
下面我们看看在注册中心接收到注册,下线等请求执行时,维护numberOfRenewsPerMinThreshold
注册,当前实例数量+2,下线,当前实例数量-2,然后再次*0.85,计算期望每分钟最小心跳数
在Eureka Server中有专门的定时任务进行更新numberOfRenewsPerMinThreshold,默认每15min执行一次
主要流程如下:
注意,自动服务故障移除没有进行numberOfRenewsPerMinThreshold的更新
<font color= 'blue'>服务故障实例的摘除需要判断当前是否处于自我保护模式,而自我保护模式的默认是开启(isSelfPreservationModeEnabled),需要判断上一分钟的心跳总数是否大于期望每分钟最小心跳数,如果在15分钟内,累计丢失了15%以上的节点心跳,那么Eureka Server就会认为当前所处的网络环境异常,从而处于自动保护模式,故障实例将不会移除,再等待15min后,进行expectedNumberOfRenewsPerMin的基于当前服务实例的重新计算后,自我保护模式才会关闭!</font>
自我保护服务开启模拟:
Ⅶ Nacos注册中心之概要设计
在之前的文章中分析了Nacos配置中心,配置中心的核心是配置的创建、读取、推送。
注册中心的核心比配置中心多一个 服务探活 模块,他俩的相似度非常高,甚至阿里内部的注册中心就叫 ConfigServer 。
Nacos注册中心打算分成几个模块来分析,本文重点在于 概要设计 ,基于2.0.0版本。
用Nacos的源码来搭建源码阅读和调试环境,可参考 《Nacos配置中心模块详解》 Nacos调试环境搭建 部分。
其中 JVM参数可以指定只启动Naming模块,也可以不指定,默认全都启动。
example模块下将NamingExample复制一份进行测试。
客户端视角的服务发现模型(注意:服务端视角的模型定义与客户端视角有区别)包含以下几点内容:
他们的关系如下
除了上述的三层模型外,Nacos注册中心和配置中心有着一样的namespace设计,与client绑定,可隔离环境,租户。
Nacos 2.0 为ephemeral不同的实例提供了两套流程:
本文从总体上分析了Nacos 2.0的模型设计、接口设计以及交互流程,读完后对Nacos的服务发现有一个整体上的认识。
后续篇幅会从细节入手,如bbo Nacos扩展、一致性协议、探活、CMDB扩展等逐一进行分析。
Ⅷ Dubbo——Registry服务注册
注册中心(Registry)在微服务架构中的作用举足轻重,有了它,服务提供者(Provider) 和消费者(Consumer) 就能感知彼此。
Registry 只是 Consumer 和 Provider 感知彼此状态变化的一种便捷途径而已,它们彼此的实际通讯交互过程是直接进行的,对于 Registry 来说是透明无感的。Provider 状态发生变化了,会由 Registry 主动推送订阅了该 Provider 的所有 Consumer,这保证了 Consumer 感知 Provider 状态变化的及时性,也将和具体业务需求逻辑交互解耦,提升了系统的稳定性。
Dubbo 中存在很多概念,但有些理解起来就特别费劲,如本文的 Registry,翻译过来的意思是“注册中心”,但它其实是应用本地的注册中心客户端,真正的“注册中心”服务是其他独立部署的进程,或进程组成的集群,比如 ZooKeeper 集群。本地的 Registry 通过和 ZooKeeper 等进行实时的信息同步,维持这些内容的一致性,从而实现了注册中心这个特性。另外,就 Registry 而言,Consumer 和 Provider 只是个用户视角的概念,它们被抽象为了一条 URL 。
RegistryFactory 就是产生一个注册中心的工程,它有个自适应的方法getRegistry,那么我们知道bbo会通过javassist动态产生一个RegistryFactory$Adaptive类,并且getRegistry方法的内部实现大致是如下:
它通过传入的URL的protocol协议字段排判断是什么类型注册中心。例如,url的protocol的协议是zookeeper,那么就会根据SPI的ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension("zookeeper")得到一个产生ZooKeeper注册中心的工厂,也就是ZookeeperRegistryFactory,而ZookeeperRegistryFactory这个类的getRegistry就是返回一个Zookeeper注册中心。
可以看出其语义,一个注册中心Registry是一个节点(extends Node),并且它具有注册服务(extends RegistryService)的功能。
bbo支持如下这些注册中心zookeeper、consul、etcd3、eureka、nacas、redis、sofa,那么就会产生相应如下的Registry:ZookeeperRegistry、ConsulRegistry、EtcdRegistry、NacosRegistry、RedisRegistry、SofaRegistry。类图如下:
所以我们知道,这些注册中心都是继承FailbackRegistry,这个FailbackRegistry其意思就是说,如果一个服务注册到当前某个注册中心注册失败后,可会在后台产生一个daemon线程,定时的把注册失败服务重新注册,并且有一定的重试限制。
在上面的类图中我们并没有发现有个名为EurekaRegistry这样的类,因为实现了另一个接口ServiceDiscovery方式,类名为EurekaServiceDiscovery来进行服务发现。
bbo的协议是通过名为org.apache.bbo.rpc.Protocol来进行抽象的,那么注册协议也是一样的,是通过org.apache.bbo.registry.integration.RegistryProtocol来表达的,继承org.apache.bbo.rpc.Protocol。RegistryPrtocol是扩展点Protocol的具体实现,会一次调用其setter方法来注入其需要的属性,RegistryPrtocol其中有个属性就是RegistryFactory,那么就要为它注入一个具体的RegistryFactory,那么这个具体的RegistryFactory工厂就是上面的RegistryFactory$Adaptive。因为注入的属性对象会从SpringExtensionFactory和SpiExtensionFactory工厂中查询,刚好RegistryFactory也是一个扩展点,所以会在SpiExtensionFactory找出,并且SpiExtensionFactory工厂的实现如下:
所以知道是返回一个自适应的扩展点,即RegistryFactory$Adaptive。
Protocol协议具有导出服务export()功能,和引用服务refer()功能。在RegistryProtocol中,在这个2个方法内就有对服务注册到注册中心的操作。
在服务导出中,首先要有一个认知,做bbo服务暴露的时候,我们有2中方式,一种是通过注解的方式:
@DubboService、@Service(非spring的)。或者通过xml的方式<bbo:service />。
不管采用哪一种方式,最终需要暴露的服务首先会包装成一个ServiceBean的对象。这个ServiceBean 持有具体需要服务注册的对象ref。ServiceBean的类图如下:
服务导出也是是一个繁琐的过程,本文只讨论其服务导出引入与注册中心交互。
DubboBootstrap是一个bbo框架启动的帮助类,他有一个start()方法,在该方法的内部就会调用exportServices()用于导出服务,和调用referServices()进行引用服务。
一般使用bbo框架的都会引入Spring框架,Spring框架有一个事件监听机制,bbo正是监听Spring的上下文刷新事件ContextRefreshedEvent,来启动Dubbo服务的。这个服务监听类就是。
registry方法定位到FailbackRegistry,主要作用当服务注册失败后,可以在后端线程重试。
接下来分析AbstractRegistry 的作用和FailbackRegistry的重试机制,并且详细剖析ZookeeperRegistry。
首先,直接引出这个类的作用,该类主要把服务提供者信息缓存本地文件上,文件目录是:当前用户目录下的/.bbo/bbo-registry- {hos}-${port}.cache。
在解读源码前,先阅读下AbstractRegistry类的成员变量,从成员变量中可以看到这个类是怎么完成数据的本地化存储的。
上面的注释已经非常的清晰了,这里就不在描述,需要关注的是notify()这个函数,所以当每个服务注册和订阅时,首次创建注册中心都会进行notify操作。具体来看下notify方法。
从上面可以知道,把消费端的订阅的服务信息存入了file文件中,doSaveProperties就是文件操作,不进行分析。再一次强调下,消费端订阅时,会订阅某个具体服务下3个节点(providers,configurations,routers)。
接着,FailbackRegistry继承自AbstractRegistry。
其构造函数如下,可以得知除了调用AbstractRegistry构造方法外,并且创建一个HashedWheelTimer类型的定时器。
并且FailbackRegistry 成员记录一组注册失败和订阅失败的集合,然后通过retryTimer定式扫描这些失败集合,重新发起订阅和注册。
下面是失败集合:
参考:
https://www.cnblogs.com/liferecord/p/13462175.html
https://www.cnblogs.com/liferecord/p/13497411.html
https://www.cnblogs.com/Cubemen/p/12294377.html
https://blog.csdn.net/cold___play/article/details/107007130
https://www.jianshu.com/p/75931e545b36
Ⅸ 如何更好地学习bbo源代码
1、Dubbo与Spring的整合 Dubbo在使用上可以做到非常简单,不管是Provider还是Consumer都可以通过Spring的配置文件进行配置,配置完之后,就可以像使用 spring bean一样进行服务暴露和调用了,完全看不到bbo api的存在。这是因为bbo使用了spring提供的可扩展Schema自定义配置支持。在spring配置文件中,可以像、这样进行配置。 META-INF下的spring.handlers文件中指定了bbo的xml解析类:DubboNamespaceHandler。像前面的被解 析成ServiceConfig,被解析成ReferenceConfig等等。 2、jdk spi扩展 由于Dubbo是开源框架,必须要提供很多的可扩展点。Dubbo是通过扩展jdk spi机制来实现可扩展的。具体来说,就是在META-INF目录下,放置文件名为接口全称,文件中为key、value键值对,value为具体实现类 的全类名,key为标志值。由于bbo使用了url总线的设计,即很多参数通过URL对象来传递,在实际中,具体要用到哪个值,可以通过url中的参 数值来指定。 Dubbo对spi的扩展是通过ExtensionLoader来实现的,查看ExtensionLoader的源码,可以看到Dubbo对jdk spi做了三个方面的扩展:
(1)jdk spi仅仅通过接口类名获取所有实现,而ExtensionLoader则通过接口类名和key值获取一个实现;
(2)Adaptive实现,就是生成一个代理类,这样就可以根据实际调用时的一些参数动态决定要调用的类了。
(3)自动包装实现,这种实现的类一般是自动激活的,常用于包装类,比如Protocol的两个实现类:ProtocolFilterWrapper、ProtocolListenerWrapper。 3、url总线设计 Dubbo为了使得各层解耦,采用了url总线的设计。我们通常的设计会把层与层之间的交互参数做成Model,这样层与层之间沟通成本比较大,扩展起来也比较麻烦。因此,Dubbo把各层之间的通信都采用url的形式。比如,注册中心启动时,参数的url为: registry://0.0.0.0:9090?codec=registry&transporter=netty 这就表示当前是注册中心,绑定到所有ip,端口是9090,解析器类型是registry,使用的底层网络通信框架是netty。
二、Dubbo启动过程
Dubbo分为注册中心、服务提供者(provider)、服务消费者(consumer)三个部分。 1、注册中心启动过程 注册中心的启动过程,主要看两个类:RegistrySynchronizer、RegistryReceiver,两个类的初始化方法都是start。 RegistrySynchronizer的start方法:
(1)把所有配置信息load到内存;
(2)把当前注册中心信息保存到数据库;
(3)启动5个定时器。 5个定时器的功能是: (1)AutoRedirectTask,自动重定向定时器。默认1小时运行1次。如果当前注册中心的连接数高于平均值的1.2倍,则将多出来的连接数重定向到其他注册中心上,以达到注册中心集群的连接数均衡。 (2)DirtyCheckTask,脏数据检查定时器。作用是:分别检查缓存provider、数据库provider、缓存consumer、数据库 consumer的数据,清除脏数据;清理不存活的provider和consumer数据;对于缓存中的存在的provider或consumer而数 据库不存在,重新注册和订阅。 (3)ChangedClearTask,changes变更表的定时清理任务。作用是读取changes表,清除过期数据。 (4)AlivedCheckTask,注册中心存活状态定时检查,会定时更新registries表的expire字段,用以判断注册中心的存活状态。如果有新的注册中心,发送同步消息,将当前所有注册中心的地址通知到所有客户端。 (5)ChangedCheckTask,变更检查定时器。检查changes表的变更,检查类型包括:参数覆盖变更、路由变更、服务消费者变更、权重变
Ⅹ Kitex 源码解析 —— 将服务注册进入注册中心的细节
Kitex为 字节跳动 内部的 Golang 微服务 RPC 框架,具有 高性能 、 强可扩展 的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。
这次我们可以从 官方示例 中的 easy_note 这个demo 开始分析,因为它基本展示了 Kitex 的基本使用方法。
下面图例为官方在 demo 中展示的架构图,通过简单的分析可得, note , user 通过 注册中心 (Etcd) 进行注册 , api 通过 注册中心 来发现 note , user 两个 rpc 服务, 并进行业务处理。
从 kitex-examples/hello 这个最简单示例分析,从 cloudwego/kitex 的快速上手可知,这里用了最简单的 直链 来链接 server 和 client。而这次的 easy_note 中使用了 注册中心 来作为 服务之间的 桥梁 (middleware), 为什么不使用之前的方式而是使用了注册中心?
我们搜索一下注册中心的作用,可知: 服务注册中心的主要作用就是“服务的注册”和“服务的发现”
我们将服务交给注册中心管理,虽然可以避免处理复杂的手动管理,我们也许需要还要考虑更多问题,例如:
这次目标之一就是来解析解析服务是如何在服务启动时进行 注册 ( Register ) 这个操作的, 这次我们从 easy_note/cmd/user 这个服务开始分析, 因为它是被 注册 进入 Etcd 的服务之一。
我们从其中的 main.go 开始下手,以下的内容是经过简化后的文件,是实现配置服务,启动服务的文件
由注释可知 WithRegister() 为配置 注册信息 的函数 ,那为什么直接就看这个函数呢?
一是因为主要配置注册的主要逻辑在其中,二是因为 With... 的函数结构都是大同小异,十分相似的。
再然后我们进入 kitex/server/option.go ,先看看 di.Push(fmt.Sprintf("WithRegistry(%T)", r)) 这一行,
这个 *util.Slice 是什么 ?进去看看?
进入 kitex/pkg/utils/slice.go , 我发现它很简短。但是它好眼熟,它好像是一个非常常见的数据结构 —— Stack (栈) !
在这个文件之下有它的 slice__test.go 文件 ,看到这里的朋友可以去试验一下是否这个 Slice 和我的想法是否一致,大家看文章是要思考的嘛!最好可以动动手!
我们再进入 o.Registry = r 这一行,可以得知 Options 用于初始化 server, Option 用于配置 Options (我觉得这种命名方式很巧妙,我感觉基本达到了 见名知意 的作用),里面东西很多,我们今天只看 Register 部分
到了这里我们可以暂停思考一下,到达这一步是怎么个过程呢?是通过 main.go/user.NewServer() 的方法进来的。
那 NewServer() 的作用是什么?是用于配置初始化服务器的 可选参数 ,
配置完了参数什么时候生效呢 ( Register 是什么时候发生的呢) ?其实配置的实现就在 main.go NewServer() 的下一句, Run() !
进入Run方法的实现,可以得知 register 是发生在 server 启动成功后 的,停止也是会向 Etcd 进行注销操作的 (大家可以在同文件的 Stop() 中查看)
至此 服务完成了向 Etcd 的注册,我忽略了许多其他细节,这些细节也很有意思,希望大家可以自己试着探索
这次文章其实向大家分析了如何配置服务,以及向注册中心进行注册的方法和时机。
虽然省略了许多细节,但是通过这篇文章可以学到什么呢?