Apache Dubbo 是一款高性能、轻量级的开源 JAVA 服务框架。提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。Dubbo之前是阿里开发,并得到广泛的应用,后来贡献给了Apache开源组织。
Dubbo默认的底层网络通讯使用的是Netty,服务提供方NettyServer使用两级线程池,其中 EventLoopGroup(boss) 主要用来接受客户端的链接请求,并把接受的请求分发给 EventLoopGroup(worker) 来处理,boss和worker线程组我们称之为IO线程。
如果服务提供方的逻辑能迅速完成,并且不会发起新的IO请求,那么直接在IO线程上处理会更快,因为这减少了线程池调度。但如果处理逻辑很慢,或者需要发起新的IO请求,比如需要查询数据库,则IO线程必须派发请求到新的线程池进行处理,否则IO线程会阻塞,将导致不能接收其它请求。
生产环境,该服务大约QPS在1万左右,总共10个节点。最近该服务在高峰期,频繁触发流控和降级。查看dubbo日志,大量线程池耗尽的警告日志:
WARN 2021-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了,线程池耗尽了,这样势必造成请求的积压,触发流控和降级。
Dubbo可以通过配置,当线程池满时,会dump出JStack日志出来,便于分析排查问题。一般配置如下:
<dubbo:Application name="${server.name}" >
<dubbo:parameter key="dump.directory" value="${account.dubbo.dump.directory:/home/dubbo_dump/}${server.name}${server.id}" />
</dubbo:application>
默认会输出到/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=325
at java.util.Collections$SynchronizedMap.get(Collections.java:2584)
- blocked on java.util.Collections$SynchronizedMap@2d796a15
at 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版本后,问题解决。
总结,线程池调优,主要关注线程的如下几种状态: