1 背景概述

线上事故:在做活动促销的时候,交易中台的商品服务发生了个别节点的宕机而此时间段内QPS并没有超过告警配置的线程数阈值1500。

于是决定做一次压测,当对自己的商品服务做压测20000个请求时,从监控看到客户端线程池(DubboClientHandler)飙升到800左右、而服务端线程池(DubboServerHandler)只有70左右,抛出线程已耗尽的异常。

当前配置如下:

1)dubbo版本为一直在用的2.5.5版本。

2)当前线程池配置为cached模式的ThreadPool,对应的SPI为:com.alibaba.dubbo.common.threadpool.support.cached.CachedThreadPool

<dubbo:protocol name="dubbo" port="8888" threads="5000" dispatcher="all"/>

从Grafana监控看,ClientThreadPool非常多,最高达到了7000+。所以很明显这个threads属性的设置并没生效

于是,业务方的疑问是:

1)为何请求20000时,客户端和服务端线程池从监控图表看差距甚大?两者的差别是什么?

2)两者分工协作的机制是怎样的?设定的这个阈值有没有生效、是作为总和统一分配dispatch还是独立分配管理?

3)自定义业务线程池设置的大小有没有效果?

带着这些问题,作为架构师的我马上来了兴趣,决定做一次比较完整的案件分析。

2 案件分析

2.1 追根溯源

2.1.0 Dubbo常用线程池

Dubbo的线程模型中可使用4种线程池

  • CachedThreadPool
  • LimitedThreadPool
  • FixedThreadPool
  • EagerThreadPool

​​​​​​​

线程主要执行2种逻辑,一是普通IO事件,比如建立连接,断开连接,二是请求IO事件,执行业务逻辑。
在Dubbo的Dispatcher扩展点会使用到这些线程池,Dispatcher这个扩展点用于决定Netty ChannelHandler中的那些事件在Dubbo提供的线程池中执行。

Dubbo默认的底层网络通讯使用的是Netty,服务提供方NettyServer使用两级线程池,其中 EventLoopGroup(boss) 主要用来接受客户端的链接请求,并把接受的请求分发给 EventLoopGroup(worker) 来处理,boss和worker线程组我们称之为IO线程。
如果服务提供方的逻辑能迅速完成,并且不会发起新的IO请求,那么直接在IO线程上处理会更快,因为这减少了线程池调度。但如果处理逻辑很慢,或者需要发起新的IO请求,比如需要查询数据库,则IO线程必须派发请求到新的线程池进行处理,否则IO线程会阻塞,将导致不能接收其它请求。

2.1.1 理解线程模型的区别

根据请求的消息类被IO线程处理还是被业务线程池处理,Dubbo提供了下面几种线程模型:

all : (AllDispatcher类)所有消息都派发到业务线程池,这些消息包括请求/响应/连接事件/断开事件/心跳等,这些线程模型如下图:

direct : (DirectDispacher类)所有消息都不派发到业务线程池,全部在IO线程上直接执行

message : (MessageOnlyDispatcher类)只有请求响应消息派发到业务线程池,其他连接断开事件/心跳等消息,直接在IO线程上执行。

本案件是使用的AllDispatcher模式(默认模式),可知所有事件都直接交给业务线程池进行处理了。cached是缓存线程池,空闲一分钟自动删除,需要时重建,设置的5000从源码看实际上并不会去读取:

1

2

3

4

5

6

7

8

public Executor getExecutor(URL url) {

    String name = url.getParameter("threadname""Dubbo");

    int cores = url.getParameter("corethreads"0);

    int threads = url.getParameter("threads"2147483647);

    int queues = url.getParameter("queues"0);

    int alive = url.getParameter("alive"60000);

    return new ThreadPoolExecutor(cores, threads, (long)alive, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 new SynchronousQueue() : (queues < 0 new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));

}

从int threads = url.getParameter("threads", 2147483647);可知默认设置为最大值了,也就可以理解为线程无限扩容。所以,会出现消费端线程数分配多的问题。

2.1.2 Dubbo请求处理分析

Dubbo中的request分为:有去无回(单向)、有来有回(双向)两种:

