Dubbo线程池调优

1. Dubbo简介及线程池策略Apache Dubbo 是一款高性能、轻量级的开源 JAVA 服务框架 。提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维 。Dubbo之前是阿里开发,并得到广泛的应用,后来贡献给了Apache开源组织 。
? Dubbo默认的底层网络通讯使用的是Netty,服务提供方NettyServer使用两级线程池,其中 EventLoopGroup(boss) 主要用来接受客户端的链接请求,并把接受的请求分发给 EventLoopGroup(worker) 来处理,boss和worker线程组我们称之为IO线程 。
? 如果服务提供方的逻辑能迅速完成,并且不会发起新的IO请求,那么直接在IO线程上处理会更快,因为这减少了线程池调度 。但如果处理逻辑很慢,或者需要发起新的IO请求,比如需要查询数据库,则IO线程必须派发请求到新的线程池进行处理,否则IO线程会阻塞,将导致不能接收其它请求 。
2. 线程池报警? 生产环境,该服务大约QPS在1万左右,总共10个节点 。最近该服务在高峰期,频繁触发流控和降级 。查看dubbo日志,大量线程池耗尽的警告日志:
WARN2021-05-11 **:**:** WARN AbortPolicyWithReport:65 -[DUBBO] Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-**.**.**.**:**, Pool Size: 500 (active: 500, core: 500, max: 500, largest: 500), Task: 1285578 (completed: 1285135), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false)? 该服务线程池最大500,通过日志可以看到active线程已经达到500了,线程池耗尽了,这样势必造成请求的积压,触发流控和降级 。
3. 问题排查? Dubbo可以通过配置,当线程池满时,会dump出JStack日志出来,便于分析排查问题 。一般配置如下:
<dubbo:Application name="${server.name}" ><dubbo:parameter key="dump.directory"value=https://www.isolves.com/it/cxkf/kj/2021-05-18/"${account.dubbo.dump.directory:/home/dubbo_dump/}${server.name}${server.id}" />? 默认会输出到/home/java这个目录下 。
? 通过排查日志发现,大量线程是BLOCKED状态的,日志如下:
"DubboServerHandler-ip:port-thread-449" Id=633 BLOCKED on java.util.Collections$SynchronizedMap@2d796a15 owned by "DubboServerHandler-ip:port-thread-203" Id=325at java.util.Collections$SynchronizedMap.get(Collections.java:2584)-blocked on java.util.Collections$SynchronizedMap@2d796a15at com.google.gson.Gson.getAdapter(Gson.java:332)at com.google.gson.Gson.fromJson(Gson.java:802)at com.google.gson.Gson.fromJson(Gson.java:768)at com.google.gson.Gson.fromJson(Gson.java:717)at com.google.gson.Gson.fromJson(Gson.java:689)...? 通过查看日志发现,最后问题出现在Gson做json反序列化时造成的 。再来查看下Gson的源码发现:
public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {TypeAdapter<?> cached = typeTokenCache.get(type);if (cached != null) {return (TypeAdapter<T>) cached;}...}? Gson这里是获取适配器,Gson是通过适配器设计模式,问题就出现在获取适配器这里 。再来看下typeTokenCache的定义:
private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache= Collections.synchronizedMap(new HashMap<TypeToken<?>, TypeAdapter<?>>());? 在早期的JDK版本中,使用线程安全的Map一般都是通过synchronizedMap这种方式,其实底层就是通过synchronized锁实现的 。synchronized是互斥锁,也是重量级锁,虽然目前得到很多优化,但是当高并发下,线程获取不到锁,会立马进入BLOCKED状态,这就是Dubbo线程池满的原因 。
? 解决方式如下:
? 在早期由于没有提供JUI包,也就是ConcurrentHashMap,所以使用synchronizedMap这种方式实现高并发 。从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树 。相信新的Gson版本肯定会做相应的升级,于是查看Gson的2.8.5版本的源码,果然升级了,源码如下:
private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<TypeToken<?>, TypeAdapter<?>>();? 升级Gson到2.8.5版本后,问题解决 。
? 总结,线程池调优,主要关注线程的如下几种状态: