一次數據庫泄露的解決經歷

  • 2020 年 8 月 22 日
  • 筆記

前言

最近用了公司某框架,部署到現場後,現場運維開始維護現場數據,在不斷操作的過程中,系統崩潰,查看後台日誌,druid連接池已經獲取不到連接。於是開始了排查之旅。在此記錄。

排查開始

首先後台的報錯是這樣的。

exception=org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 10, maxActive 10

第一反應maxActive設置的數量太少了。於是改為100。重新啟動,並再次操作大量數據。發現過了一段時間100個也滿了。

此時問題不簡單了。看來是有代碼用了程序連接後,沒有釋放。

接下來開始確認原因到底是不是有沒有釋放。

在項目中使用的druid連接池。druid連接池是自帶圖形化監控工具的。於是開始在項目中配置,啟動druid連接池。

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DruidConfig {

   @Bean
   public ServletRegistrationBean statViewServlet(){
      ServletRegistrationBean srb =
         new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
      //設置控制台管理用戶
      srb.addInitParameter("loginUsername","root");
      srb.addInitParameter("loginPassword","root");
      //是否可以重置數據
      srb.addInitParameter("resetEnable","false");
      return srb;
   }

   /**
    * 註冊FilterRegistrationBean
    * @return
    */
   @Bean
   public FilterRegistrationBean druidStatFilter() {
      FilterRegistrationBean bean = new FilterRegistrationBean(new WebStatFilter());
      //添加過濾規則.
      bean.addUrlPatterns("/*");
      //添加不需要忽略的格式信息.
      bean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
      return bean;
   }
}

重新啟動後,訪問:localhost:8081/druid。這個路徑有人大概會懷疑如果我是通過網關管理的微服務框架,需要通過網關轉發訪問嗎?其實大可不必,通過網關也可以,也是跳轉到直接訪問的地址。

進入訪問地址,跳轉到如下的登錄界面。

輸入剛才在代碼中配置的用戶名和密碼,則可以成功進入。

我們此時要點擊數據源,去關注建立的邏輯連接數和關閉的邏輯連接數,關鍵的指標在這裡,如果連接池的開啟和關閉是正常的,那麼二者的值應該是相等的。

再看看此時的活躍連接數

為0,此時正常,因為還沒有進行操作。

接下來開始對之前的操作進行復現,鎖定具體的操作。重複之前現場運維所做操作。

通過不斷的點擊功能,縮小功能範圍,最終發現,只要點擊左側樹,就會造成邏輯打開連接和邏輯關閉次數不一致。

活躍連接數也到了二者之差。等了幾分鐘,仍然是這個情況。那麼實錘了 這裡的代碼有問題,連接應該沒有釋放。那麼代碼那麼多,該如何發現具體代碼的位置呢。

接下來配置druid的abandon策略。通過abandon可以強制回收數據庫的連接。而活躍的連接被回收則會打印堆棧信息,這是就知道是哪裡的sql代碼沒有釋放了。

配置如下:

spring:
  datasource:
    druid:
      remove-abandoned: true
      remove-abandoned-timeout: 30
      log-abandoned: true

重啟項目,這個時候durid的監控活躍連接數的功能就可以看到代碼信息。

我們點擊如下位置:

就會彈出上圖的堆棧信息。打馬賽克的地方就是代碼的詳細位置,會標記出來。開發人員去響應的類找到相應代碼查看即可。

經過查看代碼發現,代碼的連接釋放存在問題。是自己封裝的sql查詢類。改為mybatis的寫法後,問題解決。