oneway 就是单向,其他的调用类型都是有返回的。

结合2.1.1的派发策略,参考官网线程模型 | Apache Dubbo介绍,

需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景,所以在默认的情况下,客户端接收到响应后,由于使用 all 的派发策略,会把响应请求派发到客户端线程池中去,但不是必定的,只能说响应会进入客户端线程池中去,但是这个响应可能是一个经过解析后的响应,也可能是一个没有经过解析的响应。所以这个响应有可能在进入线程池之前就被I/O线程解析过了,如果 IO 线程没有解析,那就在客户端线程里面去解析。

通过读源码DubboCodec和DecodeableRpcResult分析,是在I/O线程里面做解码的。

再参考官方消费端线程池模型 | Apache Dubbo分析Consumer和Provider的线程模型:

得到结论:

1)当业务数据返回后,默认在 IO 线程上进行反序列化操作,如果配置了 decode.in.io 参数为 false,则延迟到独立的客户端线程池进行反序列化操作。

2)所有响应还是会转发到客户端线程池里面,在这个里面进行解码操作(如果 IO 线程没有解码的话)把结果返回到用户线程中去。

3)对于线程池客户端的默认实现是 cached,服务端的默认实现是 fixed:固定大小线程池,启动时建立线程,不关闭,一直持有(缺省)。

从上述官方如下表述可以看到:

对 2.7.5 版本之前的 Dubbo 应用,尤其是一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题,具体问题讨论可参见 Need a limited Threadpool in consumer side #2013

改进后的消费端线程池模型,通过复用业务端被阻塞的线程,很好的解决了这个问题。

因为在 2.7.5 版本之前,是每一个连接都对应一个客户端线程池。相当于做了连接级别的线程隔离,但是实际上这个线程隔离是没有必要的。反而影响了性能。参考https://github.com/apache/dubbo/issues/2013。

而在 2.7.5 版本里面,就是不管你多少链接,均共用一个客户端线程池,引入了 Threadless Executor 的概念。

简单的来说,优化结果就是从多个线程池改为了共用一个线程池。

2.2 建议方案

由于2.5.5版本存在这个问题, 官方建议是升级dubbo版本,但由于涉及到很多基础组件需要同步升级、推广周期较长,因此为了及时解决问题,建议先从2.7.5版本fork 修复该问题的代码到当前2.5.5版本并打包dubbo源码为一个特殊版本2.2.5-enhanced。

1)问题讨论&描述:https://github.com/apache/dubbo/issues/2013

2)问题代码块:每次都会去新分配客户端线程池

public WrappedChannelHandler(ChannelHandler handler, URL url) {

     this.handler = handler;

     this.url = url;

    // would create a new ThreadPoolExecutor

     executor = (ThreadPoolExecutor) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);

 }

3)问题解决的可行性方案:https://github.com/apache/dubbo/pull/4131和https://github.com/apache/dubbo/pull/5490,在2.7.5中进行了修复。

具体代码改动清单:

https://github.com/apache/dubbo/commit/5cc3821438e4f9bcf3a8503ba08e9a0008ef0f4c

https://github.com/apache/dubbo/commit/5f8ac2b2e16ef0fa85b25280b3388a187cc415a5

总结来看,2.7.5主要包括了如下几点:

1)ConsumerConfig.java:新增配置项threads和queues(在具体版本 2.6.9中被添加, https://github.com/apache/dubbo/commit/ed4384a2e5ba4d047a3e73d666244b3f41521f5d):

// consumer thread pool type: cached, fixed, limit, eagerprivate String threadpool;// consumer threadpool core thread sizeprivate Integer corethreads;// consumer threadpool thread sizeprivate Integer threads;// consumer threadpool queue sizeprivate Integer queues;

2)定义了新的ThreadlessExecutors和Ring环形队列存储机制来保证不会每次直接产生新实例:

public class DefaultExecutorRepository implements ExecutorRepository {private static final Logger logger = LoggerFactory.getLogger(DefaultExecutorRepository.class);private int DEFAULT_SCHEDULER_SIZE = Runtime.getRuntime().availableProcessors();private final ExecutorService SHARED_EXECUTOR = Executors.newCachedThreadPool(new NamedThreadFactory("DubboSharedHandler", true));private Ring<ScheduledExecutorService> scheduledExecutors = new Ring<>();private ScheduledExecutorService reconnectScheduledExecutor;private ConcurrentMap<String, ConcurrentMap<Integer, ExecutorService>> data = new ConcurrentHashMap<>();//......public ExecutorService createExecutorIfAbsent(URL url) {String componentKey = EXECUTOR_SERVICE_COMPONENT_KEY;if (CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(SIDE_KEY))) {componentKey = CONSUMER_SIDE;}Map<Integer, ExecutorService> executors = data.computeIfAbsent(componentKey, k -> new ConcurrentHashMap<>());return executors.computeIfAbsent(url.getPort(), k -> (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url));}public ExecutorService getExecutor(URL url) {String componentKey = EXECUTOR_SERVICE_COMPONENT_KEY;if (CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(SIDE_KEY))) {componentKey = CONSUMER_SIDE;}Map<Integer, ExecutorService> executors = data.get(componentKey);if (executors == null) {return null;}return executors.get(url.getPort());}

3)针对WrappedChannelHandler弊病:在获取线程池的方法是完全的新增逻辑:

   /*** Currently, this method is mainly customized to facilitate the thread model on consumer side.* 1. Use ThreadlessExecutor, aka., delegate callback directly to the thread initiating the call.* 2. Use shared executor to execute the callback.** @param msg* @return*/public ExecutorService getPreferredExecutorService(Object msg) {if (msg instanceof Response) {Response response = (Response) msg;DefaultFuture responseFuture = DefaultFuture.getFuture(response.getId());// a typical scenario is the response returned after timeout, the timeout response may has completed the futureif (responseFuture == null) {return getSharedExecutorService();} else {ExecutorService executor = responseFuture.getExecutor();if (executor == null || executor.isShutdown()) {executor = getSharedExecutorService();}return executor;}} else {return getSharedExecutorService();}}/*** get the shared executor for current Server or Client** @return*/public ExecutorService getSharedExecutorService() {return ExtensionLoader.getExtensionLoader(ExecutorRepository.class).getDefaultExtension().createExecutorIfAbsent(url);}@Deprecatedpublic ExecutorService getExecutorService() {return getSharedExecutorService();}

参考讨论文章:https://github.com/apache/dubbo/issues/7054、https://github.com/apache/dubbo/pull/7109

2.2.1 其它建议-配置变更

1)建议变更为message模式,cache的线程数根据实际情况调整、建议800-1200。

2)在配置的时候 线程池是针对协议的,有两个属性,一个是threads是配置业务线程数量, 还有一个是iothreads配置I/O线程数量:在com.alibaba.dubbo.remoting.transport.netty.doOpen()逻辑中,通过dubbo URL获取到iothreads数量、创建一个ServerBootstrap并关联到netty NioServerSocketChannel上。
3)不同的调用者共用一个业务线程池。

2.2.2 其它建议-设置限流阈值或降级Fallback

同时接入限流/熔断组件在管理端设定阈值和策略,推荐使用阿里Sentinel组件使用 Sentinel 实现接口限流_运维开发故事的博客-CSDN博客_sentinel 接口限流

