您好,登錄后才能下訂單哦!
本篇內容介紹了“高并發場景創建JedisPool有哪些注意事項”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
一個平靜的下午,報警、Moji群里接連傳來并行MOA默認集群 /service/parallel 出現異常的提示信息,服務維護人員查看日志發現是發生了并行任務線程池被打滿的問題。線程池滿會導致新請求直接被拒絕,大量業務請求報錯,極速版附近的人、基因、聊天室等多個業務進入降級狀態... 而導致這一系列嚴重影響的問題原因,是大家最熟悉不過的 new JedisPool() 這一行代碼。
Jedis是Java訪問Redis最常用的客戶端組件
從慢請求日志我們發現,單一請求阻塞線程的時間最長可達到10分鐘以上。簡單的new JedisPool()為何會長時間阻塞線程?通過搭建測試服務、復現問題、抓取jstack分析后,我們發現JedisPool中向JMX注冊對象的邏輯,在特定的場景會出現嚴重的鎖競爭與阻塞問題。
并行MOA工程 ->
MOA(MOARedisClient) ->
MCF(RedisDao) ->
Jedis(JedisPool) ->
commons-pool(BaseGenericObjectPool) ->
JDK(JMX)
問題出現在并行MOA通過MOARedisClient訪問下游服務新啟動實例的過程中,此時需要通過new JedisPool()創建與下游實例的連接池。
JedisPool使用commons-pool來管理連接對象,commons-pool創建對象池時會向JMX注冊,以便于在運行時通過JMX接口獲取對象池相關的監控數據。但向JMX注冊的過程,包含以下邏輯
commons-pool向JMX注冊BaseGenericObjectPool對象,JMX要求每個對象有不同的名字
commons-pool生成不同名字的方式是:基于一個默認相同的名字,末尾添加一個自增ID
每次new JedisPool()時ID從1開始嘗試,發現名字重復后ID自增+1后再次重試,直至發現一個未被占用的ID、重試成功為止
嘗試某個名字是否被占用,會共用一把全局的鎖,同一時刻只能有一個JedisPool對象對某一個名字ID驗證是否重復
commons-pool中遍歷ID嘗試注冊objName的代碼
JMX中注冊對象的代碼,會獲取一把全局的鎖
當前進程中已創建了大量的JedisPool,有大量的自增ID已被占用(如1~1w已被占用)
此時創建下一個JedisPool,需要遍歷1w次已有ID才能以1w + 1這個ID注冊對象,這1w次嘗試每次都需要加鎖
當有多個線程同時創建JedisPool時,每個線程都需要遍歷所有ID,并且遍歷過程中每次加鎖都會導致其他線程無法重試、只能等待
假設1個線程遍歷1w次需要1秒,100個線程各遍歷1w次、共計100w次嘗試需要串行執行,并且100個線程是交替獲得鎖、交替重試,最終導致100個線程都需要100秒才能重試完
16:14 /service/phpmoa/v1_microvideo_index 執行常規的發布操作
16:16 /service/parallel 并行任務線程池被打滿、開始通過擴容和隔離實例解決
16:26 服務逐步恢復
并行MOA使用了MSC線程池組件,從活躍線程數監控可以看到每個并行MOA實例線程池被打滿到恢復的時間
被阻塞的線程是能夠自動恢復的,并且恢復的時間并不統一。從日志中我們首先找到了阻塞線程的慢請求
execution finished after parallel timeout: /service/phpmoa/v1_microvideo_index,isRiskFeeds, startTime = 2020-11-30 16:26:02,428, server = 10.116.88.15:20000, routeTime = 2020-11-30 16:26:02,428, blacklistTime = 2020-11-30 16:26:02,428, executeTime = 2020-11-30 16:37:21,657, timeCost = 679229
剛好是調用 /service/phpmoa/v1_microvideo_index 服務,但記錄的執行時間最長可達到10分鐘以上。慢日志中包含各個階段的耗時,因此耗時的邏輯可以鎖定在 blacklistTime 和 executeTime 之間,這里只對應一行通過MOA框架MOARedisClient調用下游服務的代碼
在MOARedisClient.exeuteByServer()內部,僅有2個邏輯可能出現較長的耗時,一個是RedisFactory.getRedisDao(),這里會與下游實例創建連接。另一個是doInvoke()真正發起請求,由于后者的耗時會提交到Hubble,并且未發現達到分鐘級的耗時,因此問題的原因更可能出現在創建RedisDao的邏輯中。
由于RedisFactory.getRedisDao()各個階段的耗時缺少監控,并且服務出現異常期間沒有及時通過jstack打印堆棧信息,問題排查到這一步僅靠分析很難繼續進行。
我們查找了 /service/phpmoa/v1_microvideo_index 的發布記錄,發現這個服務每次發布的時候,/service/parallel 都會有短暫的errorCount波動,因此推斷該問題是能夠通過重啟 /service/phpmoa/v1_microvideo_index 來復現的。
重啟線上服務有可能再次導致服務異常、影響線上業務,所以我們先嘗試在線上環境復制上下游項目、發布成不同的ServiceUri,并增加一個測試接口,通過壓測平臺制造流量,搭建起和線上調用鏈路基本一致的測試環境。
除了在MOA和MCF的代碼中增加各階段耗時的日志外,對于并行MOA出現線程池滿拒絕請求、以及出現10秒以上慢請求的場景,均增加了自動打印jstack的機制。
在適當調整模擬流量的壓力后,重啟測試的 /service/phpmoa/v1_microvideo_index 服務后,問題提復現了。這一次我們拿到了詳細的耗時信息,以及線程池滿后的jstack堆棧信息,才進一步分析到問題的根本原因。
問題復現后的jstack堆棧,611個線程停留在等待鎖的步驟
將JMX關閉后,對比其他未關閉的實例沒有再復現該問題
調用的下游服務極多、下游實例數極多,需要創建大量的JedisPool
下游重啟過程中并行MOA需要創建大量新的JedisPool,并且并行創建的線程數很多(最多800個)
下游服務發布后出問題(microvideo_index)、下游實例數多的服務發布問題嚴重(230個)、發布速度快的服務問題嚴重(2分鐘)、多個服務同時發布的時候問題嚴重(microvideo_index和user_location在同一時間段做發布)
各個并行MOA實例能夠自動恢復,但恢復的時間點差異較大(具體耗時取決于已有ID數量、并行創建JedisPool的線程數據量,各實例的情況可能不一致)
異常期間并行MOA服務的CPU使用率大幅升高(在頻繁獲取鎖)
相同時刻其他并行MOA的集群未出問題(因為請求量低、并行創建JedisPool的線程少)
業務上使用JedisPool的場景,多通過MCF的RedisDao封裝。RedisDao主要用于兩個場景
通過MomoStore訪問Redis數據源、訪問OneStore底層使用RedisDao。由于MomoStore對于新實例的連接建立是在接收事件通知后單線程執行的,受并發創建JedisPool的影響較少。
由于與下游新實例創建連接的動作是在業務請求中完成的,所以使用MOARedisClient的場景受并發創建JedisPool影響的可能性較大。當服務與并行MOA具備類似的特征:下游服務多、實例多,并行執行的請求多,在下游服務發布時也容易出現相同的問題。使用MOARedisClient在某些場景下的執行時間超出設定的timeout時間,也與該問題有關。
最簡單有效的解決方案是關閉JedisPool的JMX配置,可以在MCF的代碼中統一修改、通過升級MCF版本修復。對于已接入Mesh的服務,由于MOARedisClient實際與下游通信的地址是127.0.0.1,所需建立的連接池很少,所以不會受該問題影響。后續我們會掃描所有使用MOARedisClient、但尚未接入Mesh的服務,推動升級MCF版本消除這一隱患。
在MSC線程池中加入線程池滿自動打印jstack的機制。
“高并發場景創建JedisPool有哪些注意事項”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。