dbcp基本配置和重連配置

最近在看一些dbcp的相關內容,順便做一下記錄,免得自己給忘記了。

1. 引入dbcp (選擇1.4)
Java代碼  收藏代碼
  1. <dependency>  
  2.     <groupId>com.alibaba.external</groupId>  
  3.     <artifactId>jakarta.commons.dbcp</artifactId>  
  4.     <version>1.4</version>  
  5. </dependency>  

 

2. dbcp的基本配置

相關配置說明:

 

  1. initialSize :連接池啓動時創建的初始化連接數量(默認值爲0)
  2. maxActive :連接池中可同時連接的最大的連接數(默認值爲8,調整爲20,高峯單機器在20併發左右,自己根據應用場景定)
  3. maxIdle:連接池中最大的空閒的連接數,超過的空閒連接將被釋放,如果設置爲負數表示不限制(默認爲8個,maxIdle不能設置太小,因爲假如在高負載的情況下,連接的打開時間比關閉的時間快,會引起連接池中idle的個數 上升超過maxIdle,而造成頻繁的連接銷燬和創建,類似於jvm參數中的Xmx設置)
  4. minIdle:連接池中最小的空閒的連接數,低於這個數量會被創建新的連接(默認爲0,調整爲5,該參數越接近maxIdle,性能越好,因爲連接的創建和銷燬,都是需要消耗資源的;但是不能太大,因爲在機器很空閒的時候,也會創建低於minidle個數的連接,類似於jvm參數中的Xmn設置)
  5. maxWait  :最大等待時間,當沒有可用連接時,連接池等待連接釋放的最大時間,超過該時間限制會拋出異常,如果設置-1表示無限等待(默認爲無限,調整爲60000ms,避免因線程池不夠用,而導致請求被無限制掛起)
  6. poolPreparedStatements:開啓池的prepared(默認是false,未調整,經過測試,開啓後的性能沒有關閉的好。)
  7. maxOpenPreparedStatements:開啓池的prepared 後的同時最大連接數(默認無限制,同上,未配置)
  8. minEvictableIdleTimeMillis  :連接池中連接,在時間段內一直空閒, 被逐出連接池的時間
  9. (默認爲30分鐘,可以適當做調整,需要和後端服務端的策略配置相關)
  10. removeAbandonedTimeout  :超過時間限制,回收沒有用(廢棄)的連接(默認爲 300秒,調整爲180)
  11. removeAbandoned  :超過removeAbandonedTimeout時間後,是否進 行沒用連接(廢棄)的回收(默認爲false,調整爲true)

removeAbandoned參數解釋:
  1. 如果開啓了removeAbandoned,當getNumIdle() < 2) and (getNumActive() > getMaxActive() - 3)時被觸發.
  2. 舉例當maxActive=20, 活動連接爲18,空閒連接爲1時可以觸發"removeAbandoned".但是活動連接只有在沒有被使用的時間超 過"removeAbandonedTimeout"時才被回收
  3. logAbandoned: 標記當連接被回收時是否打印程序的stack traces日誌(默認爲false,未調整)

一般會是幾種情況出現需要removeAbandoned: 
  1. 代碼未在finally釋放connection , 不過我們都用sqlmapClientTemplate,底層都有鏈接釋放的過程
  2. 遇到數據庫死鎖。以前遇到過後端存儲過程做了鎖表操作,導致前臺集羣中連接池全都被block住,後續的業務處理因爲拿不到鏈接所有都處理失敗了。

