前段时间现场的运维同学反馈某个服务出现一段时间里Dubbo消费端线程池被打满导致部分接口出现不可用的问题,这里记录下排查的过程。

首先,查看业务日志,发现出现了大量Thread pool is EXHAUSTED的报错。

项目里Dubbo服务端线程池配置如下,

 <dubbo:protocol name="dubbo" port="-1" threads="500" threadpool="cached" dispatcher="message"/>

这里简单解释下,线程数设置为500,线程池类型选的是cache,这里的cache线程池并不是指JUC里提供的,Dubbo里它的实现如下:

public Executor getExecutor(URL url) {String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);//默认为0int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);//默认为0int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);//默认60sreturn new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,queues == 0 ? new SynchronousQueue<Runnable>() :(queues < 0 ? new LinkedBlockingQueue<Runnable>(): new LinkedBlockingQueue<Runnable>(queues)),new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
}

dispatcher设置的是message类型,dubbo一共提供了AllDispatcher、DirectDispatcher、MessageOnlyDispatcher、ExecutionDispatcher、ConnectionOrderedDispatcher等5种dispatcherMessageOnlyDispatcher的示意图如下,它表示在Dubbo线程只执行received和反序列化,其他诸如sent、connected等事件均由IO线程处理,它能让服务端的线程池能尽可能地专注于执行业务代码。关于这方面更详细的信息可以参考官方文档 :Dubbo协议-Provider端线程模型。

1.jstack执行报错

基于以往的调用情况,一般来说消费端线程池设置为500绝对是够用的,所以当出现Thread pool is EXHAUSTED的时候一开始的反应就是服务端的线程被block了。用jstack pid > dubbo.log导出堆栈信息,反馈执行报错了

看网上有人说是因为进程的启动用户和执行用户不一致导致的,但我们都是用root用户执行,所以不是这个原因。实际上,在linux中jvm运行时会在/temp路径下创建一个文件夹hsperfdata_USERUSER是启动java进程的用户),文件夹下会有些pid文件用来存放jvm进程信息。jstack等命令通过读取/tmp/hsperfdata_USER下的pid文件获取连接信息。查看/tmp/hsperfdata_root内的文件,发现确实有一些pid文件,但是并没有找到需要的进程文件。

进一步查看相关资料后了解到,为了防止temp文件过多,centos7会定时调用命令/usr/bin/systemd-tmpfiles --clean进行临时文件的清理,清理规则定义在配置文件/usr/lib/tmpfiles.d/tmp.conf,执行时间由/usr/lib/systemd/system下的systemd-tmpfiles-clean.timer进行管理。查看清理规则配置文件

v 需要清理的目录

x 忽略的目录及目录下的子文件,可以使用shell通配符

X 忽略的指定目录,不包含子文件,可以使用shell通配符

可以发现,/temp下的文件默认是10天清理一次,查看系统日志,发现确实有删除的操作。

可以添加如下规则,让系统删除时不要删除java相关的进程文件。

x /tmp/hsperfdata
X /tmp/.java_

然而现在的问题是相关的pid文件已经被删除了,利用jstack打印堆栈信息已经不可能,只能另寻它路。这时我用报错日志Thread pool is EXHAUSTED去源码里找是在什么地方打印出来的,看能不能找到相关信息,最后定位如下相关代码。

