1. 緣由
日常敲代碼時,運維同事突然把小組人員都拉進了一個羣裏,說一臺線上機器內存耗盡,OOM 導致服務註冊的 Mesh 客戶端被幹掉了,部分服務調用異常。運維同事查看機器負載,發現我們組內一個Java 服務佔用的內存有點異常,啓動命令-Xmx128m
指定了最大堆內存只有 128M,但是整個進程佔用的內存達到了 640M,顯然是有問題的
2. 線上排查
運維截圖一扔,鍋是甩不掉的,老老實實登錄到線上機器排查。內存佔用過高首先想到的就是發生了內存泄露
,使用 Jmap -histo $pid > heap.log
輸出堆內對象統計情況到文件中,查看文件發現堆中佔用內存最多的是各種數組,沒有發現明顯的問題。沒法子,使用 top -H p $pid
命令檢查該進程內運行的線程狀況,終於發現了可疑點,在這個Java 服務裏面運行的子線程居然有 5000 個,並且幾乎全部都在 Sleeping 狀態
這種情況首先想到的是發生了線程死鎖,資源爭用導致大量線程被阻塞了。使用 jstack -l $pid > stack.log
將線程棧相關狀況輸出到文件中,打開文件一搜索卻大失所望,根本沒有死鎖發生。線程狀態大都在 TIMED_WAITING
,不過隨着一行行往下看,也發現了一個可疑點,以下這種 OkHttp ConnectionPool
的線程出現得太多了,線程序號甚至達到了1082707
"OkHttp ConnectionPool" #1082707 daemon prio=5 os_prio=0 tid=0x00007f564c18f000 nid=0x1a4d in Object.wait() [0x00007f5602cb4000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:460)
at okhttp3.ConnectionPool$1.run(ConnectionPool.java:67)
- locked <0x00000000fc30fb30> (a okhttp3.ConnectionPool)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- <0x00000000fc305f98> (a java.util.concurrent.ThreadPoolExecutor$Worker)
此時回過頭來看堆內存文件,發現以下記錄,okhttp3.ConnectionPool
這個連接池對象實例佔用的內存雖然不多,只有300K,但是實例總數居然有 7876 個,毫無疑問是有問題的
40: 7876 315040 okhttp3.ConnectionPool
3. 代碼排查
可疑點在於 ConnectionPool
這個對象的數量,在項目中搜索 ConnectionPool
對象初始化調用點,發現 OkHttpClient
初始化時會調用 ConnectionPool
的構造方法,也就是每一個 OkHttpClient
實例被創建出來都會伴隨着一個連接池 ConnectionPool
的創建。而在項目代碼中,每次進行 RPC 調用的時候都會重新創建一個 OkHttpClient
對象,至此一切豁然開朗
- 原因剖析
OkHttpClient
對象在使用過後會被 JVM 回收,但是OkHttp
源碼中ConnectionPool
的構造方法裏默認最大線程空閒數是5,keepAlive 時間爲5分鐘,也就是發起一次網絡連接後,5分鐘內不會斷開連接。這樣當OkHttpClient
對象被 JVM 回收時,ConnectionPool
因爲有線程還保持着與服務端的連接,處於Active
狀態,5 分鐘內不會被回收,自然也不會釋放線程資源。線程會佔用內存空間,這樣隨着時間積累,線程數量越來越多,進程佔用的內存自然也越來越多了/** * Create a new connection pool with tuning parameters appropriate for a single-user application. * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity. */ public ConnectionPool() { this(5, 5, TimeUnit.MINUTES); }
4. 解決方法
問題根源找到了,修復自然簡單。項目代碼中每次進行 RPC 調用都重新創建一個 OkHttpClient
對象,這實際上是極大的性能浪費,只要在代碼中保存一個靜態 OkHttpClient
對象,每次網絡請求都複用這個對象就可以了