一份優化過的配置:
基本配置代碼  收藏代碼
  1. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">   
  2.     <property name="driverClassName" value="com.mysql.jdbc.Driver" />  
  3.     <property name="url" value="xxxx" />  
  4.     <property name="username"><value>xxxx</value></property>  
  5.         <property name="password"><value>xxxxx</value></property>  
  6.         <property name="maxActive"><value>20</value></property>  
  7.         <property name="initialSize"><value>1</value></property>  
  8.         <property name="maxWait"><value>60000</value></property>  
  9.         <property name="maxIdle"><value>20</value></property>  
  10.         <property name="minIdle"><value>3</value></property>  
  11.         <property name="removeAbandoned"><value>true</value></property>  
  12.         <property name="removeAbandonedTimeout"><value>180</value></property>  
  13.         <property name="connectionProperties"><value>clientEncoding=GBK</value></property>  
  14. </bean>  
 
 

2. dbcp的鏈接validate配置
  1. dbcp是採用了commons-pool做爲其連接池管理,testOnBorrow,testOnReturn, testWhileIdle是pool是提供的幾種校驗機制,通過外部鉤子的方式回調dbcp的相關數據庫鏈接(validationQuery)校驗
  2. dbcp相關外部鉤子類:PoolableConnectionFactory,繼承於common-pool PoolableObjectFactory
  3. dbcp通過GenericObjectPool這一入口,進行連接池的borrow,return處理
  4. testOnBorrow : 顧明思義,就是在進行borrowObject進行處理時,對拿到的connection進行validateObject校驗
  5. testOnReturn : 顧明思義,就是在進行returnObject對返回的connection進行validateObject校驗,個人覺得對數據庫連接池的管理意義不大
  6. testWhileIdle : 關注的重點,GenericObjectPool中針對pool管理,起了一個Evict的TimerTask定時線程進行控制(可通過設置參數timeBetweenEvictionRunsMillis>0),定時對線程池中的鏈接進行validateObject校驗,對無效的鏈接進行關閉後,會調用ensureMinIdle,適當建立鏈接保證最小的minIdle連接數。
  7. timeBetweenEvictionRunsMillis,設置的Evict線程的時間,單位ms,大於0纔會開啓evict檢查線程
  8. validateQuery, 代表檢查的sql
  9. validateQueryTimeout, 代表在執行檢查時,通過statement設置,statement.setQueryTimeout(validationQueryTimeout)
  10. numTestsPerEvictionRun,代表每次檢查鏈接的數量,建議設置和maxActive一樣大,這樣每次可以有效檢查所有的鏈接.
Validate配置代碼  收藏代碼
  1. <property name="testWhileIdle"><value>true</value></property> <!-- 打開檢查,用異步線程evict進行檢查 -->  
  2.     <property name="testOnBorrow"><value>false</value></property>  
  3.     <property name="testOnReturn"><value>false</value></property>  
  4.     <property name="validationQuery"><value>select sysdate from dual</value></property>  
  5.     <property name="validationQueryTimeout"><value>1</value></property>  
  6.     <property name="timeBetweenEvictionRunsMillis"><value>30000</value></property>  
  7.     <property name="numTestsPerEvictionRun"><value>20</value></property>  

 相關配置需求:

 

  1. 目前網站的應用大部分的瓶頸還是在I/O這一塊,大部分的I/O還是在數據庫的這一層面上,每一個請求可能會調用10來次SQL查詢,如果不走事務,一個請求會重複獲取鏈接,如果每次獲取鏈接都進行validateObject,性能開銷不是很能接受,可以假定一次SQL操作消毫0.5~1ms(一般走了網絡請求基本就這數)
  2. 網站異常數據庫重啓,網絡異常斷開的頻率是非常低的,一般也就在數據庫升級,演習維護時纔會進行,而且一般也是選在晚上,訪問量相對比較低的請求,而且一般會有人員值班關注,所以異步的validateObject是可以接受,但一個前提需要確保能保證在一個合理的時間段內,數據庫能完成自動重聯。

從代碼層面簡單介紹下dbcp的validate實現:

1.  common-pools提供的PoolableObjectFactory,針對pool池的管理操作接口

 

