前言Duubo在互联网技术中应用非常广泛。从我实习到现在,我公司一直使用dubbo作为rpc框架,所以这也导致我们有必要对这项技术有更深入的了解,因为我们遇到的问题会一直存在。这次给大家说说我排查线上dubbo线程池耗尽的意外的思路。我写了一系列dubbo文章。看完本章,如果想了解更多关于dubbo的细节,可以查看之前的文章:Dubbo高性能NIO框架-NettyNetty常见面试题总结敖丙的RPC超时设置是不小心搞的敖丙发现的BUG在Dubbo源码,三位表扬我一天Dubbo基础Dubbo服务公开流程Dubbo服务参考流程Dubbo服务调用流程Dubbo的SPI机制是什么?Dubbo集群容错负载均衡Dubbo面试题RPC实践Netty问题。一天早上,手机突然收到公司发来的服务警告信息。线程池已用尽。在去公司的路上,首先想起的是公司最近的活动?流量突然增加?一大早就有人发布系统?我不在的时候我的系统又坏了?带着各种思绪看着公司群里的同步信息,上面的都可能被反驳!!!以下是当时的告警信息:RejectedExecutionException:ThreadpoolisEXHAUSTED(线程池耗尽)!ThreadName:DubboServerHandler-xx.xx.xxx:2201,PoolSize:300(active:283,core:300,max:300,largest:300)问:这个问题是怎么产生的?我们可以扩展线程池吗?如何解决问题?dubbo中线程池的默认实现是什么?A:我们在排查问题的时候,一定要有一种思维,一定要找出因果关系,才能提高自己。带着这样的思考,我们一步步揭开谜底。带着疑问,我们来查看一下dubbo的代码配置,了解一下dubbo的底层实现。只有了解了底层的实现,才能更准确的发现问题,处理问题,提升自己……首先我们看一下我们的代码配置:从抛出的异常来看,我们设置的线程池大小为300.这里我再说明一点,并不是线程池配置越大越好。这个跟我们的系统级别和JVM参数的配置有关。一般我们使用默认配置200,可以从以下几个方面考虑:1.JVM参数:-Xms初始堆大小-Xmx最大堆大小-Xss每个线程堆栈大小2.系统级别1.最大系统可以被创建线程2.公式行:线程数=(机器自身可用内存-(JVM分配的堆内存+JVM元数据区))/Xss的值例如我们可以看到ThreadPool接口是一个扩展点,默认实现是固定的,里面有个getExecutor方法,通过@Adaptive注解修改。dubbo中有4个ThreadPool的实现类1.CachedThreadPool缓存线程池,超过keepAliveTime会删除,使用时重新创建。2.FixedThreadPool线程池,线程数固定。3.LimitedThreadPool可扩展的线程池,线程只增长不收缩。4.EagerThreadPool当核心线程数繁忙时,创建一个新的线程,而不是将任务放入阻塞队列。这使用它自己的队列TaskQueue。这里直接看FixedThreadPool异常处理机制的默认实现。从代码中我们可以发现:dubbo线程池使用了jdk的ThreadPoolExecutor,默认线程数为200,使用默认的SynchronousQueue队列。如果用户配置的队列长度大于0,则使用LinkedBlockingQueue队列。如果一个线程由于执行异常而结束,一个新的线程将被添加到线程池中。因此,由于dubbo默认使用直接提交的SynchronousQueue工作队列,所有的任务都会直接提交给线程池中的某个工作线程。如果没有可用的线程,它将拒绝任务的处理并抛出我们当前遇到的消息。下面介绍我们创建线程池的必要参数:corePoolSize——池中保存的线程数,包括空闲线程。maximumPoolSize-池中允许的最大线程数。keepAliveTime-当线程数大于核心数时,这是多余的空闲线程在终止前等待新任务的最长时间。unit-keepAliveTime参数的时间单位。workQueue-用于在执行前保存任务的队列。此队列仅包含由execute方法提交的Runnable任务。threadFactory-执行程序创建新线程时使用的工厂。处理程序由于超出线程范围和队列容量而阻塞执行时使用的处理程序。ThreadPoolExecutor是Executors类的底层实现。好了,看了这么多源码,从上面我们已经明白了这个异常的来源,那么这次遇到的线程池被耗尽是什么原因呢?第一阶段排查思路:由于10.33.xx.xxx本机线程池耗尽,伴随着长时间的ygc;所以怀疑是ygc时间长导致机器资源不足,拖累了线程池;2021-03-26T10:14:45.476+0800:64922.846:[GC(AllocationFailure)2021-02-24T11:17:45.477+0800:64922.847:[ParNew:1708298K->39822K(1887488K),3.9215459secs]4189242K->2521094K(5033216K),3.9225545secs][Times:user=12.77sys,real=0.092一直在思考是什么原因导致YGC时间这么长:这里简单解释一下为什么高IO会导致GC时间长1.JVMGC需要由系统调用write()来记录GC行为.2.write()调用可以被后台磁盘IO阻塞。3.记录GC日志是JVM暂停的一部分,所以write()调用的时间也会算在JVMSTW的暂停时间里。通过GC日志,我们可以看到垃圾回收时新生代的停顿时间为3.92s;新生代空间在1.8G左右明显不正常;ParNew收集器的工作过程中会出现以下步骤:(1)Mark-Markthelivingobject---->(2)Copytheobjectfromtheedenareatosurvivorarea---->(3)清理eden区理论上第三步时间是一定的,那么可能需要很长时间,要么在第一步,要么在第二步。如果第一步的标记时间太长,说明本次GC前eden区有大量的小对象(因为eden区的大小是一定的)),量级应该是几十倍正常对象的数量。如果第二步时间过长,那么有以下几种可能:1.标记之后,eden区中仍然存在大量对象(以回收后的新生代为代表的对象仍然占用大量内存),这个可以从gclog中排除(回收后的新生代大小还是39M)2.标记后还是有大量零散的小对象3.YGC启动了fullGC,但是我们没有看到上面的情况此时的log都指向一种可能,即新生代中存在大量零散的小对象;为了验证这个说法,只有一种方法可以分析heapsnapshot,但是我们当时恰好有一台机器被重启了,找不到原因。当我们无法验证时,第二台机器出现了同样的问题。当我们准备再次跳转日志时,回头查看GC日志,发现GC正常。那么为了推翻第1阶段的结论,这里有一些常用的服务器命令:top:这个是最常用的,也是显示信息最全的。你可以看到负载、内存、cpu和许多其他东西。例如使用top常用分析步骤:1.top-Hp命令查看哪个线程占用率高2.使用printf命令查看该线程的十六进制数3.使用jstack命令查看线程占用率高的方法当前线程正在执行使用jmap查看内存状态,分析是否存在内存泄漏。jmap-heap3331:查看java堆(heap)使用情况jmap-histo3331:查看堆内存中对象的数量和大小(直方图)jmap-histo:live3331:JVM会先触发gc,然后统计信息jmap-dump:format=b,file=heapDump3331:将内存使用的详细信息输出到文件中。当然,还有很多其他的命令,比如jstack、jinfo、uptime等很多命令。..开始第二阶段的排查:第二台异常机器伴随着各种redis查询超时,导致所有查询都跑到DB上,导致数据库压力大增,SQl报警慢等。于是又去查看是什么原因导致我们的redis查询超时?第一步肯定是检查redis服务的各项性能指标是否异常,而且结果变化不大,所以问题肯定出在服务器本身。检查自身的指标,发现:也就是在告警期间期间,cpu、cpu_iowait、指标会飙升,磁盘IO会呈现不间断的锯齿波,其他指标基本不会波动。但是什么导致CPU波动呢?CPU波动和线程池耗尽有什么因果关系?因此,似乎答案是磁盘IO高,导致cpu_iowait变高。如果瞬间没有分配CPU和其他IO,时间片就分配不上,rt就会抖动。最后我们发现我们磁盘IO高的原因也和我们的日志采集系统有关。相信现在有大量的公司将TRACE作为服务整个环节的最终管理。然后用异步的方式把记录在内存中的tracelog一次性写到磁盘上,所以会有IO抖动。接下来,让我们看看trace是如何工作的。trace是一个类似于strace的动态跟踪工具,属于问题诊断和调试工具1.trace包括两部分:收集trace日志和解析trace日志。2.trace收集trace日志本质上是trace模块运行时记录的控制流信息的日志重点:trace模块将上述信息组织成二进制格式写入内存,不导出上述信息在内存中,直到跟踪跟踪停止。二进制文件只是为您提供一个想法。每个人遇到问题或者场景不一样,所以得出的结论也不一样。只能给大家做个参考!!!综上所述,我们在排查问题的时候,很容易被其他一些表象所迷惑,认为原本是果的关系变成了因,但这是一个复杂的过程,只有不断积累,不断学习才能做到,所以写下一篇文章希望对大家有所帮助。最后要强调的是,大家不要迷糊解决问题,这样下次问题再次出现时,你还是在不明原因的情况下发呆去解决。这样一来,你长期的个人成长是非常有限的,对一项技术的理解往往从发现问题提升到理解解决方案。遇到这样的情况,身边的大佬们总是选择先解决问题(遇到什么线上问题,先恢复再考虑找原因),然后追根究底,找到答案之前放弃。比如我老公司的领导遇到内存溢出的问题时,从中午一直分析到半夜转储的数据。分析完之后,在流程安定下来回家前写了一大页wiki吧。我看到它时,从心底里赞叹不已。做你所做的,热爱你所做的。我是敖丙你知道的越多,你不知道的就越多。下次见。