转自:http://manzhizhen.iteye.com/blog/2391177

在上回《Dubbo源代码实现六》中我们已经了解到,对于Dubbo集群中的Provider角色,有IO线程池(默认无界)和业务处理线程池(默认200)两个线程池,所以当业务的并发比较高,或者某些业务处理变慢,业务线程池就很容易被“打满”,抛出“RejectedExecutionException: Thread pool is EXHAUSTED! ”异常。当然,前提是我们每给Provider的线程池配置等待Queue。

既然Provider端已经抛出异常,表明自己已经受不了了,但线上我们却发现,Consumer无动于衷,发出的那笔请求还在那里傻傻的候着,直到超时。这样极其容易导致整个系统的“雪崩”,因为它违背了fail-fast原则。我们希望一旦Provider由于线程池被打满而无法收到请求,Consumer应该立即感知然后快速失败来释放线程。后来发现,完全是Dispatcher配置得不对,默认是all,我们应该配置成message。

我们从源码角度来看看到底是咋回事,这里先假设我们用的是Netty框架来实现IO操作,上回我们已经提到过,NettyHandler、NettyServer、MultiMessageHandler、HeartbeatHandler都实现了ChannelHandler接口,来实现接收、发送、连接断开和异常处理等操作,目前上面提到的这四个Handler都是在IO线程池中按顺序被调用,但HeartbeatHandler调用后下一个Handler是?这时候就要Dispatcher来上场了,Dispatcher是dubbo中的调度器,用来决定操作是在IO中执行还是业务线程池执行,来一张官方的图(http://dubbo.io/user-guide/demos/线程模型.html):

上图Dispatcher后面跟着的ThreadPool就是我们所说的业务线程池。Dispatcher分为5类,默认是all,解释也直接参考官方截图:

因为默认是all,所以包括请求、响应、连接、断开和心跳都交给业务线程池来处理,则无疑加大了业务线程池的负担,因为默认是200。每种Dispatcher,都有对应的ChannelHandler,ChannelHandler将Handler的调动形成调用链。如果配置的是all,那么接下来上场的就是AllChannelHandler;如果配置的是message,那么接下来上场的就是MessageOnlyChannelHandler,这些ChannelHandler都是WrappedChannelHandler的子类,WrappedChannelHandler默认把请求、响应、连接、断开、心跳操作都交给Handler来处理:

protected final ChannelHandler handler;

public void connected(Channel channel) throws RemotingException {

handler.connected(channel);

}

public void disconnected(Channel channel) throws RemotingException {

handler.disconnected(channel);

}

public void sent(Channel channel, Object message) throws RemotingException {

handler.sent(channel, message);

}

public void received(Channel channel, Object message) throws RemotingException {

handler.received(channel, message);

}

public void caught(Channel channel, Throwable exception) throws RemotingException {

handler.caught(channel, exception);

}

很显然,如果直接使用WrappedChannelHandler的处理方式,那么Handler的调用会在当前的线程中完成(这里是IO线程),我们看看AllChannelHandler内部实现:

public void connected(Channel channel) throws RemotingException {

ExecutorService cexecutor = getExecutorService();

try{

cexecutor.execute(new ChannelEventRunnable(channel, handler ,ChannelState.CONNECTED));

}catch (Throwable t) {

throw new ExecutionException("connect event", channel, getClass()+" error when process connected event ." , t);

}

}

public void disconnected(Channel channel) throws RemotingException {

ExecutorService cexecutor = getExecutorService();

try{

cexecutor.execute(new ChannelEventRunnable(channel, handler,ChannelState.DISCONNECTED));

}catch (Throwable t) {

throw new ExecutionException("disconnect event", channel, getClass()+" error when process disconnected event ." , t);

}

}

public void received(Channel channel, Object message) throws RemotingException {

ExecutorService cexecutor = getExecutorService();

try {

cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));

} catch (Throwable t) {

throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);

}

}

