當前位置:
首頁 >
记一次死锁问题的排查和解决
發布時間:2025/4/9
35
豆豆
生活随笔
收集整理的這篇文章主要介紹了
记一次死锁问题的排查和解决
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
? ? ?說起來這個事情還是挺悲催的,記得上周忙的不亦樂乎,目標是改動之前另外一個團隊留下來的一坨代碼中的一些bug,這個項目是做OLAP分析的。分為兩個模塊,邏輯server主要負責一些元數據的操作,比如頁面上展示的一些信息,而分析server負責運行查詢語句。由于他們之前使用的是mondrian作為OLAP分析引擎,所以輸入的查詢是MDX語句,然后結果是一個二維的數據。這是主要的項目背景,當然使用mondrian的過程中發現他的確夠慢的。 并且mondrian另一個問題,它的確在內部實現了一些緩存,緩存好像是基于cell的。可是它的緩存所有是保存在進程內部的,這就導致每個分析server是有狀態的,不能擴展成多個。否則就不能利用這些緩存了,另外,由于我們須要支持大量的數據源(每個產品可能有一個或者多個數據源),每個數據源可能定義多個報表,每個報表相應著一個MDX查詢語句。這就導致緩存的數據非常大,非常easy就造成OOM的現象。因此我們接下來的任務就是把這個緩存移出去。放到第三方的緩存系統中。 回到正題,正當忙完準備周五上線呢,上線之后沒怎么驗證就匆匆在用戶群里面吼了一聲,因此大家都打開點啊點,突然老大過來說怎么如今打開報表什么的這么慢啊。我查了一下發現的確挺慢的,為什么在測試環境中沒有發現呢?多次驗證之后開始懷疑自己可能真的改錯了什么了,立刻回滾到之前的版本號,然后就剩下我一頭汗水中排查究竟出現了什么問題。 好在將線上的環境切到測試環境中非常easy就把這個現象給復現了。主要是點開某個報表,然后經過一段時間的載入,接下來點開該報表之后就會快非常多,由于接下來的操作都是從緩存中獲取的。可是當我在頁面上點擊“清除緩存”之后(這個操作事實上時清除整個報表的緩存和mondrian內部的緩存),發現會等待非常長的時間才干返回。然后這個操作是異步的。在頁面上我還能進行其它操作。可是當我再次點擊其它報表的“清除緩存“的操作就會出現卡頓,然后發現打開其它的報表可能要等待一段時間,問題就這么非常easy的復現了。 之前沒有針對java這方面的排查經驗。可是也知道jstack,jmap之類的工具,于是馬上用jstack把整個進程的堆棧抓取下來(非常是懊悔沒有在回滾之前運行jstack),發現的確出現了問題: Found one Java-level deadlock:
=============================
"mondrian.rolap.RolapResultShepherd$executor_160":waiting to lock monitor 0x0000000043b2bf70 (object 0x0000000702080db0, a mondrian.rolap.MemberCacheHelper),which is held by "mondrian.rolap.RolapResultShepherd$executor_152"
"mondrian.rolap.RolapResultShepherd$executor_152":waiting to lock monitor 0x00007f4e6c0751c8 (object 0x0000000702081270, a mondrian.rolap.MemberCacheHelper),which is held by "http-8182-11"
"http-8182-11":waiting to lock monitor 0x0000000043b2bf70 (object 0x0000000702080db0, a mondrian.rolap.MemberCacheHelper),which is held by "mondrian.rolap.RolapResultShepherd$executor_152"
這意味著程序里面出現了死鎖。這里牽扯到了三個線程,可是當中的兩個線程都持有了一個鎖而且希望鎖住對方持有的鎖,而第三個線程正在等待前兩個線程中某個線程已經持有的鎖,有了這個堆棧就非常easy排查問題了,而且在堆棧信息中發現非常多線程都在等待這兩個線程中已經持有的鎖。可是由于這兩個線程已經處于死鎖狀態了,其它的線程僅僅能同步的等待。這樣繼續在前端操作這些報表遲早把tomcat中的線程消耗完。 依據堆棧找到相應的代碼,代碼運行的是清理緩存的操作,可是緩存是對于每個cube下的hierarchy創建的,因此依據詳細的堆棧中的調用信息例如以下: at mondrian.rolap.SmartMemberReader.flushCacheSimple(SmartMemberReader.java:577)- waiting to lock <0x00000007020a8990> (a mondrian.rolap.MemberCacheHelper)at mondrian.rolap.RolapCubeHierarchy$CacheRolapCubeHierarchyMemberReader.flushCacheSimple(RolapCubeHierarchy.java:883)at mondrian.rolap.RolapCubeHierarchy.flushCacheSimple(RolapCubeHierarchy.java:458)at mondrian.rolap.MemberCacheHelper.flushCache(MemberCacheHelper.java:166)- locked <0x00000007020a8e50> (a mondrian.rolap.MemberCacheHelper)at mondrian.rolap.RolapCubeHierarchy$CacheRolapCubeHierarchyMemberReader.flushCache(RolapCubeHierarchy.java:878)at mondrian.rolap.RolapCubeHierarchy.flushCache(RolapCubeHierarchy.java:451)
? ? ?最先進入的這個flushCache函數是hierarchy級別的緩存清理,它事實上是調用它的成員變量reader對象的clearCache方法。這個reader用于讀取這個hierarchy下的members,能夠直接從數據源(關系數據庫)中讀取,也維護了members的緩存,因此調用reader的clearCache方法也就是調用它的cache對象的方法,這個cache對象名為rolapCubeCacheHelper,類型為MemberCacheHelper,可是發如今reader中的clearCache方法運行的詳細操作例如以下: @Overridepublic void flushCache(){super.flushCache();rolapCubeCacheHelper.flushCache();}
? ? ?首先調用父類的flushCache方法。父類又是什么鬼,打開父類的flushCache方法發現更奇怪的事情: public void flushCache(){synchronized( cacheHelper){cacheHelper .flushCache();}}
? ? ?這是父類的flushCache方法,它事實上就是對成員變量的cacheHelper對象加鎖,然后使用cacheHelper的flushCache方法,打開cacheHelper對象才發現它又是一個MemberCacheHelper對象,這時候問題來了。為什么父類和子類都保存了一個MemberCacheHelper對象呢?事實上MemberCacheHelper這個對象就是一個緩存的結構體,父類有一些公有的緩存數據,子類有自己的緩存信息,這樣也能說得過去。繼續到MemberCacheHelper類的flushCache方法: // Must sync here because we want the three maps to be modified together.public synchronized void flushCache() {mapMemberToChildren.clear();mapKeyToMember.clear();mapLevelToMembers.clear();if (rolapHierarchy instanceof RolapCubeHierarchy){((RolapCubeHierarchy)rolapHierarchy ).flushCacheSimple();}// We also need to clear the approxRowCount of each level.for (Level level : rolapHierarchy.getLevels()) {((RolapLevel)level ).setApproxRowCount(Integer. MIN_VALUE);}}
這里對緩存中的每個map進行clear。然后又對這個hierarchy運行flushCacheSimple方法,我勒個擦。怎么又回來了,這個hierarchy對象不就是我們進出進入flushCache的那個hierarchy對象嗎?過了一遍flushCacheSimple方法發現它終于又調用了reader的flushCacheSimple方法,這個函數運行的操作類似于flushCache: public void flushCacheSimple(){super.flushCacheSimple();rolapCubeCacheHelper.flushCacheSimple();}
好了。繼續到MemberCacheHelper的flushCacheSimple方法: public void flushCacheSimple(){synchronized(cacheHelper){cacheHelper.flushCacheSimple();}}
我勒個擦。這里又加鎖,之前不是已經加過了嗎?當然這個鎖因該是可重入的,這里自然不會造成死鎖,可是以下的rolapCubeCacheHelper對象也是MemberCacheHelper對象啊!
這里面運行的操作和flushCache方法不是一樣的嗎?。這究竟是在做什么。當然理了這么多也發現了出現死鎖的根源了。就在于reader運行的flushCache方法,這里面分別調用了父類和當前類的cacheHelper對象的flushCache,可是這種方法還會調用flushCacheSimple方法。這種方法再次調用reader的flushCacheSimple方法,這里再次調用父類和當前類的cacheHelper對象的flushCacheSimple方法。并且每次調用都須要加鎖。這就導致了例如以下的死鎖情況: A線程運行flushCache方法,它已經完畢了super.flushCache方法,然后運行當前reader對象的flushCache方法。首先及時須要持有這個helper對象的鎖。然后再運行到flushCacheSimple的時候申請父類的helper對象的鎖。
開始沒有定位到這個問題之前不曉得死鎖究竟是怎么回事造成的,于是想著讓全部的線程順序運行flushCache方法就能夠避免死鎖了(不要并發了),可是嘗試了一下發現不能這樣,由于其它線程還是有可能調用這個flushCache方法,這個不是由我控制的。于是僅僅能詳細了解這個函數究竟運行了什么,發現flushCache和flushCacheSimple方法事實上是反復的,不曉得當初寫這段代碼的人是怎么想的,于是就把全部的flushCacheSimple方法的調用去掉。這樣就不會再有持有A鎖再去申請B鎖的情況了。
問題算是攻克了,終于hotfix版本號也算是上線了,一顆懸著的心也算放下了,著這個過程中我也學到了不少知識: 1、學會而且善于使用java提供的分析工具。比如jstack、jstat、jmap、以及開源的MAT等等。 2、遇到問題不要害怕,不要一味的埋怨這個問題不是我造成的,我也不知道怎么回事之類的。靜下心來思考整個流程,運用曾經的理論知識和經驗一定可以把問題解決的。沒有什么問題是偶然的。假設出錯一定是代碼有問題。 3、測試非常重要。尤其壓力測試,我們項目眼下人手緊缺。QA也沒有專職的,所以基本上是開發在開發環境上測試一下功能。并沒有做過性能測試之類的東西,我認為測試應該盡可能覆蓋線上可能出現的各種情況。 4、上線之前做好回滾,否則你會非常狼狽,幸虧這點我每次操作之前都先備份。 5、在編碼的時候,尤其一個操作會涉及到多個synchronized操作的時候尤其要注意,回顧一下當初避免死鎖的幾個方法。按順序加鎖往往是最好的解決的方法。 6、搞清楚一個方法究竟想要做什么?輸入是什么,輸出是什么。會造成什么影響。在寫完一個方法之后在腦子中模擬一下整個函數的運行流程是否符合預想。 7、假設真的遇到這種需求:父類和子類都持有一個類型的對象,讓他們獨立操作。父類對象的操作完畢之后在運行子類對象的操作,而不要穿插著調用。
接下來的一段時間要開始搞mondrian了,希望可以從這個OLAP運行引擎中學到一些東西。只是自己的編譯原理方面的知識差點兒為0,這方面須要補強啊,我對于mondrian中重點要看的東西應該是:1、怎樣解析MDX(類似于怎樣解析SQL)。2、怎樣將MDX動態的翻譯成一串SQL(類似于怎樣生成運行計劃),3、緩存怎樣實現,4、運行MDX或者SQL時怎樣使用緩存。5、假設使用聚合表進行優化。 希望順利~
這意味著程序里面出現了死鎖。這里牽扯到了三個線程,可是當中的兩個線程都持有了一個鎖而且希望鎖住對方持有的鎖,而第三個線程正在等待前兩個線程中某個線程已經持有的鎖,有了這個堆棧就非常easy排查問題了,而且在堆棧信息中發現非常多線程都在等待這兩個線程中已經持有的鎖。可是由于這兩個線程已經處于死鎖狀態了,其它的線程僅僅能同步的等待。這樣繼續在前端操作這些報表遲早把tomcat中的線程消耗完。 依據堆棧找到相應的代碼,代碼運行的是清理緩存的操作,可是緩存是對于每個cube下的hierarchy創建的,因此依據詳細的堆棧中的調用信息例如以下: at mondrian.rolap.SmartMemberReader.flushCacheSimple(SmartMemberReader.java:577)- waiting to lock <0x00000007020a8990> (a mondrian.rolap.MemberCacheHelper)at mondrian.rolap.RolapCubeHierarchy$CacheRolapCubeHierarchyMemberReader.flushCacheSimple(RolapCubeHierarchy.java:883)at mondrian.rolap.RolapCubeHierarchy.flushCacheSimple(RolapCubeHierarchy.java:458)at mondrian.rolap.MemberCacheHelper.flushCache(MemberCacheHelper.java:166)- locked <0x00000007020a8e50> (a mondrian.rolap.MemberCacheHelper)at mondrian.rolap.RolapCubeHierarchy$CacheRolapCubeHierarchyMemberReader.flushCache(RolapCubeHierarchy.java:878)at mondrian.rolap.RolapCubeHierarchy.flushCache(RolapCubeHierarchy.java:451)
? ? ?最先進入的這個flushCache函數是hierarchy級別的緩存清理,它事實上是調用它的成員變量reader對象的clearCache方法。這個reader用于讀取這個hierarchy下的members,能夠直接從數據源(關系數據庫)中讀取,也維護了members的緩存,因此調用reader的clearCache方法也就是調用它的cache對象的方法,這個cache對象名為rolapCubeCacheHelper,類型為MemberCacheHelper,可是發如今reader中的clearCache方法運行的詳細操作例如以下: @Overridepublic void flushCache(){super.flushCache();rolapCubeCacheHelper.flushCache();}
? ? ?首先調用父類的flushCache方法。父類又是什么鬼,打開父類的flushCache方法發現更奇怪的事情: public void flushCache(){synchronized( cacheHelper){cacheHelper .flushCache();}}
? ? ?這是父類的flushCache方法,它事實上就是對成員變量的cacheHelper對象加鎖,然后使用cacheHelper的flushCache方法,打開cacheHelper對象才發現它又是一個MemberCacheHelper對象,這時候問題來了。為什么父類和子類都保存了一個MemberCacheHelper對象呢?事實上MemberCacheHelper這個對象就是一個緩存的結構體,父類有一些公有的緩存數據,子類有自己的緩存信息,這樣也能說得過去。繼續到MemberCacheHelper類的flushCache方法: // Must sync here because we want the three maps to be modified together.public synchronized void flushCache() {mapMemberToChildren.clear();mapKeyToMember.clear();mapLevelToMembers.clear();if (rolapHierarchy instanceof RolapCubeHierarchy){((RolapCubeHierarchy)rolapHierarchy ).flushCacheSimple();}// We also need to clear the approxRowCount of each level.for (Level level : rolapHierarchy.getLevels()) {((RolapLevel)level ).setApproxRowCount(Integer. MIN_VALUE);}}
這里對緩存中的每個map進行clear。然后又對這個hierarchy運行flushCacheSimple方法,我勒個擦。怎么又回來了,這個hierarchy對象不就是我們進出進入flushCache的那個hierarchy對象嗎?過了一遍flushCacheSimple方法發現它終于又調用了reader的flushCacheSimple方法,這個函數運行的操作類似于flushCache: public void flushCacheSimple(){super.flushCacheSimple();rolapCubeCacheHelper.flushCacheSimple();}
好了。繼續到MemberCacheHelper的flushCacheSimple方法: public void flushCacheSimple(){synchronized(cacheHelper){cacheHelper.flushCacheSimple();}}
我勒個擦。這里又加鎖,之前不是已經加過了嗎?當然這個鎖因該是可重入的,這里自然不會造成死鎖,可是以下的rolapCubeCacheHelper對象也是MemberCacheHelper對象啊!
最后進入flushCacheSimple方法,這徹底凌亂了:
public synchronized void flushCacheSimple() {mapMemberToChildren.clear();mapKeyToMember.clear();mapLevelToMembers.clear();// We also need to clear the approxRowCount of each level.for (Level level : rolapHierarchy.getLevels()) {((RolapLevel)level).setApproxRowCount(Integer.MIN_VALUE);}}這里面運行的操作和flushCache方法不是一樣的嗎?。這究竟是在做什么。當然理了這么多也發現了出現死鎖的根源了。就在于reader運行的flushCache方法,這里面分別調用了父類和當前類的cacheHelper對象的flushCache,可是這種方法還會調用flushCacheSimple方法。這種方法再次調用reader的flushCacheSimple方法,這里再次調用父類和當前類的cacheHelper對象的flushCacheSimple方法。并且每次調用都須要加鎖。這就導致了例如以下的死鎖情況: A線程運行flushCache方法,它已經完畢了super.flushCache方法,然后運行當前reader對象的flushCache方法。首先及時須要持有這個helper對象的鎖。然后再運行到flushCacheSimple的時候申請父類的helper對象的鎖。
B線程可能在運行super.flushCache進入這個函數意味著須要持有父類的helper,可是當它運行flushCacheSimple的時候有須要申請當前類的helper對象的鎖。于是就造成了死鎖。
開始沒有定位到這個問題之前不曉得死鎖究竟是怎么回事造成的,于是想著讓全部的線程順序運行flushCache方法就能夠避免死鎖了(不要并發了),可是嘗試了一下發現不能這樣,由于其它線程還是有可能調用這個flushCache方法,這個不是由我控制的。于是僅僅能詳細了解這個函數究竟運行了什么,發現flushCache和flushCacheSimple方法事實上是反復的,不曉得當初寫這段代碼的人是怎么想的,于是就把全部的flushCacheSimple方法的調用去掉。這樣就不會再有持有A鎖再去申請B鎖的情況了。
問題算是攻克了,終于hotfix版本號也算是上線了,一顆懸著的心也算放下了,著這個過程中我也學到了不少知識: 1、學會而且善于使用java提供的分析工具。比如jstack、jstat、jmap、以及開源的MAT等等。 2、遇到問題不要害怕,不要一味的埋怨這個問題不是我造成的,我也不知道怎么回事之類的。靜下心來思考整個流程,運用曾經的理論知識和經驗一定可以把問題解決的。沒有什么問題是偶然的。假設出錯一定是代碼有問題。 3、測試非常重要。尤其壓力測試,我們項目眼下人手緊缺。QA也沒有專職的,所以基本上是開發在開發環境上測試一下功能。并沒有做過性能測試之類的東西,我認為測試應該盡可能覆蓋線上可能出現的各種情況。 4、上線之前做好回滾,否則你會非常狼狽,幸虧這點我每次操作之前都先備份。 5、在編碼的時候,尤其一個操作會涉及到多個synchronized操作的時候尤其要注意,回顧一下當初避免死鎖的幾個方法。按順序加鎖往往是最好的解決的方法。 6、搞清楚一個方法究竟想要做什么?輸入是什么,輸出是什么。會造成什么影響。在寫完一個方法之后在腦子中模擬一下整個函數的運行流程是否符合預想。 7、假設真的遇到這種需求:父類和子類都持有一個類型的對象,讓他們獨立操作。父類對象的操作完畢之后在運行子類對象的操作,而不要穿插著調用。
接下來的一段時間要開始搞mondrian了,希望可以從這個OLAP運行引擎中學到一些東西。只是自己的編譯原理方面的知識差點兒為0,這方面須要補強啊,我對于mondrian中重點要看的東西應該是:1、怎樣解析MDX(類似于怎樣解析SQL)。2、怎樣將MDX動態的翻譯成一串SQL(類似于怎樣生成運行計劃),3、緩存怎樣實現,4、運行MDX或者SQL時怎樣使用緩存。5、假設使用聚合表進行優化。 希望順利~
轉載于:https://www.cnblogs.com/liguangsunls/p/7136843.html
總結
以上是生活随笔為你收集整理的记一次死锁问题的排查和解决的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL查询日志总结
- 下一篇: 数组玩法揭秘