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日志,大量線程池耗盡的警告日志:
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了,線程池耗盡了,這樣勢必造成請求的積壓,觸發流控和降級。
3. 問題排查
? 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版本后,問題解決。
? 總結,線程池調優,主要關注線程的如下幾種狀態:
- 死鎖, Deadlock(重點排查)
- 執行中,Runnable
- 等待資源, Waiting on condition(重點排查)
- 等待獲取監視器, Waiting on monitor entry(重點排查)
- 暫停,Suspended
- 對象等待中,Object.wait() 或 TIMED_WAITING
- 阻塞, Blocked(重點排查)
- 停止,Parked