public void caught(Channel channel, Throwable exception) throws RemotingException {

ExecutorService cexecutor = getExecutorService();

try{

cexecutor.execute(new ChannelEventRunnable(channel, handler ,ChannelState.CAUGHT, exception));

}catch (Throwable t) {

throw new ExecutionException("caught event", channel, getClass()+" error when process caught event ." , t);

}

}

可以看出,AllChannelHandler覆盖了WrappedChannelHandler所有的关键操作,都将其放进到ExecutorService(这里指的是业务线程池)中异步来处理,但唯一没有异步操作的就是sent方法,该方法主要用于应答,但官方文档却说使用all时应答也是放到业务线程池的,写错了?这里,关键的地方来了,一旦业务线程池满了,将抛出执行拒绝异常,将进入caught方法来处理,而该方法使用的仍然是业务线程池,所以很有可能这时业务线程池还是满的,于是悲剧了,直接导致下游的一个HeaderExchangeHandler没机会调用,而异常处理后的应答消息正是HeaderExchangeHandler#caught来完成的,所以最后NettyHandler#writeRequested也没有被调用,Consumer只能死等到超时,无法收到Provider的线程池打满异常。

从上面的分析得出结论,当Dispatcher使用all时,一旦Provider线程池被打满,由于异常处理也需要用业务线程池,如果此时运气好,业务线程池有空闲线程,那么Consumer将收到Provider发送的线程池打满异常;但很可能此时业务线程池还是满的,于是悲剧,异常处理和应答步骤也没有线程可以跑,导致无法应答Consumer,这时候Consumer只能苦等到超时!

这也是为什么我们有时候能在Consumer看到线程池打满异常,有时候看到的确是超时异常。

为啥我们设置Dispatcher为message可以规避此问题?直接看MessageOnlyChannelHandler的实现:

public void received(Channel channel, Object message) throws RemotingException {

ExecutorService cexecutor = executor;

if (cexecutor == null || cexecutor.isShutdown()) {

cexecutor = SHARED_EXECUTOR;

}

try {

cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));

} catch (Throwable t) {

throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);

}

}

没错,MessageOnlyChannelHandler只覆盖了WrappedChannelHandler的received方法,意味着只有请求处理会用到业务线程池,其他的非业务操作直接在IO线程池执行,这不正是我们想要的吗?所以使用message的Dispatcher,不会存在Provider线程池满了,Consumer却还在傻等的情况,因为默认IO线程池是无界的,一定会有线程来处理异常和应答(如果你把它设置成有界,那我也没啥好说的了)。

所以,为了减少在Provider线程池打满时整个系统雪崩的风险,建议将Dispatcher设置成message:

<!--StartFragment--> <!--EndFragment-->

<dubbo:protocol name="dubbo" port="8888" threads="500" dispatcher="message" />

