最近在看一些dbcp的相關內容,順便作一下記錄,省得本身給忘記了。javascript
1. 引入dbcp (選擇1.4)html
Java代碼 java
- <dependency>
- <groupId>com.alibaba.external</groupId>
- <artifactId>jakarta.commons.dbcp</artifactId>
- <version>1.4</version>
- </dependency>
2. dbcp的基本配置mysql
相關配置說明:算法
- initialSize :鏈接池啓動時建立的初始化鏈接數量(默認值爲0)
- maxActive :鏈接池中可同時鏈接的最大的鏈接數(默認值爲8,調整爲20,高峯單機器在20併發左右,本身根據應用場景定)
- maxIdle:鏈接池中最大的空閒的鏈接數,超過的空閒鏈接將被釋放,若是設置爲負數表示不限制(默認爲8個,maxIdle不能設置過小,由於假如在高負載的狀況下,鏈接的打開時間比關閉的時間快,會引發鏈接池中idle的個數 上升超過maxIdle,而形成頻繁的鏈接銷燬和建立,相似於jvm參數中的Xmx設置)
- minIdle:鏈接池中最小的空閒的鏈接數,低於這個數量會被建立新的鏈接(默認爲0,調整爲5,該參數越接近maxIdle,性能越好,由於鏈接的建立和銷燬,都是須要消耗資源的;可是不能太大,由於在機器很空閒的時候,也會建立低於minidle個數的鏈接,相似於jvm參數中的Xmn設置)
- maxWait :最大等待時間,當沒有可用鏈接時,鏈接池等待鏈接釋放的最大時間,超過該時間限制會拋出異常,若是設置-1表示無限等待(默認爲無限,調整爲60000ms,避免因線程池不夠用,而致使請求被無限制掛起)
- poolPreparedStatements:開啓池的prepared(默認是false,未調整,通過測試,開啓後的性能沒有關閉的好。)
- maxOpenPreparedStatements:開啓池的prepared 後的同時最大鏈接數(默認無限制,同上,未配置)
- minEvictableIdleTimeMillis :鏈接池中鏈接,在時間段內一直空閒, 被逐出鏈接池的時間
- (默認爲30分鐘,能夠適當作調整,須要和後端服務端的策略配置相關)
- removeAbandonedTimeout :超過期間限制,回收沒有用(廢棄)的鏈接(默認爲 300秒,調整爲180)
- removeAbandoned :超過removeAbandonedTimeout時間後,是否進 行沒用鏈接(廢棄)的回收(默認爲false,調整爲true)
removeAbandoned參數解釋:spring
- 若是開啓了removeAbandoned,當getNumIdle() < 2) and (getNumActive() > getMaxActive() - 3)時被觸發.
- 舉例當maxActive=20, 活動鏈接爲18,空閒鏈接爲1時能夠觸發"removeAbandoned".可是活動鏈接只有在沒有被使用的時間超 過"removeAbandonedTimeout"時才被回收
- logAbandoned: 標記當鏈接被回收時是否打印程序的stack traces日誌(默認爲false,未調整)
通常會是幾種狀況出現須要removeAbandoned: sql
- 代碼未在finally釋放connection , 不過咱們都用sqlmapClientTemplate,底層都有連接釋放的過程
- 遇到數據庫死鎖。之前遇到事後端存儲過程作了鎖表操做,致使前臺集羣中鏈接池全都被block住,後續的業務處理由於拿不到連接全部都處理失敗了。
一份優化過的配置:數據庫
基本配置代碼 apache
- <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="com.mysql.jdbc.Driver" />
- <property name="url" value="xxxx" />
- <property name="username"><value>xxxx</value></property>
- <property name="password"><value>xxxxx</value></property>
- <property name="maxActive"><value>20</value></property>
- <property name="initialSize"><value>1</value></property>
- <property name="maxWait"><value>60000</value></property>
- <property name="maxIdle"><value>20</value></property>
- <property name="minIdle"><value>3</value></property>
- <property name="removeAbandoned"><value>true</value></property>
- <property name="removeAbandonedTimeout"><value>180</value></property>
- <property name="connectionProperties"><value>clientEncoding=GBK</value></property>
- </bean>
2. dbcp的連接validate配置後端
- dbcp是採用了commons-pool作爲其鏈接池管理,testOnBorrow,testOnReturn, testWhileIdle是pool是提供的幾種校驗機制,經過外部鉤子的方式回調dbcp的相關數據庫連接(validationQuery)校驗
- dbcp相關外部鉤子類:PoolableConnectionFactory,繼承於common-pool PoolableObjectFactory
- dbcp經過GenericObjectPool這一入口,進行鏈接池的borrow,return處理
- testOnBorrow : 顧明思義,就是在進行borrowObject進行處理時,對拿到的connection進行validateObject校驗
- testOnReturn : 顧明思義,就是在進行returnObject對返回的connection進行validateObject校驗,我的以爲對數據庫鏈接池的管理意義不大
- testWhileIdle : 關注的重點,GenericObjectPool中針對pool管理,起了一個Evict的TimerTask定時線程進行控制(可經過設置參數timeBetweenEvictionRunsMillis>0),定時對線程池中的連接進行validateObject校驗,對無效的連接進行關閉後,會調用ensureMinIdle,適當創建連接保證最小的minIdle鏈接數。
- timeBetweenEvictionRunsMillis,設置的Evict線程的時間,單位ms,大於0纔會開啓evict檢查線程
- validateQuery, 表明檢查的sql
- validateQueryTimeout, 表明在執行檢查時,經過statement設置,statement.setQueryTimeout(validationQueryTimeout)
- numTestsPerEvictionRun,表明每次檢查連接的數量,建議設置和maxActive同樣大,這樣每次能夠有效檢查全部的連接.
Validate配置代碼
- <property name="testWhileIdle"><value>true</value></property> <!-- 打開檢查,用異步線程evict進行檢查 -->
- <property name="testOnBorrow"><value>false</value></property>
- <property name="testOnReturn"><value>false</value></property>
- <property name="validationQuery"><value>select sysdate from dual</value></property>
- <property name="validationQueryTimeout"><value>1</value></property>
- <property name="timeBetweenEvictionRunsMillis"><value>30000</value></property>
- <property name="numTestsPerEvictionRun"><value>20</value></property>
相關配置需求:
- 目前網站的應用大部分的瓶頸仍是在I/O這一塊,大部分的I/O仍是在數據庫的這一層面上,每個請求可能會調用10來次SQL查詢,若是不走事務,一個請求會重複獲取連接,若是每次獲取連接都進行validateObject,性能開銷不是很能接受,能夠假定一次SQL操做消毫0.5~1ms(通常走了網絡請求基本就這數)
- 網站異常數據庫重啓,網絡異常斷開的頻率是很是低的,通常也就在數據庫升級,演習維護時纔會進行,並且通常也是選在晚上,訪問量相對比較低的請求,並且通常會有人員值班關注,因此異步的validateObject是能夠接受,但一個前提須要確保能保證在一個合理的時間段內,數據庫能完成自動重聯。
從代碼層面簡單介紹下dbcp的validate實現:
1. common-pools提供的PoolableObjectFactory,針對pool池的管理操做接口
Java代碼
- public interface PoolableObjectFactory {
-
- Object makeObject() throws Exception;
-
- void destroyObject(Object obj) throws Exception;
-
- boolean validateObject(Object obj);
-
- void activateObject(Object obj) throws Exception;
-
- void passivateObject(Object obj) throws Exception;
- }
2. dbcp實現的pool從池管理操做
這裏貼了一個相關validate代碼,具體類可見:PoolableConnectionFactory.validateConnection()
Java代碼
- public class PoolableConnectionFactory implements PoolableObjectFactory {
-
- ......
- public boolean validateObject(Object obj) { //驗證validateObject
- if(obj instanceof Connection) {
- try {
- validateConnection((Connection) obj);
- return true;
- } catch(Exception e) {
- return false;
- }
- } else {
- return false;
- }
- }
- public void validateConnection(Connection conn) throws SQLException {
- String query = _validationQuery;
- if(conn.isClosed()) {
- throw new SQLException("validateConnection: connection closed");
- }
- if(null != query) {
- Statement stmt = null;
- ResultSet rset = null;
- try {
- stmt = conn.createStatement();
- if (_validationQueryTimeout > 0) {
- stmt.setQueryTimeout(_validationQueryTimeout);
- }
- rset = stmt.executeQuery(query);
- if(!rset.next()) {
- throw new SQLException("validationQuery didn't return a row");
- }
- } finally {
- if (rset != null) {
- try {
- rset.close();
- } catch(Exception t) {
- // ignored
- }
- }
- if (stmt != null) {
- try {
- stmt.close();
- } catch(Exception t) {
- // ignored
- }
- }
- }
- }
- }
-
- ....
-
- }
3. pool池的evict調用代碼:GenericObjectPool (apache commons pool version 1.5.4)
Java代碼
- protected synchronized void startEvictor(long delay) { //啓動Evictor爲TimerTask
- if(null != _evictor) {
- EvictionTimer.cancel(_evictor);
- _evictor = null;
- }
- if(delay > 0) {
- _evictor = new Evictor();
- EvictionTimer.schedule(_evictor, delay, delay);
- }
- }
-
- for (int i=0,m=getNumTests();i<m;i++) {
- final ObjectTimestampPair pair;
- .......
- boolean removeObject = false;
- // 空閒連接處理
- final long idleTimeMilis = System.currentTimeMillis() - pair.tstamp;
- if ((getMinEvictableIdleTimeMillis() > 0) &&
- (idleTimeMilis > getMinEvictableIdleTimeMillis())) {
- removeObject = true;
- } else if ((getSoftMinEvictableIdleTimeMillis() > 0) &&
- (idleTimeMilis > getSoftMinEvictableIdleTimeMillis()) &&
- ((getNumIdle() + 1)> getMinIdle())) {
- removeObject = true;
- }
- // testWhileIdle sql 檢查處理
- if(getTestWhileIdle() && !removeObject) {
- boolean active = false;
- try {
- _factory.activateObject(pair.value);
- active = true;
- } catch(Exception e) {
- removeObject=true;
- }
- if(active) {
- if(!_factory.validateObject(pair.value)) {
- removeObject=true;
- } else {
- try {
- _factory.passivateObject(pair.value);
- } catch(Exception e) {
- removeObject=true;
- }
- }
- }
- }
- // 真正關閉
- if (removeObject) {
- try {
- _factory.destroyObject(pair.value);
- } catch(Exception e) {
- // ignored
- }
- }
- ........
注意: 目前dbcp的pool的實現是使用了公用的apache common pools進行擴展處理,因此和原生的鏈接池處理,代碼看上去有點彆扭,感受自動重連這塊異常處理不怎麼好,我也就只重點關注了這部分代碼而已 .
3. dbcp的連接自動重鏈相關測試
相關場景:
- 數據庫意外重啓後,原先的數據庫鏈接池能自動廢棄老的無用的連接,創建新的數據庫連接
- 網絡異常中斷後,原先的創建的tcp連接,應該能進行自動切換
測試需求1步驟
- 創建一testCase代碼
- 配置mysql數據庫
- 循環執行在SQL查詢過程
- 異常重啓mysql數據庫
測試需求2步驟
- 創建一testCase代碼
- 配置mysql數據庫
- 循環執行在SQL查詢過程
- 經過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查詢正常
跟蹤了一下代碼,發現這麼一個問題:
- 在數據庫關閉的時候,client中pool經過borrowObject獲取一個異常連接返回給client
- client在使用具體的異常連接進行sql調用出錯了,拋了異常
- 在finally,調用connection.close(),本意是應該調用pool經過returnObject返回到的池中,但在跟蹤代碼時,未見調用GenericObjectPool的returnObject
- 繼續查,發如今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代碼
- timeoutTask = new CancelTask();
- //經過TimerTask啓動必定時任務
- Connection.getCancelTimer().schedule(timeoutTask, this.timeoutInMillis);
對應的CancelTask的代碼:
Java代碼
- class CancelTask extends TimerTask {
-
- long connectionId = 0;
-
- CancelTask() throws SQLException {
- connectionId = connection.getIO().getThreadId();
- }
-
- public void run() {
-
- Thread cancelThread = new Thread() {
-
- public void run() {
- Connection cancelConn = null;
- java.sql.Statement cancelStmt = null;
-
- try {
- cancelConn = connection.duplicate();
- cancelStmt = cancelConn.createStatement();
- // 簡單暴力,再發起一條KILL SQL,關閉先前的sql thread id
- cancelStmt.execute("KILL QUERY " + connectionId);
- wasCancelled = true;
- } catch (SQLException sqlEx) {
- throw new RuntimeException(sqlEx.toString());
- } finally {
- if (cancelStmt != null) {
- try {
- cancelStmt.close();
- } catch (SQLException sqlEx) {
- throw new RuntimeException(sqlEx.toString());
- }
- }
-
- if (cancelConn != null) {
- try {
- cancelConn.close();
- } catch (SQLException sqlEx) {
- throw new RuntimeException(sqlEx.toString());
- }
- }
- }
- }
- };
-
- cancelThread.start();
- }
- }
緣由總結一句話: queryTimeout的實現是經過底層數據庫提供的機制,好比KILL QUERY pid. 若是此時的網絡不通,出現阻塞現象,對應的kill命令也發不出去,因此timeout設置的超時沒效果。
4.最後
最後仍是決定配置testWhileIdle掃描,主要考慮:
- pool池中的連接若是未被使用,能夠經過testWhileIdle進行連接檢查,避免在使用時後總要失敗那麼一次,能夠及時預防
- 配合鏈接池的minEvictableIdleTimeMillis(空閒連接),removeAbandoned(未釋放的連接),能夠更好的去避免由於一些異常狀況引發的問題,防範於未然。好比使用一些分佈式數據庫的中間件,會有空閒連接關閉的動做,動態伸縮鏈接池,這時候須要能及時的發現,避免請求失敗。
- testOnBorrow我的不太建議使用,存在性能問題,試想一下鏈接通常會在什麼狀況出問題,網絡或者服務端異常終端空閒連接,網絡中斷你testOnBorrow檢查發現不對再取一個連接仍是不對,針對空閒連接處理異常關閉,能夠從好業務端的重試策略進行考慮,同時配置客戶端的空閒連接超時時間,maxIdle,minIdle等。
--------------------------------------------
新加的內容:
5.dbcp密碼加密處理
之前使用jboss的jndi數據源的方式,是經過配置oracle-ds.xml,能夠設置<security-domain>EncryptDBPassword</security-domain>,引用jboss login-config.xml配置的加密配置。
Java代碼
- <application-policy name="EncryptDBPassword">
- <authentication>
- <login-module code="org.jboss.resource.security.SecureIdentityLoginModule" flag="required">
- <module-option name="username">${username}</module-option>
- <module-option name="password">${password_encrypt}</module-option>
- <module-option name="managedConnectionFactoryName">jboss.jca:service=LocalTxCM,name=${jndiName}</module-option>
- </login-module>
- </authentication>
- </application-policy>
爲了能達到一樣的效果,切換爲spring dbcp配置時,也有相似密碼加密的功能,運行期進行密碼decode,最後進行數據連接。
實現方式很簡單,分析jboss的對應SecureIdentityLoginModule的實現,無非就是走了Blowfish加密算法,本身拷貝實現一份。
Java代碼
- private static String encode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
- byte[] kbytes = "jaas is the way".getBytes();
- SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");
-
- Cipher cipher = Cipher.getInstance("Blowfish");
- cipher.init(Cipher.ENCRYPT_MODE, key);
- byte[] encoding = cipher.doFinal(secret.getBytes());
- BigInteger n = new BigInteger(encoding);
- return n.toString(16);
- }
-
- private static char[] decode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
- byte[] kbytes = "jaas is the way".getBytes();
- SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");
-
- BigInteger n = new BigInteger(secret, 16);
- byte[] encoding = n.toByteArray();
-
- Cipher cipher = Cipher.getInstance("Blowfish");
- cipher.init(Cipher.DECRYPT_MODE, key);
- byte[] decode = cipher.doFinal(encoding);
- return new String(decode).toCharArray();
- }
最後的配置替換爲:
Xml代碼
- <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- ......
- <property name="password"><!-- 注意多了一層轉化,將密碼串調用decode解密爲最初的數據庫密碼 -->
- <bean class="com.xxxxx.EncryptDBPasswordFactory">
- <property name="password" value="${xxxx.password.encrypted}" />
- </bean>
- </property>
- ........
- </bean>
--------------------------------------------
新加的內容:
6.數據庫重連機制
常見的問題:
1. 數據庫意外重啓後,原先的數據庫鏈接池能自動廢棄老的無用的連接,創建新的數據庫連接
2. 網絡異常中斷後,原先的創建的tcp連接,應該能進行自動切換。好比網站演習中的交換機重啓會致使網絡瞬斷
3. 分佈式數據庫中間件,好比amoeba會定時的將空閒連接異常關閉,客戶端會出現半開的空閒連接。
大體的解決思路:
1. sql心跳檢查
主動式 ,即我前面提到的sql validate相關配置
2. 請求探雷
犧牲小我,完成大個人精神。 拿連接嘗試一下,發現處理失敗丟棄連接,探雷的請求總會失敗幾個,就是前面遇到的問題一,dbcp已經支持該功能,不須要額外置。
3. 設置合理的超時時間,
解決半開連接. 通常數據庫mysql,oracle都有必定的連接空閒斷開的機制,並且當你使用一些分佈式中間件(軟件一類的),空閒連接控制會更加嚴格,這時候設置合理的超時時間能夠有效避免半開連接。
通常超時時間,dbcp主要是minEvictableIdleTimeMillis(空閒連接) , removeAbandonedTimeout(連接泄漏)。能夠見前面的參數解釋。
參考文獻:http://agapple.iteye.com/blog/772507