Dubbo线程池满导致宕机的案例分析解决相关推荐

  1. 如何使用Arthas定位线上 Dubbo 线程池满异常

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | 公众号「Kirito的技术分享」 前言 本文是 ...

  2. Arthas | 定位线上 Dubbo 线程池满异常

    作者 | 徐靖峰  阿里云高级开发工程师 前言 Dubbo 线程池满异常应该是大多数 Dubbo 用户都遇到过的一个问题,本文以 Arthas 3.1.7 版本为例,介绍如何针对该异常进行诊断,主要使 ...

  3. 记一次线上压测Dubbo线程池队列满的问题

    本文记录一次线上全链路压测出现的Dubbo线程池队列满的问题. 1 问题描述 线上做全链路压测,其中涉及三个系统,调用关系A->B->C,均是dubbo调用.压测的时候C出现CPU满导致服 ...

  4. 记几次 [线上环境] Dubbo 线程池占满原因分析(第三次:GC STW)

    [线上环境] Dubbo 线程池占满原因排查系列 记几次 [线上环境] Dubbo 线程池占满原因分析(第一次:HttpClient) 记几次 [线上环境] Dubbo 线程池占满原因分析(第二次:C ...

  5. Dubbo线程池问题思考Thread pool is EXHAUSTED!

    问题 前几天,我们的生产上突然出现了这样一个问题,调下面的查询方法报错,线程池满的问题,如下图: 问题思路 简单思考:我们都知道线程池的参数都包含什么含义!核心线程数,可建线程数,存储任务队列,拒绝策 ...

  6. dubbo官方文档_不可忽视的Dubbo线程池

    问题描述 线上突然出现Dubbo超时调用,时间刚好为Consumer端设置的超时时间. 有好几个不同的接口都报超时了 第1次调用超时,第2次(或第3次)重试调用非常快(正常水平) Dubbo调用超时的 ...

  7. dubbo日志关闭_不可忽视的Dubbo线程池避坑指南

    推荐阅读: 阿里巴巴4面Java岗位:算法+性能调优+并发+多线程+数据库 Dubbo+Kafka+MyBatis+reids+Spring+多线程等,学完就去面试BAT 问题描述 线上突然出现Dub ...

  8. dubbo 服务压测_不可忽视的Dubbo线程池

    问题描述 线上突然出现Dubbo超时调用,时间刚好为Consumer端设置的超时时间. 有好几个不同的接口都报超时了 第1次调用超时,第2次(或第3次)重试调用非常快(正常水平) Dubbo调用超时的 ...

  9. 用了很多年Dubbo,连Dubbo线程池监控都不知道,觉得自己很厉害?

    前言 micrometer 中自带了很多其他框架的指标信息,可以很方便的通过 prometheus 进行采集和监控,常用的有 JVM 的信息,Http 请求的信息,Tomcat 线程的信息等. 对于一 ...

最新文章

  1. 国内三分之一世界500强企业正布局区块链,区块链风口已经出现
  2. 制作r710 linux系统盘,记一次云主机系统盘扩容及制作私有镜像的操作步骤
  3. 转载:.NET Memory Leak: XmlSerializing your way to a Memory Leak
  4. windows下putty 连接linux远程管理
  5. 是时候了!网易首谈AI加持的AR
  6. C# 解析 Json数据
  7. Reading HTML content from a UIWebView
  8. 程序员面试金典——番外篇之下一个较大元素II
  9. 词法分析器(不讲武德java版)
  10. 对接湖南CA使用U_KEY登录
  11. 08_MySQL的函数
  12. 英特尔深度相机- Python 包装器的示例代码
  13. ABAP LVC DEMO程序
  14. 芯片如何储存信息_手机上的你以为信息删了就彻底删除了?事情没那么简单
  15. CUDA的Occupancy和Achieved Occupancy概念
  16. 基于单片机的盆栽自动浇花系统-毕设课设protues仿真全套资料
  17. 手把手教你如何快速发表论文(干货)
  18. 记录一次并发情况下的redis导致服务假死的问题
  19. linux监听火狐浏览器关闭,火狐浏览器window.close()关闭无效
  20. 决定系数 均方误差mse_误差分析计算公式及其 matlab 代码实现(mse、mape、rmse等)...

热门文章

  1. Linux命令手册-通过文件名查找文件(find、which、locate)(详解)
  2. 快速分割任何视频 Flv 音频aac wma等等文件,无需重新编码
  3. oracle 查询字段中文注释,oracle 查询表字段明细、字段注释、表注释
  4. 为什么要学量化,因为证券的黄金十年要来了
  5. 亚硝酸盐去除树脂原理
  6. Springboot注入RestTemplate异常Field restTemplate in xxx.ApiRestUtils required a bea
  7. 操作系统的接口与实现
  8. 【转载】深入浅出 - 小牛学堂-2018大数据之共享单车实战
  9. 微信小程序开发——base64位图片显示问题
  10. 《树莓派用户指南(第3版)》——2.4 在SD卡上安装NOOBS