一、前言
記錄一次服務假死的整個排查過程,服務基礎爲spring boot + druid + 多數據源切換,在請求過多(尤其是長事務請求)時,服務出現請求無響應的狀況,之前未完結的查詢也沒有任何返回結果。
二、定位問題原因
問題出現時,表現如下圖,後臺無任何報錯,sql語句戛然而止,後續的查詢被中斷。這時如果再次發起某個請求,後臺服務處於大部分時間不能收到新的請求的狀態,或者偶爾可收到請求但不會執行crud。經過一段時間後,日誌輸出了session校驗的內容,此時我推測服務並不是真的宕機,而是處於假死狀態。
druid配置如下
看了下配置文件,最大連接池數量設置爲100,但重現問題的過程不需要特別多的請求,穩妥起見,將最大數量改爲200,問題沒有解決,初步推測不是因爲請求達到了連接池上限。
CPU和內存均無異常。
期間還優化過業務邏輯,甚至將同步請求改爲異步請求,都無濟於事。一籌莫展之際,想起了druid自帶的監控功能。打開監控頁面,找到了一處令我懷疑的地方。數據源中的邏輯打開和邏輯關閉次數起初是一致的,隨着查詢次數增多,邏輯關閉次數小於邏輯打開次數,於是我懷疑數據庫連接池出現了泄露的情況,根據URI監控中顯示的情況,jdbc出錯數剛好等於邏輯打開與邏輯關閉次數的差,也就是說,很有可能由於jdbc出錯導致數據庫連接池未正確關閉。
(圖片爲部分截圖,下面還有個1沒有截到)
按着這個思路,對測試部分代碼進行了排查,無果。後來根據druid官方的文檔,找到了下面這段話。
從這段話中可以看出,判斷是否是泄露應該在URI監控中,點擊URI進入詳情頁面,查看打開和關閉的數量是否相等。於是我在邏輯連接打開和邏輯連接關閉次數有巨大差異的情況下,對每一個URI都進行了覈查,所有URI詳情中,連接池獲取連接次數都是等於連接池關閉連接次數的,理論上證明數據庫連接池並無泄露。
前面圖中druid的文檔中還提到了removeAbandoned等三個屬性用以檢測數據庫連接池泄露,於是我將這三個屬性寫在了yml裏。
一通華麗麗的操作下來,服務依舊被玩壞,然後打開了druid的監控,找到了數據源頁面中的ActiveConnectionStackTrace。點開,沒數據???控制檯也沒輸出日誌??? 網上找了一些文章,然後大膽的懷疑是不是druid的配置沒生效?再看一下druid運行時的數據源,果不其然。初始化連接大小、最小空閒連接數、最大連接數、超時時間等等等等,除了數據庫指向是生效的,其他配置使用的都是缺省值。所以我即使把最大數量從100改成了200,依然是沒幾個請求就爆炸了。
三、解決方案
下面開始着手解決配置沒生效的問題。由於項目是多數據源切換,於是找到了這個配置文件,嘗試着改造了一下,將yml中的配置set到了DruidDataSource對象中。
(下圖僅展示了與本文有關的內容)
@Configuration
public class DruidConfig {
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
@Value("${spring.datasource.druid.pool-prepared-statements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.druid.max-pool-prepared-statement-per-connection-size}")
private int maxPoolPreparedStatementPerConnectionSize;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.min-evictable-idle-time-millis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.test-while-idle}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.test-on-borrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.test-on-return}")
private boolean testOnReturn;
@Value("${spring.datasource.druid.remove-abandoned}")
private boolean removeAbandoned;
@Value("${spring.datasource.druid.remove-abandoned-timeout}")
private int removeAbandonedTimeout;
@Value("${spring.datasource.druid.log-abandoned}")
private boolean logAbandoned;
/**
* 設置數據庫連接池
*
* @author sunbin
* @since 2020年4月21日
* @version 2020年4月21日
* @param dataSource
*/
private void setDruidDataSource(DruidDataSource dataSource) {
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
dataSource.setRemoveAbandoned(removeAbandoned);
dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
dataSource.setLogAbandoned(logAbandoned);
}
/**
* 默認數據源
*
* @author 89390
* @since 2019年4月15日
* @version 2019年4月15日
* @return
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
setDruidDataSource(dataSource);
return dataSource;
}
}
重啓服務,查看druid monitor,與yml中配置的一致了,即證明此時配置已經生效,再次測試,服務假死問題解決。
四、分析
項目中,spring boot版本爲2.0.3.RELEASE
druid版本爲1.1.10
考慮到版本等問題,雖然我們在yml中配置了druid連接池的其它屬性,但是不會生效。因爲默認是使用的java.sql.Datasource的類來獲取屬性的,有些屬性datasource沒有。如果我們想讓配置生效,需要手動創建Druid的配置文件。由於項目中多數據源的功能,已經有了Druid的配置文件,所以無需新建,適當修改即可。
五、參考
https://github.com/alibaba/druid/wiki/%E8%BF%9E%E6%8E%A5%E6%B3%84%E6%BC%8F%E7%9B%91%E6%B5%8B