大家好,大家好,我是小楼!今天给大家带来一篇关于DubboIO交互的文章。这篇文章是同事写的。它用有趣的文字写出枯燥的知识点。通俗易懂,非常有趣,迫不及待的请作者授权,分享给大家。:一些有趣的问题Dubbo是一个优秀的RPC框架,它有一个错综复杂的线程模型。在这篇文章中,作者从自己浅薄的知识出发,分析了Dubbo的整个IO流程。在开始之前,我们先看看以下几个问题:业务方法执行后,数据包是否发送出去?netty3和netty4在线程模型上有什么区别?当数据包到达操作系统的套接字缓冲区时发生了什么?Provider输出的日志耗时很少,但是Consumer端超时了。我们如何解决问题?数据包是在物理层直接通过管道发送的吗?当Consumer业务线程在Condition上等待时,什么时候被唤醒?...下面我将以Dubbo2.5.3作为Consumer,以Dubbo2.7.3作为Provider来描述整个交互过程。从数据包的角度,我以第一人称来叙述。系好安全带,出发吧。趣味旅行1.Dubbo2.5.3Consumer发起请求我是一个小包,出生在一个叫Dubbo2.5.3Consumer的小镇,我的使命是传递信息,我也喜欢旅行。有一天,我会被派出去,据说要去一个地方,叫Dubbo2.7.3Provider。这一天,业务线程发起了一个方法调用。在FailoverClusterInvoker#doInvoke中选择了一个Provider,然后通过各种ConsumerFilter,通过Netty3管道,最后通过NioWorker#scheduleWriteIfNecessary方法,来到了NioWorker的writeTaskQueue队列。回头看主线程,发现他在等待DefaultFuture中的Condition。我不知道他在等什么,也不知道他要等多久。在writeTaskQueue队列里排了一会,看到netty3IO工作线程一直在不停的执行run方法。每个人都称这是一个无限循环。最后还是幸运的,NioWorker#processWriteTaskQueue选择了我,我被写到操作系统的Socket缓冲区,我在缓冲区里等待,反正时间够用,回想今天的游记,期间去掉了两个游记组,分别称为主线程和netty3IO工作线程。嗯,两个旅行团的服务都不错,效率很高。我简单的把今天的所见所闻记录下来,画成图。当然,我忽略了不重要的部分。2.操作系统发送数据包我在操作系统socketbuffer中,经过了很多神奇的事情。在一个叫做传输层的地方,我添加了目的端口号和源端口号。在一个叫网络层的地方,我添加了目的IP和源IP。同时,通过目的IP和掩码的AND运算,我发现“下一跳”的IP是在一个叫数据链路层的地方通过ARP协议添加到我的目标MAC地址和“下一跳”的源MAC地址是最有意思的,每次换缆车都要修改目标MAC地址和源MAC地址,后来问了同行业的数据包小伙伴,这个模式叫做“nexthop”,一个接一个跳。这里有很多数据包,大的单辆缆车,几个小的挤一辆缆车,可怕的是,如果尺寸是比较大,需要拆分做多条缆车(虽然这对我们的数据包问题来说不算什么),这个叫拆包贴,期间我们路过交换机和路由器,这些地方玩的很开心。当然也有不愉快的事情,就是拥堵,目的地缆车爆满,再想拉走也来不及了,只能等了。3.Provider端的体验并不轻松。我来到了目的地。坐上了一艘叫“零拷贝”的快艇,很快就到了netty4。Inboundhandler,我来到了AllChannelHandler的线程池,当然我有很多选择,但是我随机选择了一个目的地,这里会经过解码,一系列Filter,目的地“业务方法”,NettyCodecAdapter#InternalDecoder解码器功能很强大,拆包贴都能搞定。我会在AllChannelHandler的线程池中停留一段时间,所以也画了一张图记录下旅途。从此,我的旅行结束了,新的故事会随着新的数据包继续。4.Provider端生成一个新的数据包。我是一个数据包,出生在一个叫Dubbo2.7.3的小镇Provider。我的任务是唤醒命中注定的线程。接下来,我将开始一个名为Dubbo2.7.3Provider的地方的旅程。Dubbo2.5.3Consumer的地方。Provider业务方法执行后,业务线程通过io.netty.channel.AbstractChannelHandlerContext#writeAndFlush,然后通过io.netty.util.concurrent.SingleThreadEventExecutor#execute执行addTask,将任务放入队列io.netty.util.concurrent.SingleThreadEventExecutor#taskQueue我跟着io.netty.channel.AbstractChannelHandlerContext$WriteTask等待NioEventLoop启动。等待的时候,我记录下自己走过的脚步。在这里,我看到NioEventLoop是一个无限循环。它不断地从任务队列中取任务,执行任务AbstractChannelHandlerContext.WriteAndFlushTask,然后引导我们在socketbuffer中等待,乐此不疲。我似乎明白他有一种固执、追求极致的工匠精神。在io.netty.channel.AbstractChannel.AbstractUnsafe#write之后,我到达了操作系统套接字缓冲区。在操作系统层面,它和大多数数据包一样,也是到达目的地的缆车。5、到达dubbo2.5.3Consumer端。我在操作系统套接字缓冲区中等待了一段时间。我也乘坐“零拷贝”快艇,到达了真正的目的地dubbo2.5.3。Consumer,这里我发现NioWorker#run是一个死循环,然后执行NioWorker#processSelectedKeys,通过NioWorker#read读出来,就到了AllChannelHandler的线程池,这是一个业务线程池。我在这里等了一会,任务调度的时候,看到执行了com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#doReceived,同时执行了Condition的信号。我看到远处一条阻塞的线程被唤醒,我似乎明白,因为我的到来,一条沉睡的线程被唤醒,我想这应该是我生命的意义。至此,我的使命已经完成,这一趟旅程也结束了。总结netty3和netty4的线程模型下面根据两个包的readme来总结一下netty3和netty4的线程模型。1、netty3的写过程2、Netty4的读写过程说明:这里没有netty3的读过程,netty3的读过程和netty4一样,管道由IO线程执行。总结:netty3和netty4线程模型的区别在于编写过程。在netty3中,pipeline由业务线程执行,而在netty4中,pipeline不分读写,统一由IO线程执行。netty4中ChannelPipeline中的Handler链由I/O线程统一串行调度。不管是读操作还是写操作,netty3中的写操作都是由业务线程处理的。处理程序链。在netty4中,可以减少线程间上下文切换带来的时间消耗,而在netty3中,业务线程可以并发执行Handler链。如果有一些耗时的Handler操作,netty4的效率会很低,但是这些耗时操作可以考虑先在业务线程中执行,而不是在Handler中执行。由于可以并发执行业务线程,因此也可以提高效率。解决一些疑难问题我遇到了一些典型的疑难问题。比如Provider承诺的didi.log耗时正常,但Consumer端超时,有如下排查思路。didi.log的过滤器其实在最内层。通常不反映真正的业务方法实现。除了Provider的业务方向的执行,序列化也可能比较耗时,所以可以使用arthas监听最外层的方法org.apache.dubbo.remoting.transport.DecodeHandler#received来消除high的问题耗时的商业方法。Provider中的数据包写入是否耗时,监控io.netty.channel.AbstractChannelHandlerContext#invokeWrite方法,也可以通过netstat查看当前tcpsocket的一些信息,如Recv-Q、Send-Q、Recv-Q已到达接收缓冲区,但应用程序代码仍未读取数据。Send-Q表示已经到达发送缓冲区,但是对方还没有回应Ack数据。通常情况下,这两种数据是不会累积的。如果它们堆积起来,可能会出现问题。看ConsumerNioWorker#processSelectedKeys(dubbo2.5.3)方法是否耗时。整个链接的所有细节,直到最后……问题肯定是可以解决的。Epilogue在整个交互过程中,作者省略了一些线程栈调用的细节和源码细节,比如序列化和反序列化,dubbo如何读取完整的数据包,以及在业务方法执行之前过滤器是如何排序和分布的对,netty的Reactor模式是如何实现的。这些都是非常有趣的问题...
