逻辑集群
Dubbo里两个主要参与实体是provider和consumer,两者都是相对服务而言,前者是服务的具体实现,后者是服务的消费者。
服务在客户端被异化成提供同质的服务的逻辑集群,消费者的服务请求最终都会通过集群select出一个invoker进行远程调用,整个过程对用户是非感知的。
上图是客户端引用服务的完整过程,其中主要涉及5个实体:
- Directory提供目录服务,一组invoker必定会聚集在一个目录下。它实现NotifyListener接口,监听注册中心提供者变更事件,注册中心向其推送提供者变更。目录收到的是提供者的配置信息,基于提供者配置信息客户端生成invoker,并缓存在目录里。缓存是个方法名->invoker的map结构。没有指定methods参数的则key为‘*’,代表所有方法可用,但是优先级比指定方法名的invoker低。
- Invoker是对确定节点提供者的逻辑抽象,invoker封装了远程调用的实现细节,主要包括应用协议和传输协议等等。因为它基于提供者配置生成(配置包括但不仅限于提供者的地址和端口信息),因此它能确定指向一个节点。Invoker其实是一个边界,它是RPC协议的具体实现,这里暂且认为它是一个黑盒实体,在RPC一篇再做领域建模和细化。
- Reference引用远端服务,它并不确定指向一个节点的提供者,而是引用由多个提供同样服务能力的invoker组成的集群。它被提供给消费者,消费者的所有请求通过它最终被委派给invoker发起remoting调用。需要说明一点,Reference并不是dubbo里的ReferenceConfig实体,而是返回给消费者的服务代理。
- Cluster代表逻辑集群,它的背后是一组提供同质服务的节点。在dubbo里Cluster实体并不直接提供集群能力,它基于动态扩展生成特定HA策略的集群invoker,由集群invoker提供集群的相关能力。集群invoker也是一个invoker,它必定有且仅有一个目录,相应的也间接拥有0到多个invoker。
集群invoker的结构一般如下:
- 消费多个注册中心的服务,最终就如上图的嵌套结构。每个注册中心生成一个集群invoker,其下包含所有注册于该中心的invoker,而最终所有这些集群invoker又都会聚集在一个大的集群invoker之下。
- 消费单注册中心的服务,集群直接聚集invoker,此时就只会有一个集群invoker。
为了叙述方便,后文提到集群如果不做特殊说明则是指集群invoker。
- 集群通过动态扩展点弱依赖LoadBalance,并依赖后者提供负载均衡能力,后者从集群下的所有invoker里select一个可用的,select策略可以有多种,比如加权轮询、一致性hash等等。
Reference最终按以下结构将以上实体聚合起来
引用和集群(非invoker)是依赖关系,前者依赖后者生成集群invoker。集群invoker和普通invoker(图里假设应用协议是dubbo)都实现了接口invoker,但前者通过目录聚集了后者的。
调用链路上,reference将请求委派给集群,集群从目录下list所有invoker再由负载均衡select,最后由被选出的invoker发起remoting,如果失败则遵循当次集群HA协议重试或者快速失败等等。
注册中心
前面几章都或多或少提到了注册中心,注册中心是以URL流转驱动的。提供者将其配置以及地址信息通过注册中心送达到消费者,消费者再基于该配置生成对提供者的引用。理解注册中心必须首先理解URL,对URL还有疑问的可以参考上篇里的整体架构章节。
注册中心对订阅和发布的匹配是以path,以及version,group和classifier三个参数作为依据。因此提供者可以通过group和classifier提供差异化服务,消费者也可以通过这两者指定差异化服务。
zookeeper注册中心
接下来再以zookeeper注册中心举例说明一下注册中心的工作原理。
- 提供者export时,注册中心在zookeeper上按照/group/service/provider/url的结构写入配置信息;消费者consume时,注册中心则在zookeeper上provider节点上加上zk watcher。因此当provider节点下有写入时zookeeper就会及时将全量URL推送给注册中心,注册中心再对推送过来的URL与订阅URL进行version,group和classifier三个维度匹配,并确定是enable的服务后再推送给目录,目录会将当前所有invoker的URL信息与推送过来的URL信息做正交比较剔除重复的,对剩下的做引用。
- Dubbo还支持消费者全量订阅,只需要配置service为“”,注册中心就会为消费者在group目录下创建zk watcher。消费者还可以通过配置category为“”全量监听服务的所有目录信息;如果想监听特定的几个目录,则只需要使用使用","分隔特定目录即可。
- 一般而言zookeeper注册中心创建的都是ephemeral节点,因此在对应的写入节点offline后相应节点也会消失,因此zookeeper此时能正确推送服务下线。注册中心只是一个概念,它依赖注册中心的物理实现,而并不是有专门节点run 注册中心服务,因此注册中心的写入都是由export节点发起的。
RPC
RPC分层
RPC组件分成6层,自上而下分别是应用层,交换层,传输层,转码/序列化层、事件处理层、事件分发层。
端
Dubbo抽象出端(endpoint)的概念,端是个点,点对点之间可以进行双向传输。在此基础上扩展出通道(Channel),客户端(Client),服务端(Server)三个概念。在传输层,客户端和服务端更多体现为语义上的区别,并不区分请求和应答职责,二者拥有的都是发送能力。但客户端拥有体现其特有职责的重连能力,连接肯定都是由客户端发起,它一般是在连接超时时由心跳任务发起。客户端没有显式的连接以及断连语义,在客户端被初始化出来时就默认开启并建立与服务端连接,且通过定时任务维护通道的连接状态。因此客户端和服务端除了重连以外都只有close和send两个影响网络出书的动作。
服务端和客户端以通道作为传输桥梁,通道和客户端是一一对应的关系,但和服务端是多对一的关系。因为客户端和通道一对一的关系,所以在设计层面可以处理成客户端继承通道。但不管是客户端还是服务端,它们和通道的关系都体现为聚合,通过聚合的通道进行消息传输。那为什么要让客户端继承通道,只能请带着疑问继续往下看。
交换层在传输层基础上扩展了请求和应答语义,体现信息交换的含义。它增加了三个实体,分别是ExchangeChannel,ExchangeClient和ExchangeServer。交换层通道增加request动作特指客户端到服务端的请求。交换层客户端和服务端也是点,但是有方向的点,区分明确的请求和应答职责。
客户端状态
客户端有4个动作,分别是open, connect, disconnect和close,与之相应的有5个状态,除了4个动作伴随的状态以外还有halt,客户端本身并不体现该状态,该状态只是出于对心跳超时的描述需要加入。交换层有定时任务做心跳探测,超时会发起重连。
服务端状态相对简单很多,服务端不会主动发起连接或者断连,因此只有open和close动作,相应的也只有opened和closed两个状态。服务端也有心跳任务,心跳超时只会自动关闭server。
事件处理
ChannelHandler用于网络事件流式处理,它支持5种事件:connected,sent,received,disconnected,caught。分别对应5种网络事件:连接,发送消息,接收消息,断连和异常捕获。需要注意一点,它们对应的是网络事件,并不是端的动作或者状态,只能说端的动作会引起某个网络事件的触发,例如客户端发起connect,引起服务端connected事件。但并不是每个端动作都会触发网络事件,比如open,比如close。
处理器实现是个包装类,通过多层包装编排处理器执行顺序,对事件进行流式处理。它内聚在传输层client或者server里,在收到相应网络事件后被及时触发,并依次执行。它的执行顺序和客户端恰好相反,由下而上,自传输层到应用层。
ExchangeHandler为交换层扩展了reply语义,它并不是一种状态,只是一个动作特指服务端到客户端的应答,它发生在触发的received事件处理执行到交换层时。
下图是处理器在通用情况下的逻辑结构
- 集合消息的接收(消息体是个list),会循环list分别委派,非集合消息或者其他类型动作直接转发,这是dubbo应用协议专用。
- 心跳时间粘连,按事件类型将读或者写事件的时间关联到channel属性,供心跳探测任务做心跳超时判断。
- 事件分发,它其实是事件分发层的具体实现,不同分发策略会使用不同的分发处理器分发事件,比如图上是默认分发策略--完全线程池分发,除此以外还有完全同步调用,只接受消息时线程池分发,除发送以外都线程池分发以及使用线程池顺序串行并有流控警告的分发方式。
- 逆序列化消息,dubbo应用协议专用。它并不是codec的具体实现,只是特定支持dubbo应用协议。
- 交换层应答语义支持,该处理实体不是交换事件处理实体,但明确是ExchangeHandler。它提供交换层的语义支持。例如服务端收到消息后即时应答,客户端收到应答唤醒pending线程返回结果以及服务端事件处理有执行异常时将异常再发送给客户端。
- 服务端应答内容处理,它是交换事件处理实体。在客户端,是ExchangeHandlerAdapter的默认实现,所有方法均为空实现;在服务端,视具体应用协议有不同交换层处理实体实现,但不论哪种实现都会保证通过serviceKey找到exporter,也就是找到本地服务,执行并返回执行结果。
RPC
介绍完事件处理实体和端后再回过头来看端的静态结构图,AbstractPeer是Server和Client各种实现的父类,它既是端又是事件处理实体,因此无论是Server还是Client的实现都有双重角色。
RPC子域以Invoker为根,它由Protocol组件基于动态决策生成,各组件按分层结构依次聚合。在端的一节提过客户端同时也是个通道,交换层通道聚合的传输层通道就可以是传输层客户端。这样处理的必要性在于语义的完整性,请求在交换层经由客户端到通道,通道又依赖传输层通道的传输能力传输。如果不这么处理,则请求直接通过传输通道发送出去而不会经由客户端。
如果交换通道内聚传输客户端或者交换传输端,这两者不管哪种设计都破坏了端和通道的场景语义,也破坏了交换通道本身的独立性,端的任何变化都可能会引起它的变化。
Client会默认启动连接状态检查的定时任务,通道状态非连接时会使用新通道连接。
最后以thrift应用协议以及netty传输协议举例,描绘一下整个RPC的交互。
- 交换客户端将请求发送到传输层后就返回ResponseFutre,后者的get方法会进入堵塞等待,直到结果返回被唤醒。
- 事件处理实体被NettyHandler聚合,后者作为netty的网络事件处理器,在相应网络事件触发后被依次执行。比如请求发出后,以及回复抵达后等等。本节开篇说过Client的双重角色,NettyHandler持有的事件处理实体就是NettyClient本身。
- 步骤15在回答抵达后被执行,它唤醒在堵塞等待返回结果的线程,并把值以及请求状态写入返回变量中。
- NettyClient和NettyServer做了简化处理,并没有体现NettyChannel。