Dubbo中Handler(ChannelHandler)的5种状态:
状态描述connectedChannel已经被创建disconnectedChannel已被断开sent消息被发送received消息被接收caught捕获到异常Dubbo针对每个特性都会实现对应的ChannelHandler:
Handler作用ExchangeHandlerAdapter用于查找服务方法并调用HeaderExchangeHandler封装处理Request/Response和Telnet调用能力DecodeHandler支持在Dubbo线程池中做解码ChannelHandlerDispatcher封装多Handler广播调用AllChannelHandler支持Dubbo线程池调用业务方法HeartbeatHandler支持心跳处理MultiMessageHandler支持流中多消息报文批处理ConnectionOrderedChannelHandler单独线程池处理RCP的连接和断开MessageOnlyChannelHandler仅在线程池处理接收报文,其他事件在I/O线程处理WrappedChannelHandler基于内存key-value存储封装和共享线程池能力,比如记录线程池等NettyServerHandler封装Netty服务端事件,处理连接、断开、读取、写入和异常等NettyClientHandler封装Netty客户端事件,处理连接、断开、读取、写入和异常等Dubbo中提供了大量的handler去承载特性和扩展,这些Handler最终会和底层通信框架做关联,比如Netty等。一次完整的RPC调用贯穿了一系列的Handler,如果直接挂载到底层通信框架(Netty),因为整个链路比较长,则需要出发大量链式查找和事件,不仅低效,而且浪费资源。
在Dubbo框架中,NettyServerHandler实现了ChannelInboundHandler、NettyClientHandler实现了ChannelOutboundHandler。Dubbo通过装饰器模式层包装handler,从而不需要将每个Handler都追加到Pipeline中。在NettyServer和NettyClient中最多有3个Handler,分别是编码、解码和NettyServerHandler或NettyClientHandler。
在DubboProtocol中通过内部类继承自ExchangeHandlerAdapter,完成服务提供方Invoker实例的查找并进行服务的真实调用:
上面的Handler是实现是触发业务方法调用的关键,在服务暴露时服务端已经按照特定的规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中,客户端调用过来时必须携带相同信息构造的key,找到对应Exporter然后调用。
在①中查找当前已经暴露的服务。在②中主要包含实例的Filter和真实业务对象,当触发invoker#invoke方法时,就会执行具体的业务逻辑。在DubboProtocol中,跟踪getInvoker调用,会发现在服务端唯一标识的服务是由4部分组成的:端口、接口名、接口版本和接口分组。
在①中主要获取协议暴露的端口,比如Dubbo协议默认的端口为20880。在②中获取客户端传递过来的接口名称(大部分场景都是接口名)。在③中主要根据服务端口、接口名、接口分组和接口版本构造唯一的key。在④中简单地从HashMap中取出对应的Exporter并调用Invoker属性值。
Dubbo为了编织这些Handler,适应不同的场景,提供了一套可以定制的线程模型。为了使概念更氢气,我么描述的I/O线程是指底层直接负责读写报文,比如Netty线程池。Dubbo中提供的线程池负责业务方法调用,称为业务线程。如果一些事件逻辑可以很快执行完成,比如做个标记而已,则可以直接在I/O线程中处理。如果事件处理耗时或阻塞,比如读写数据库操作等,则应该将耗时或阻塞的任务转到业务线程池执行。因为I/O线程用于接收请求,如果I/O线程饱和,则不会接收新的请求。
Dubbo线程模型:
Dispatcher就是线程池派发器。这里需要注意的是,Dispatcher真实的职责是创建具有线程派发能力的ChannelHandler,比如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHandler等,其本身并不具备线程派发能力。
Dispatcher术语Dubbo中的扩展点,这个扩展点用来动态产生Handler,以满足不同额场景。目前Dubbo支持6种策略调用:
线程分发策略:
分发策略分发实现作用allAlldispatcher将左右I/O事件交给Dubbo线程池处理,Dubbo默认启用connectionConnectionOrderedDiispatcher单独线程池处理连接断开事件,和Dubbo线程池分开directDirectDispatcher所有方法调用和事件处理在I/O线程中,不推荐executionExecutionDispatcher只在线程池处理接收请求,其他事件在I/O线程池中messageMessageOnlyChannelHandler只在线程池处理请求和响应事件,其他事件在I/O线程池中mockdispatcherMockDispatcher默认返回Null具体业务方需要根据使用场景的不同的策略。建议使用默认策略即可,如果在TCP连接中需要做安全加密或校验,则可以使用ConnectionOrderDispatcher策略。如果引入新的线程池,则不可避免地导致额外的线程切换,用户可以在Dubbo配置总指定dispatcher属性让具体策略生效。
在Dubbo框架内部,所有方法都会被抽象成Request/Response,每次调用(一次会话)都会创建一个请求Request,如果是方法调用则会返回一个Response对象。 HeaderExchangeHandler用来处理这种场景,它主要负责以下4种事情:
更新发送和读取请求时间戳判断请求格式或编解码是否出错,并响应客户端失则的具体原因。处理Request请求和Response正常响应。支持Telnet调用 ①负责响应读取时间并更新时间戳,在Dubbo心跳处理中会使用当前值并判断是否超过空闲时间。②主要处理事件类型,目前主要处理readonly事件,用于Dubbo优雅停机。当注册中心反注册元数据时,因为网络原因,客户端不能及时感知注册中心事件,服务端会发送readonly报文告知下线。④处理收到的Response响应,告知业务调用方。⑤校验客端不支持Telnet调用,因为只有服务提供方暴露服务才有意义。这里有个小改进,因为客户端支持异步参数回调,但为什么这里不能支持Telne调用呢?异步参数回调客户端实际上也会暴露一个服务,因此针对这种场景Telnet应该是允许调用的。⑥触发Telnet调用,并将字符串返回给Telnet客户端。处理请求和响应(HeaderExchangeHandler#handleRequest,handleResponse):
处理请求报文:
在处理请求时,因为在编解码层报错会透传到Handler,所以在①中首先会判断是否因为请求报文不正确,如果发生错误,则服务端会将具体异常包装成字符串返回,如果直接使用异常对象,则可能造成无法序列化的错误。在②中触发Dubbo协议方法调用,并且把方法调用返回值发送给客户端。如果调用发生未知错误,则会通过③做容错并返回。当发送请求时,会在DefaultFuture中保存请求对象并阻塞请求线程,在④中会唤醒阻塞线程并将Response中的结果通知调用方。
Dubbo默认客户端和服务端都会发送心跳报文,用来保持TCP长连接状态。在客户端和服务端,Dubbo内部开启一个线程循环扫描并检测连接是否超时,在服务端如果发现超时则会主动关闭客户端连接,在客户端发现超时则会主动重新创建连接。默认心跳检测时间是60秒,具体应用可以通过heartbeat进行配置。
Dubbo在服务端和客户端都复用心跳实现代码,抽象成HeartBeatTask任务进行处理:
①遍历所有的Channel,在服务端对应的是所有客户端连接,在客户端对应的是服务端连接②主要忽略已经关闭的Socket连接。③判断当前TCP连接是否空闲,如果空闲就发送心跳报文。目前判断是否空闲的,根据Channel是否有读或写来决定,比如1分钟内没有读或写就发送心跳报文。④处理客户端超时重新建立TCP连接,目前的策略是检查是否在3分钟内(用户可以设置)都没有成功接收或发送报文。如果在服务端检测则会通过⑤主动关闭远程连接。