原来Dubbo重写了线程池的AbortPolicy策略,看rejectedExecution方法,打印日志后抛出异常前,执行了一个dumpJstack方法,这名字看上去像是要输出堆栈信息,看它的具体实现,发现出现异常时会每隔10分钟执行一次。

    private void dumpJStack() {long now = System.currentTimeMillis();//dump every 10 minutesif (now - lastPrintTime < TEN_MINUTES_MILLS) {return;}.....省略一些代码......ExecutorService pool = Executors.newSingleThreadExecutor();pool.execute(() -> {//获取输出路径String dumpPath = getDumpPath();.....省略一些代码......//try-with-resourcestry (FileOutputStream jStackStream = new FileOutputStream(new File(dumpPath, "Dubbo_JStack.log" + "." + dateStr))) {//输出堆栈信息JVMUtil.jstack(jStackStream);} catch (Throwable t) {logger.error("dump jStack error", t);} finally {guard.release();}});//must shutdown thread pool ,if not will lead to OOMpool.shutdown();}

关键的代码在这句String dumpPath = getDumpPath(),看它的实现

项目里并没有配置DUMP_DIRECTORY的值,所以输出路径是默认值USER_HOME,USER_HOME是根据System.getProperty("user.home")计算来的,在window环境为C:\Users\Administrator,而在linux环境里为/USERUSER表示当前的用户名。

真正获取堆栈信息的是JVMUtil.jstack(jStackStream)方法,看眼它的实现就会发现它跟jconsole这些java提供的图形化工具的原理是一样的。

找到相应的文件路径,发现确实有个名为Dubbo_JStack.log.xx的文件,xx表示文件生成的时间。打开日志文件发现有大量的DubboServerHandler线程处理TIMED_WAITING状态,看调用栈都是在等待获取Druid连接池的连接。

Druid连接池是一个生产者消费者模型,其中生产端是单线程的,示意图如下:

关于Druid连接池更为详细的信息可以参考:Druid连接池实现原理,文章里某些图片有一些细节上的小问题,但并不影响理解大致原理。

当连接池里没有可用连接时,消费端线程会执行等待,直到有连接被创建或者回收或者超时被唤醒。

而因为Druid连接池生产端是单线程的,所以如果某一次RPC调用在获取数据库连接时耗时过长甚至超时,那么会导致后面所有RPC调用在需要获取数据库连接时阻塞,进而引发服务端Dubbo线程池溢出,抛出异常Thread pool is EXHAUSTED

获取连接时间过长甚至超时,可能是连接池已有连接已达到maxActive,生产端不再生产连接导致,已有连接也都未回收,导致消费端一直在等待直到超时。然而,当查看日志,发现连接池内的连接数根本就没到maxActive,创建连接消耗了59s依然失败了。

2.模拟超过数据库最大连接数

去Druid项目的Github主页找看看有没有人遇到过类似的问题,发现有同学提到可能是数据库达到最大连接数。

写了一个测试案例来模拟创建大于数据库最大连接数,发现达到最大连接数后确实出现报错Connection reset

抓包的结果也显示数据库向应用端发送了RST包。

然而,当连接达到最大连接数时,会同时出现异常报错:no appropriate service handler found

在现场的日志中并没有找到相同的报错,而且运维的同学也反馈当时数据库的连接数是正常的,并没有达到最大连接数,所以暂时排除这个可能。

3.模拟TCP握手异常

获取连接时间过长甚至超时除了Druid连接池内连接数达到maxActive外,也有可能是在创建连接过程中因为网络问题导致的。因此,接下来就模拟一下网络异常并进行测试。

Oracle客户端和服务器使用TNS作为其数据交换协议。 它提供了一种对用户透明的层, 为不同的工业标准协议提供统一、通用的接口。示意图如下,图来源于oracle文档。

查看抓包的结果,可以看到完整的Oracle 建立连接、执行sql、返回结果的过程。

先模拟TCP三次握手异常,添加防火墙规则,拒绝客户端所有的数据包。

iptables -D  INPUT -s 192.168.x.xxx -j DROP

利用Arthas观察Druid获取连接时的耗时情况,关于trace的使用可参考:Arthas官方文档

trace com.alibaba.druid.pool.DruidAbstractDataSource createPhysicalConnection -n 1000

trace结果如下,获取连接接口的耗时一共为21s。

查看抓包结果,两次获取请求之前耗时确实为21s左右。

因为我这里测试的时候用是在window上,如果是linux系统,那么重试的耗时约为127s。因为linux系统默认会重传6次,每次重传失败下一次的等待时间就会翻倍。linux重传的次数可以通过sysctl -a | grep tcp_syn_retries查看。

同时业务日志出现了Druid连接池内连接数未到maxActive,获取新连接失败的报错。

但是,它获取连接报错的原因是:The Network Adapter could not establish the connection,而现场的日志中并没有这样的报错,所以暂时排除这种可能。

4.模拟TNS协议应用层握手异常

从上面oracle数据交换协议的示意图可以看出,在经过三次握手建立TCP连接后,还要互相发送一些诸如SET DATATYPEUSER OCI的报文,接下来就模拟一下这些报文发送异常的情况。以Rquest,Connect报文中的Connect Data值作为过滤条件添加防火墙规则。

规则如下:

iptables -I INPUT  -m string --algo bm --string "
(DESCRIPTION=(CONNECT_DATA=(SID=ora11g)(CID=(PROGRAM=)
(HOST=__jdbc__)(USER=))" -j DROP

trace的打印结果如下:

业务日志同样出现了Druid连接池未满但获取数据库连接超时的异常

但是,它获取连接报错的原因是Socket read timed out,而现场的日志中并没有这样的报错,所以暂时也排除这种可能。

5.修改Druid等配置

由于时间问题就没有更进一步的模拟各种场景来复现现场环境的异常,但是,经过前面的分析有一件事是确定的,那就是Druid在创建数据库连接时出现了异常而且等待时间过长,导致Dubbo服务端线程被占用迟迟不能释放,最终导致线程池枯竭。

项目里Druid设置的maxWait为60000,也就是60s。测试网络异常时执行完服务端代码所要的时间,如下图,

之所以是120s,是因为Druid在maxWait时间内获取数据库连接失败后会默认重试一次。也就是说如果出现一次网络异常导致获取数据库连接失败,那么在2分钟内,Dubbo服务端的线程都不能释放,如果在RPC调用并发数比较大的时候,这非常容易导致线程池枯竭。

既然是因为创建连接导致的超时,那么解决方法有以下几种:

  1. 设置创建连接的超时时间connectTimeout以及数据交互超时时间socketTimeout,关于这两个时间的作用更详细的信息可参考:深入理解JDBC的超时设置,总之它能避免单次创建连接时间过长;
  2. 调小Druid的waitTime,它表示Druid从连接池中获取连接的最长等待时间,由于默认会重试一次,所以可以调整为Dubbo消费超时时间的1/2左右;
  3. 调大Druid的minIdle,它表示连接池空的最小空闲连接数,它能保证在创建连接失败时还有一定的数量的可用连接,能尽量去处理现有的访问数据库的操作,减少服务端线程等待的情况;
  4. 查找应用中执行时间较长的sql进行优化。和第3点一样,这也是为了让Druid连接池中有尽可能多的可用连接,从而降低创建新连接的概率。

关于第一点,如果是用的是MySQL数据库,那么可以在配置数据库连接地址url时在后面添加上jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=3000&socketTimeout=4000。如果用的是Oracle数据库,网上有很多帖子的说法是错误的,比如

<property name="connectionProperties" value="connectTimeout=3000;socketTimeout=4000"/>

按上面的配置实际上是没有效果的,最后还是去源码中找答案,用的oracle驱动版本是ojdbc14 10.2.0.4.0,在oracle.jdbc.driver.OracleDriver#connect

var2里的值就包括我们在connectionProperties里设置的值,而oracle.jdbc.ReadTimeout就是我们要设置的socketTimeout。对于connectTimeout它设置的条件是var2中不包含oracle.net.CONNECT_TIMEOUT的值且DriverManager.getLoginTimeout的值不为0,而它的值可以通过Druid的loginTimeout属性设置,要注意的是它的单位是秒。

<property name="loginTimeout" value="3"/>

设置完后,重新模拟网络异常的情况,查看trace结果,可以看到已经生效了。

至于第2点和第3点,分别把waitTime设置为6000和minIdle设置为15,而第4个因为引入了其他组的依赖,需要其他同事一起参与,暂时不改动。在做了如上的改动后,现场环境在稳定运行了一段时间没有再次出现Dubbo服务端线程池Thread pool is EXHAUSTED的情况,这个问题的排查也就暂时告一段落。

这次异常排查耗费了比较多的时间,主要原因还是自己对网络通信这块知识还有很大的欠缺,下一步的计划就是要加深这一块的理论学习。

至于现场异常日志中connect reset的原因,在查阅了一些资料后了解到收到RST报文有以下几种可能:

  • 端口未打开
  • 请求超时
  • 提前关闭,执行了close方法
  • 在一个已关闭的socket上发送数据

当然实际情况远不止这几种,更多的可以参考:tcp rst产生的几种情况。后续准备按照这些可能性在测试环境进行模拟。

参考:

1.java无法热更新Unable to open socket file

2.几种TCP连接中出现RST的情况

3.Connection reset 及 connection reset by peer

4.深入理解JDBC超时设置

5.arthas在线文档

6.数据库异常引发的问题

7.Oracle的TNS协议解析

一个由Dubbo Thread pool is EXHAUSTED引发的问题排查相关推荐

  1. [Done][DUBBO] dubbo Thread pool is EXHAUSTED!

    异常信息: com.alibaba.dubbo.remoting.ExecutionException: class com.alibaba.dubbo.remoting.transport.disp ...

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

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

  3. Dubbo 线上 Thread pool is EXHAUSTED 问题排查

    本文来自作者投稿,原创作者:Tom 前景提要 早上9点第一个到公司泡了一包枸杞,准备打开极客时间看两篇文章提提神.突然客服部反馈用户发送短信收取不到验证码还一通在有大领导的群里@所有人(负责这块的同事 ...

  4. 【案例】常驻查询引发的thread pool 性能问题之二

    一 现象     某业务单机4个实例中的一个实例出现连接数远高于其他三个实例(正常是4K,问题实例是8K+),但是这4个实例的配置完全相同.业务开发反馈为部分连接失败.     执行show proc ...

  5. mysql5.6 thread pool_mysql5.6 thread pool

    从percona 的压测来看,确实很牛笔啊.提升很大. http://www.mysqlperformanceblog.com/2014/01/29/percona-server-thread-poo ...

  6. Reporting Service 告警w WARN: Thread pool pressure. Using current thread for a work item

    如果Reporting Service偶尔出现不可访问或访问出错情况,这种情况一般没有做监控的话,很难捕捉到.出现这种问题,最好检查Reporting Service的日志文件. 今天早上就遇到这样一 ...

  7. 白话Elasticsearch67-不随意调节jvm和thread pool的原因jvm和服务器内存分配的最佳实践

    文章目录 概述 不随意调节jvm和thread pool的原因 jvm gc threadpool jvm和服务器内存分配的最佳实践 jvm heap分配 将机器上少于一半的内存分配给es 为什么不要 ...

  8. Thread pool引起的程序连接数据库响应慢

    数据库版本:percona-mysql 5.6.16 ​在很长一段时间,都会出现程序连接数据库,出现响应慢的情况,正常在几到几十毫秒之间,但是偶尔会出现上百毫秒的情况: 开始由于开发重新设置并调整过程 ...

  9. 自定义parallelStream的thread pool

    文章目录 简介 通常操作 使用自定义ForkJoinPool 总结 自定义parallelStream的thread pool 简介 之前我们讲到parallelStream的底层使用到了ForkJo ...

  10. worksteal thread pool

    worksteal的场景 对于一个线程池,每个线程有一个队列,想象这种场景,有的线程队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,现象就是有的线程处于饥饿状态,而有的线程处于消化不良的状态 ...

最新文章

  1. 智能车竞赛:提问与回答
  2. 使用curl上传带有文件的POST数据
  3. 春天尚远,体育场馆预定平台距离成功彼岸还有多远?
  4. IE6下绝对定位的高度自适应
  5. 最全的时间类解析。 SimpleDateFormat + Date() 和 DateTimeFormatter + LocalDate()的区别与使用场景
  6. 剑指offer(一):二维数组中的查找
  7. 万级规模 K8s 如何管理?蚂蚁双11核心技术公开
  8. python的遍历字典里的键然后放到一个列表里_Python列表和字典互相嵌套怎么办?看完让你没有疑惑...
  9. Tomcat源代码解析系列
  10. FTP+SSL(加密的ftp)
  11. 12_首页显示热门商品和最新商品
  12. 计算机公式SUBSTITUTE,SUBSTITUTE函数公式函数用法(查找替换)
  13. 1192: 删除记录(结构体专题)
  14. 结构化设计向导与IP核生成工具——IP核生成时.xaw与.xco的区别
  15. HYSBZ 1588 营业额统计 伸展树
  16. Sqlserver中的日期类型值不能小于1753年
  17. 巨量jsvmp,扣逻辑分析
  18. weblogic的安装与部
  19. webstorm和网站制作
  20. windows的IOCP(Input Output Completion Port,输入输出完成端口)

热门文章

  1. 1、 域名系统的主要功能是什么?互联网的域名结构是怎样的?域名系统中的本地域名服务器、根域名服务器、顶级域名服务器以及权限域名服务器有何区别?2、 假定要从已知的URL获得一个万维网文档。若该万维网服
  2. 联合循环——13(智慧电厂的发展与展望)
  3. 绝对受用的求职经验分享
  4. java excel 密码_用java实现对EXCEL加打开密码的方法?
  5. 如何在PC,Mac或iPhone上启用iTunes家长限制
  6. ST188光电式传感器的应用
  7. VMware workstations安装黑苹果系统
  8. ps -aux | grep xxx详解
  9. qiankun加载vue子应用报错[import-html-entry]: error occurs while executing normal script
  10. word中利用模糊替换