查看: 2395|回复: 0

[Java语言] 深入浅出微服务框架dubbo(四):设计篇

发表于 2018-1-18 08:00:02
四、 设计篇

本篇是《深入浅出微服务框架dubbo》的终篇

4.1 线程模型

netty+zookeeper+curator+dubboProtocol+hession2seralization组合
图片描述
图片描述
图片描述
图片描述

4.2 协议数据格式

这里引用官网的一张图:
图片描述
第三行代表了协议头,Magic,serializationId,event(是否是事件数据),twoWay(twoWay代表需要响应,oneWay代表不需要响应),req/res(请求还是响应),status(响应状态,对于req为空)
Id(requestId,响应时带上,返回时通过id找到future对象唤醒线程,通过future找到invocation对象得到响应类型比如(Map,String)帮助完成序列化),dataLength(代表实体数据长度,用于判断数据是否接收完整,接收完整才能开始decode)

Dubbo协议请求数据实体举例:
RpcInvocation [methodName=sayHello,
parameterTypes=null,
arguments=null,
attachments={dubbo=2.0.0, path=com.alibaba.dubbo.demo.DemoService, version=0.0.0}]

methodName用于寻找provider(同一服务不一定暴露同样的方法)
parameterTypes和arguments就不用说了,和methodName一起,传送到provider可以找到方法名,attachments的path用于在provider寻找到exporter,继而找到invoker

dubbo协议响应数据实体举例:
RpcResult [result=Hello world1, response form provider: 172.16.221.1:20880, exception=null]
result是响应结果;exception是异常信息,最终传播到用户调用代码

4.3 通信

通信方式由阻塞式IO过度到非阻塞式IO,设计思想也从同步等待过度到事件通知机制,对于client-server模式来讲,有以下问题需要处理。
1、连接池:BIO模式下,比如数据库连接池、httpclient连接池、jedis连接池,连接个数有限,各个线程之间需要竞争连接(虽然现在业界有无锁化jdbc连接池HikariCP,但也避免不了线程竞争)。
在NIO模式下,以netty为例,用户线程可以同时向一个channel写数据(实际是写入队列),由iothread负责将队列里面的数据发送出去,数据发送统一IOthread管理,避免了线程对连接的竞争。一般netty模式下,client创建一个连接数组,每次轮训取出去发送。
2、超时处理:BIO在socket设置参数connectionTimeout、socketTimeout可以分别设置连接超时和读超时,但是在NIO下没有这种参数设置,就需要自己来判断,比如连接超时可以通过netty框架中建立连接得到返回的future对象,通过get(timetou)来处理超时;读超时就需要构造一个future对象,接收到数据后唤醒调用get方法的线程,get(timeout)超时则抛出超时异常;如果是异步处理,就需要将请求事件(可以封装到future中)缓存起来,定期扫描,对超时请求进行处理.
3、长连接保持:对于一个tcp连接,如果不关闭的话,连接会一直保持。但是server端为了避免创建大量连接,需要将空闲连接剔除,通过发心跳可以检测client连接是否正常,保持长连接,将长时间收不到没有响应的client连接剔除。
4、连接状态检测:有的时候无法收到来自远程的FIN信号,比如网络不可用情况下,这个时候需要主动发送数据来验证连接是否正常,这个时候就需要client端发起心跳检测。也可以通过每次在发送请求之前发送一个简单命令来检测,比如dbcp连接可以发送”select 1”,jedis连接发送PING。
5、重连机制:连接断开需要重新创建连接,对于client连接,连接断开有可能是由于server端发起主动关闭,还有可能网络故障client在发送数据时发生IO异常主动关闭,这个时候就需要重建连接保证数据正常发送和接收。
6、等待响应:同步等待or异步callback处理,既然NIO是非阻塞式的,那么为了最大化利用这一特性带来的好处,异步callback处理是首选,但是由于业务需要,在发送完数据需要在当前线程下等待结果的返回,由于大多数NIO通信框架IO线程和用户线程是分开的,为了达到同步得到响应结果就需要引入Feture,当前线程调用get方法(实际上是进入await),IO线程收到数据唤醒用户线程,进而去获取数据。为了达到高性能,建议发送多次请求最后再一起get等待结果,或者使用异步+回调方式。

dubbo在通信模块上采用长连接,做了很多处理,包括server端、client端双端的心跳发送响应,server端对空闲连接检测和剔除,client端对空闲连接销毁重建(每次nettyClient对应的channel引用会发生变化,这也是为了dubbo代码里面出现很多getAndAddNettyChannel代码的原因),client对超时请求的扫描,client端做了对关闭连接进行销毁和重连,可以说是非常全面,以达到高性能通信的目的。
但是我认为在在节省带宽和减少不必要的压力时可以采用短+长连接结合,设置空闲连接时间在5分钟,而不需要做心跳检测,也不需要异步重建连接,server端只需要关闭超过5分钟未发送数据的连接,client端在收到FIN时自动关闭并将连接状态设为false,下次发送数据时重新创建连接。这样做的好处是减少server端的压力并降低连接数量,坏处就是如果5分钟未发送数据需要同步创建连接

4.4 事件处理

IO线程是有限的,并且一个线程上关联许多channel,这些channel的事件处理如果交给iothread来做,比如收到查询请求,然后去数据库做查询操作,将导致其它channel上的事件长时间得不到处理或者一个channel上其它事件长时间得不到处理。因此引入线程池单独对事件做处理,比如netty提供了orderedMemoryAwareThreadPoolExecutor。Dubbo也引入了线程池,并支持decode可以在线程池中进行处理。

4.5 注册

Dubbo在注册中心连接恢复做了以下重试,节点创建重试、watch设置重试、通知重试,以zookeeper为例,在连接重连session未失效的情况下,watch以及节点都不会失效,但是在session失效的情况下,watch和节点都会消失,这就是需要有重试机制在保证session失效的时候保证节点依然存在,watch依然监听失效前所监听的节点。Dubbo的注册模块代码非常庞大和复杂,如果不考虑那么多的扩展,仅仅使用zookeeper当作注册中心,并且用curator,代码量将大大减少,比如通过curator提供的nodeCache,pathChildrenCache可以很方便的编写监听代码,代码无须重新注册watch(甚至session失效时),只需要在收到重连事件保证节点还在即可。



回复

使用道具 举报