Dubbo 源代码分析八:再说 Provider 线程池被 EXHAUSTED的更多相关文章

  1. 从源代码分析Universal-Image-Loader中的线程池

    一般来讲一个网络访问就需要App创建一个线程来执行,但是这也导致了当网络访问比较多的情况下,线程的数目可能积聚增多,虽然Android系统理论上说可以创建无数个线程,但是某一时间段,线程数的急剧增加可 ...

  2. Universal-Image-Loader完全解析--从源代码分析Universal-Image-Loader中的线程池

    一般来讲一个网络访问就需要App创建一个线程来执行,但是这也导致了当网络访问比较多的情况下,线程的数目可能积聚增多,虽然Android系统理论上说可以创建无数个线程,但是某一时间段,线程数的急剧增加可 ...

  3. Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

  4. ScheduledThreadPoolExecutor源码分析-你知道定时线程池是如何实现延迟执行和周期执行的吗?

    Java版本:8u261. 1 简介 ScheduledThreadPoolExecutor即定时线程池,是用来执行延迟任务或周期性任务的.相比于Timer的单线程,定时线程池在遇到任务抛出异常的时候 ...

  5. HIPPO-4J 1.3.0 正式发布:支持 Dubbo、RibbitMQ、RocketMQ 框架线程池

    文章首发在公众号(龙台的技术笔记),之后同步到个人网站:xiaomage.info Hippo-4J 距离上一个版本 1.2.1 已经过去一个月的时间.在此期间,由 8 位贡献者 提交了 170+ c ...

  6. Dubbo源代码分析(三):Dubbo之服务端(Service)

    如上图所看到的的Dubbo的暴露服务的过程,不难看出它也和消费者端非常像,也须要一个像reference的对象来维护service关联的全部对象及其属性.这里的reference就是provider. ...

  7. rnnlm源代码分析(八)

    系列前言 參考文献: RNNLM - Recurrent Neural Network  Language Modeling Toolkit(点此阅读) Recurrent neural networ ...

  8. 多线程学习笔记八之线程池ThreadPoolExecutor实现分析

    目录 简介 继承结构 实现分析 ThreadPoolExecutor类属性 线程池状态 构造方法 execute(Runnable command) addWorker(Runnable firstT ...

  9. Netty 源码解析(五): Netty 的线程池分析

    今天是猿灯塔“365篇原创计划”第五篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty ...

随机推荐

  1. Python学习之旅—生成器与迭代器案例剖析

    前言 前面一篇博客笔者带大家详细探讨了生成器与迭代器的本质,本次我们将实际分析一个具体案例来加深对生成器与迭代器相关知识点的理解. 本次的案例是一个文件过滤操作,所做的主要操作就是过滤出一个目录下的文 ...

  2. HDU 2064 汉诺塔III (递推)

    题意:.. 析:dp[i] 表示把 i 个盘子搬到第 3 个柱子上最少步数,那么产生先把 i-1 个盘子搬到 第3个上,再把第 i 个搬到 第 2 个上,然后再把 i-1 个盘子, 从第3个柱子搬到第 ...

  3. 洛谷 - P2657 - windy数 - 数位dp

    https://www.luogu.org/problemnew/show/P2657 不含前导零且相邻两个数字之差至少为2的正整数被称为windy数. 这道题是个显然到不能再显然的数位dp了. 来个 ...

  4. java 发送get,post请求

    package wzh.Http; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStr ...

  5. python 可迭代对象与迭代器之间的转换

    列表: >>> l = [1, 2, 3, 4] >>> l_iter = iter(l) >>> l_iter <list_iterato ...

  6. bzoj 3872: [Poi2014]Ant colony【树形dp+二分】

    啊我把分子分母混了WA了好几次-- 就是从食蚁兽在的边段成两棵树,然后dp下去可取的蚂蚁数量区间,也就是每次转移是l[e[i].to]=l[u](d[u]-1),r[e[i].to]=(r[u]+1) ...

  7. bzoj 4310: 跳蚤【后缀数组+st表+二分+贪心】

    先求一下SA 本质不同的子串个数是\( \sum n-sa[i]+1-he[i] \),按字典序二分子串,判断的时候贪心,也就是从后往前扫字符串,如果当前子串串字典序大于二分的mid子串就切一下,然后 ...

  8. (七)SpringBoot使用PageHelper分页插件

    二:添加PageHelper依赖 <dependency> <groupId>com.github.pagehelper</groupId> <artifac ...

  9. Linux下tcp服务器创建的步骤

    创建一个socket,使用函数socket() socket(套接字)实质上提供了进程通信的端点,进程通信之前,双方首先必须建立各自的一个端点,否则没有办法通信.通过socket将IP地址和端口绑定之 ...

  10. Setting up IPS/inline for Linux in Suricata

    不多说,直接上干货! 见官网 https://suricata.readthedocs.io/en/latest/setting-up-ipsinline-for-linux.html Docs » ...