Java代碼  收藏代碼
  1. public interface PoolableObjectFactory {  
  2.   
  3.   Object makeObject() throws Exception;  
  4.   
  5.   void destroyObject(Object obj) throws Exception;  
  6.   
  7.   boolean validateObject(Object obj);  
  8.   
  9.   void activateObject(Object obj) throws Exception;  
  10.   
  11.   void passivateObject(Object obj) throws Exception;  
  12. }  

 

 

2. dbcp實現的pool從池管理操作

 

這裏貼了一個相關validate代碼,具體類可見:PoolableConnectionFactory.validateConnection()

 

Java代碼  收藏代碼
  1. public class PoolableConnectionFactory implements PoolableObjectFactory {  
  2.   
  3. ......  
  4. public boolean validateObject(Object obj) { //驗證validateObject  
  5.         if(obj instanceof Connection) {  
  6.             try {  
  7.                 validateConnection((Connection) obj);  
  8.                 return true;  
  9.             } catch(Exception e) {  
  10.                 return false;  
  11.             }  
  12.         } else {  
  13.             return false;  
  14.         }  
  15.     }  
  16. public void validateConnection(Connection conn) throws SQLException {  
  17.         String query = _validationQuery;  
  18.         if(conn.isClosed()) {  
  19.             throw new SQLException("validateConnection: connection closed");  
  20.         }  
  21.         if(null != query) {  
  22.             Statement stmt = null;  
  23.             ResultSet rset = null;  
  24.             try {  
  25.                 stmt = conn.createStatement();  
  26.                 if (_validationQueryTimeout > 0) {  
  27.                     stmt.setQueryTimeout(_validationQueryTimeout);  
  28.                 }  
  29.                 rset = stmt.executeQuery(query);  
  30.                 if(!rset.next()) {  
  31.                     throw new SQLException("validationQuery didn't return a row");  
  32.                 }  
  33.             } finally {  
  34.                 if (rset != null) {  
  35.                     try {  
  36.                         rset.close();  
  37.                     } catch(Exception t) {  
  38.                         // ignored  
  39.                     }  
  40.                 }  
  41.                 if (stmt != null) {  
  42.                     try {  
  43.                         stmt.close();  
  44.                     } catch(Exception t) {  
  45.                         // ignored  
  46.                     }  
  47.                 }  
  48.             }  
  49.         }  
  50.     }  
  51.   
  52. ....  
  53.   
  54. }  

 

3. pool池的evict調用代碼:GenericObjectPool (apache commons pool version 1.5.4)

