狀況描述:
最近項目新打的版本,過不了多長時間,項目就會掛掉。狀況就是處於一種假死的狀態。索引查詢都很慢,幾乎進行不了任何操作,慢慢卡死。
然後我們再發版時,只能基於之前打好的war包,替換或者增加class檔案。
情況對比及分析:
由於之前代碼做過一次大整頓,提交的代碼比較多,所以通過復原版本的方式解決,比較困難。一是因為整頓的成果不能白白抹殺;二是那麼多檔案,靠人工挨個對比尋找,比較困難。
解決方案一:
之前, 一直對目前項目的打包方式心存質疑,所以這次發生問題時,我首先懷疑的對象是Jenkins和生產的Tomcat伺服器。我通過Bastion Host串連到生產時,發現通過Jenkins啟動應用程式,會啟動兩個兩個tomcat進程。
然後,這似乎更加堅定了我的看法,馬上就找到了營運,但是經確認後,是沒有問題的。一個是用root使用者啟動的,一個是用tomcat使用者啟動的。一個守護進程,一個應用進程。
解決方案二:
排除了伺服器的問題,開始正面考慮程式的問題。
重新發項目有問題的版本,Dump下來的日誌,然後迅速復原觀察。單台機器的dump日誌有5個G:
通過Memory Analyzer分析,在Leak Supects Report 視圖中,有如下分析結果:
上圖所示,共有三類問題a、b、c;還有一些其他的,類型為d。
先來看第一個問題(後來發現,前幾個問題都是同一個問題)
先點開Details看一下:
上圖顯示了一個很明顯的有問題的線程:地址是0x7c8ff3df0 ,名稱為pool-16-thread-1。
通過《Accumulated Objects in Dominator Tree》視圖可以看出,在該線程中,存在一個大的List對象,List對象記憶體放了大量的mysql的jdbc對象。
我們想看看JDBC對象裡面對方了那些資料。接下來我們開啟《open dominator tree for entire heap》這個視圖:
找到名為0x7c8ff3df0 pool-16-thread-1的線程。如圖也能發現,這個線程佔用了大量的空間未釋放。一層層開啟裡面的存放的對象:
這裡的資料,是我們的一張使用者表的資料。所以這就可以得出結論:一個線程內,一個list記憶體放了大量從資料庫中擷取的使用者物件。
想到這裡,我們又去看了b、c的問題描述,也是同樣的問題。估計是在不同時間點,通過gc已經回收了部分。
然後,我剛才看了a問題的details資訊,接下來我們看下a的stacktrace 堆棧資訊。
如上圖,問題就很明顯了,在Service的112行中,調用的findByCustomerID方法中,有掃描全表的操作。經過分析,找到對應的位置,對應的代碼為:
customerID = StringUtils.isNotBlank(customerID)? customer.getCustomer().getCustomerID():null; Customer oldCutsomer = customerService.findByCustomerID(customerID);
顯而易見,流程走到這裡時,customerID永遠為空白,那麼customerService.findByCustomerID(customerID)方法,會執行掃描全表操作。由於該表資料量巨大,開發所認為的使用者每次執行的索引查詢,實際上都成了慢查詢,而且需要返回全表資料。大量線程過來,佔用大量資料庫連接,導致資料庫連接數不夠;而每個線程處理時,需要大量時間, 導致項目處於一種假死的狀態。
總結分析:
1、代碼管理不規範,導致的這一場鬧劇;
2、開發許可權太小,導致一丁點兒操作都需要各種跨部門申請;
3、顧及彼此的面子,一些問題得不到深刻的反思,也沒有更好的方案來預防。