Java代碼  收藏代碼
  1. protected synchronized void startEvictor(long delay) { //啓動Evictor爲TimerTask  
  2.         if(null != _evictor) {  
  3.             EvictionTimer.cancel(_evictor);  
  4.             _evictor = null;  
  5.         }  
  6.         if(delay > 0) {  
  7.             _evictor = new Evictor();  
  8.             EvictionTimer.schedule(_evictor, delay, delay);  
  9.         }  
  10.     }  
  11.   
  12. for (int i=0,m=getNumTests();i<m;i++) {  
  13.             final ObjectTimestampPair pair;  
  14.            .......  
  15.             boolean removeObject = false;  
  16.             // 空閒鏈接處理  
  17.             final long idleTimeMilis = System.currentTimeMillis() - pair.tstamp;  
  18.             if ((getMinEvictableIdleTimeMillis() > 0) &&  
  19.                     (idleTimeMilis > getMinEvictableIdleTimeMillis())) {  
  20.                 removeObject = true;  
  21.             } else if ((getSoftMinEvictableIdleTimeMillis() > 0) &&  
  22.                     (idleTimeMilis > getSoftMinEvictableIdleTimeMillis()) &&  
  23.                     ((getNumIdle() + 1)> getMinIdle())) {   
  24.                 removeObject = true;  
  25.             }  
  26.             //  testWhileIdle sql 檢查處理  
  27.             if(getTestWhileIdle() && !removeObject) {  
  28.                 boolean active = false;  
  29.                 try {  
  30.                     _factory.activateObject(pair.value);  
  31.                     active = true;  
  32.                 } catch(Exception e) {  
  33.                     removeObject=true;  
  34.                 }  
  35.                 if(active) {  
  36.                     if(!_factory.validateObject(pair.value)) {   
  37.                         removeObject=true;  
  38.                     } else {  
  39.                         try {  
  40.                             _factory.passivateObject(pair.value);  
  41.                         } catch(Exception e) {  
  42.                             removeObject=true;  
  43.                         }  
  44.                     }  
  45.                 }  
  46.             }  
  47.             // 真正關閉  
  48.             if (removeObject) {  
  49.                 try {  
  50.                     _factory.destroyObject(pair.value);  
  51.                 } catch(Exception e) {  
  52.                     // ignored  
  53.                 }  
  54.             }  
  55.           ........  
 

注意: 目前dbcp的pool的實現是使用了公用的apache common pools進行擴展處理,所以和原生的連接池處理,代碼看上去有點彆扭,感覺自動重連這塊異常處理不怎麼好,我也就只重點關注了這部分代碼而已   .


 

3. dbcp的鏈接自動重鏈相關測試

相關場景:

  1. 數據庫意外重啓後,原先的數據庫連接池能自動廢棄老的無用的鏈接,建立新的數據庫鏈接
  2. 網絡異常中斷後,原先的建立的tcp鏈接,應該能進行自動切換

測試需求1步驟

  1. 建立一testCase代碼
  2. 配置mysql數據庫
  3. 循環執行在SQL查詢過程
  4. 異常重啓mysql數據庫

測試需求2步驟

  1. 建立一testCase代碼
  2. 配置mysql數據庫
  3. 循環執行在SQL查詢過程
  4. 通過iptables禁用網絡鏈接 

/sbin/iptables -A INPUT -s 10.16.2.69 -j REJECT
/sbin/iptables -A FORWARD -p tcp -s 10.16.2.69 --dport 3306 -m state --state NEW,ESTABLISHED -j DROP

     5. iptables -F 清空規則,恢復鏈接通道。

 

測試需求問題記錄

 

分別測試了兩種配置,有validateObject的配置和沒有validateObject的相關配置。

1. 沒有validate配置
問題一: 異常重啓mysql數據庫後,居然也可以自動恢復鏈接,sql查詢正常
跟蹤了一下代碼,發現這麼一個問題:

  1. 在數據庫關閉的時候,client中pool通過borrowObject獲取一個異常鏈接返回給client
  2. client在使用具體的異常鏈接進行sql調用出錯了,拋了異常
  3. 在finally,調用connection.close(),本意是應該調用pool通過returnObject返回到的池中,但在跟蹤代碼時,未見調用GenericObjectPool的returnObject
  4. 繼續查,發現在dbcp在中PoolingDataSource(實現DataSource接口)調用PoolableConnection(dbcp pool相關的delegate操作)進行相應關閉時,會檢查_conn.isClosed(),針對DataSource如果isClosed返回爲true的則不調用returnObject,直接丟棄了鏈接  

解釋:

  • 正因爲在獲取異常鏈接後,因爲做了_conn.isClosed()判斷,所以異常鏈接並沒有返回到連接池中,所以到數據庫重啓恢復後,每次都是調用pool重新構造一個新的connection,所以後面就正常了
  • _conn.isClosed()是否保險,從jdk的api描述中: A connection is closed if the method close has been called on it or if certain fatal errors have occurred. 裏面提供兩種情況,一種就是被調用了closed方法,另一種就是出現一些異常也說的比較含糊。

問題二:validateObject調用時,dbcp設置的validationQueryTimeout居然沒效果

看了mysql statement代碼實現,找到了答案。 

mysql com.mysql.jdbc.statemen 部分代碼

 

timeout時間處理:

Java代碼  收藏代碼
  1. timeoutTask = new CancelTask();  
  2. //通過TimerTask啓動一定時任務  
  3. Connection.getCancelTimer().schedule(timeoutTask,  this.timeoutInMillis);  

 

對應的CancelTask的代碼: 

 

Java代碼  收藏代碼
  1. class CancelTask extends TimerTask {  
  2.   
  3.         long connectionId = 0;  
  4.   
  5.         CancelTask() throws SQLException {  
  6.             connectionId = connection.getIO().getThreadId();  
  7.         }  
  8.   
  9.         public void run() {  
  10.   
  11.             Thread cancelThread = new Thread() {  
  12.   
  13.                 public void run() {  
  14.                     Connection cancelConn = null;  
  15.                     java.sql.Statement cancelStmt = null;  
  16.   
  17.                     try {  
  18.                         cancelConn = connection.duplicate();  
  19.                         cancelStmt = cancelConn.createStatement();  
  20.                                                 // 簡單暴力,再發起一條KILL SQL,關閉先前的sql thread id  
  21.                         cancelStmt.execute("KILL QUERY " + connectionId);  
  22.                         wasCancelled = true;  
  23.                     } catch (SQLException sqlEx) {  
  24.                         throw new RuntimeException(sqlEx.toString());  
  25.                     } finally {  
  26.                         if (cancelStmt != null) {  
  27.                             try {  
  28.                                 cancelStmt.close();  
  29.                             } catch (SQLException sqlEx) {  
  30.                                 throw new RuntimeException(sqlEx.toString());  
  31.                             }  
  32.                         }  
  33.   
  34.                         if (cancelConn != null) {  
  35.                             try {  
  36.                                 cancelConn.close();  
  37.                             } catch (SQLException sqlEx) {  
  38.                                 throw new RuntimeException(sqlEx.toString());  
  39.                             }  
  40.                         }  
  41.                     }  
  42.                 }  
  43.             };  
  44.   
  45.             cancelThread.start();  
  46.         }  
  47.     }  

 

 

原因總結一句話: queryTimeout的實現是通過底層數據庫提供的機制,比如KILL QUERY pid.  如果此時的網絡不通,出現阻塞現象,對應的kill命令也發不出去,所以timeout設置的超時沒效果。

4.最後

最後還是決定配置testWhileIdle掃描,主要考慮:

  1. pool池中的鏈接如果未被使用,可以通過testWhileIdle進行鏈接檢查,避免在使用時後總要失敗那麼一次,可以及時預防
  2. 配合連接池的minEvictableIdleTimeMillis(空閒鏈接),removeAbandoned(未釋放的鏈接),可以更好的去避免因爲一些異常情況引起的問題,防範於未然。比如使用一些分佈式數據庫的中間件,會有空閒鏈接關閉的動作,動態伸縮連接池,這時候需要能及時的發現,避免請求失敗。
  3. testOnBorrow個人不太建議使用,存在性能問題,試想一下連接一般會在什麼情況出問題,網絡或者服務端異常終端空閒鏈接,網絡中斷你testOnBorrow檢查發現不對再取一個鏈接還是不對,針對空閒鏈接處理異常關閉,可以從好業務端的重試策略進行考慮,同時配置客戶端的空閒鏈接超時時間,maxIdle,minIdle等。

 

--------------------------------------------

新加的內容:

5.dbcp密碼加密處理

以前使用jboss的jndi數據源的方式,是通過配置oracle-ds.xml,可以設置<security-domain>EncryptDBPassword</security-domain>,引用jboss login-config.xml配置的加密配置。

 

 

Java代碼  收藏代碼
  1. <application-policy name="EncryptDBPassword">  
  2.         <authentication>  
  3.             <login-module code="org.jboss.resource.security.SecureIdentityLoginModule" flag="required">  
  4.                 <module-option name="username">${username}</module-option>  
  5.                 <module-option name="password">${password_encrypt}</module-option>  
  6.                 <module-option name="managedConnectionFactoryName">jboss.jca:service=LocalTxCM,name=${jndiName}</module-option>  
  7.             </login-module>  
  8.         </authentication>  
  9.     </application-policy>  
 

 

爲了能達到同樣的效果,切換爲spring dbcp配置時,也有類似密碼加密的功能,運行期進行密碼decode,最後進行數據鏈接。

 

 

實現方式很簡單,分析jboss的對應SecureIdentityLoginModule的實現,無非就是走了Blowfish加密算法,自己拷貝實現一份。

 

 

Java代碼  收藏代碼
  1. private static String encode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,  
  2.                                                InvalidKeyException, BadPaddingException, IllegalBlockSizeException {  
  3.         byte[] kbytes = "jaas is the way".getBytes();  
  4.         SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");  
  5.   
  6.         Cipher cipher = Cipher.getInstance("Blowfish");  
  7.         cipher.init(Cipher.ENCRYPT_MODE, key);  
  8.         byte[] encoding = cipher.doFinal(secret.getBytes());  
  9.         BigInteger n = new BigInteger(encoding);  
  10.         return n.toString(16);  
  11.     }  
  12.   
  13.     private static char[] decode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,  
  14.                                                InvalidKeyException, BadPaddingException, IllegalBlockSizeException {  
  15.         byte[] kbytes = "jaas is the way".getBytes();  
  16.         SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");  
  17.   
  18.         BigInteger n = new BigInteger(secret, 16);  
  19.         byte[] encoding = n.toByteArray();  
  20.   
  21.         Cipher cipher = Cipher.getInstance("Blowfish");  
  22.         cipher.init(Cipher.DECRYPT_MODE, key);  
  23.         byte[] decode = cipher.doFinal(encoding);  
  24.         return new String(decode).toCharArray();  
  25.     }  
 

最後的配置替換爲:

 

 

Xml代碼  收藏代碼
  1. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">   
  2. ......  
  3.         <property name="password"><!-- 注意多了一層轉化,將密碼串調用decode解密爲最初的數據庫密碼 -->  
  4.             <bean class="com.xxxxx.EncryptDBPasswordFactory">  
  5.                 <property name="password" value="${xxxx.password.encrypted}" />  
  6.             </bean>  
  7.         </property>  
  8. ........  
  9. </bean>  

 

--------------------------------------------

新加的內容:

6.數據庫重連機制

常見的問題:

1. 數據庫意外重啓後,原先的數據庫連接池能自動廢棄老的無用的鏈接,建立新的數據庫鏈接

2. 網絡異常中斷後,原先的建立的tcp鏈接,應該能進行自動切換。比如網站演習中的交換機重啓會導致網絡瞬斷

3. 分佈式數據庫中間件,比如amoeba會定時的將空閒鏈接異常關閉,客戶端會出現半開的空閒鏈接。

 

大致的解決思路:  

1. sql心跳檢查

  主動式 ,即我前面提到的sql validate相關配置

2. 請求探雷

    犧牲小我,完成大我的精神。 拿鏈接嘗試一下,發現處理失敗丟棄鏈接,探雷的請求總會失敗幾個,就是前面遇到的問題一,dbcp已經支持該功能,不需要額外置。

3. 設置合理的超時時間,

      解決半開鏈接. 一般數據庫mysql,oracle都有一定的鏈接空閒斷開的機制,而且當你使用一些分佈式中間件(軟件一類的),空閒鏈接控制會更加嚴格,這時候設置合理的超時時間可以有效避免半開鏈接。

     一般超時時間,dbcp主要是minEvictableIdleTimeMillis(空閒鏈接) , removeAbandonedTimeout(鏈接泄漏)。可以見前面的參數解釋。

 

發佈了133 篇原創文章 · 獲贊 228 · 訪問量 430萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章