深刻理解 JDBC 的超時

這是最近讀到的講關於 JDBC 的超時問題最透徹的文章,原文是http://www.cubrid.org/blog/understanding-jdbc-internals-and-timeout-configuration ,網上現有的翻譯感受磕磕絆絆的,不少上下文信息丟失了,這裏用個人理解從新翻譯一下。java

應用程序中配置恰當的 JDBC 超時時間能減小服務失敗的時間,這篇文章咱們將討論不一樣種類的超時和推薦的配置。mysql

Web 應用服務器在 DDoS 攻擊後變得無響應

(這是一個真實案例的發生過程複述)sql

在 DDoS 攻擊以後,整個服務都不能正常工做了,由於第四層交換機不能工做,網絡鏈接斷開了,這也致使 WAS (能夠將 WAS 理解爲做者公司的應用程序)不能正常工做。攻擊發生後不久,安全團隊攔截了全部 DDoS 攻擊,而後網絡恢復正常,但 WAS 仍是不能工做。數據庫

經過分析系統的 dump 日誌發現,業務系統停在了 JDBC API 的調用上。20分鐘後系統仍處於等待狀態沒法響應,大概過了30分鐘,系統忽然發生異常,而後服務恢復正常。安全

爲何已經將查詢超時時間設置成3秒, WAS 卻等待了30分鐘?爲何30分鐘後 WAS 又開始工做了?bash

若是理解了 JDBC 的超時機制就能找到答案。服務器

爲何咱們須要知道 JDBC 驅動

當有性能問題或系統級錯誤時,WAS 和數據庫是咱們關注的兩個重要層面。在我公司 WAS 和數據庫一般由不一樣的部門負責,所以每一個部門聚焦在各自負責的領域來設法弄清楚情況。此時 WAS 和數據庫之間的部分會由於得不到足夠的關注而產生盲區。對於 Java 應用,這個盲區在數據庫鏈接池和 JDBC 之間,本文咱們將重點討論 JDBC。網絡

什麼是 JDBC 驅動

JDBC 是 Java 應用程序中用於訪問數據庫的一套標準 API,Sun 公司定義了4種類型的 JDBC 驅動。我公司主要用的是第4種,該類型驅動由純 Java 語言編寫,在 Java 應用中經過 socket 與數據庫通訊。oracle

圖1: 類型4驅動

類型4驅動是經過 socket 來處理字節流的,它的基本操做和 HttpClient 這種網絡操做類庫相同。同其餘網絡類庫同樣,也會在發生超時的時候佔用大量的 CPU 資源從而失去響應。若是你以前用過 HttpClient ,確定遇到過由於沒有設置超時致使的錯誤。若是 socket 超時設置不合適,類型4驅動也可能有一樣的錯誤(鏈接被阻塞)。框架

下面讓咱們瞭解如何配置 JDBC 驅動的 socket 超時,以及設置時需考慮哪些問題。

WAS 與數據庫間的設置超時的層次

圖2: 超時的層次

圖2展現了簡化的 WAS 和數據庫通訊時的超時層次。

更上層的超時依賴於下層的超時,只有當較低層的超時機制正常工做,上層的超時纔會正常。若是 JDBC 驅動程序的 socket 超時工做不正常,那麼更上層的超時好比 Statement 超時和事務超時都不會正常工做。

咱們收到不少評論說:

即便配置了 Statement 超時,應用程序仍是不能從故障中恢復,由於 Statement 超時在網絡故障時不起做用。

Statement 超時在網絡故障時不起做用。它只能作到:限制一次Statement 執行的時間,處理超時以防網絡故障必須由 JDBC 驅動來作。

JDBC 驅動的 socket 超時還會受操做系統的 socket 超時配置的影響。這解釋了爲何案例中的 JDBC 鏈接在網絡故障後阻塞了30分鐘才恢復,即便沒配置 JDBC 驅動的 socket 超時。

DBCP 鏈接池位於圖2的左邊。你會發現各類層面的超時與 DBCP 是分開的。DBCP 負責數據庫鏈接(即本文中說到的Connection)的建立和管理,並不涉及超時的處理。當在 DBCP 中建立了一個數據庫鏈接或發送了一條查詢校驗的 sql 語句用於檢查鏈接有效性時,socket 超時會影響這些過程的處理,但並不直接影響應用程序。

然而在應用程序中調用 DBCP 的 getConnection() 方法時,你能指定應用程序獲取數據庫鏈接的超時時間,但這和 JDBC 的鏈接超時無關。

圖3: 每一層級的超時

什麼是事務超時

事務超時是在框架(Spring、EJB容器)或應用程序層面上纔有效的超時。

事務超時多是個不常見的概念。簡單講,事務超時等於** Statement 超時 * N(須要執行的 Statement 的數量) + 其它(垃圾回收等其餘時間)**。事務超時被用來限制執行一個事務以內全部 Statement 執行的總時長。

好比,假設執行一次 Statement 執行需0.1秒,那執行幾回 Statement 並非什麼問題,但若是是執行十萬次則須要一萬秒(大約7個小時),這就能夠用上事務超時了。

EJB 的聲明式事務管理 (容器管理事務) 就是一種典型的使用場景,但聲明式事務管理只是定義了相應的規範,容器內事務的處理過程和具體實現由容器的開發者負責。咱們公司並無用 EJB,用的是最多見的 Spring 框架,因此事務超時的配置也由 Spring 來管理。在 Spring 中,事務超時能夠在 XML 文件顯式配置或在 Java 代碼中用 Transactional 註解來配置。

<tx:attributes>
        <tx:method name="…" timeout="3"/>
</tx:attributes>
複製代碼

Spring 提供的事務超時的配置很是簡單,它會記錄每一個事務的開始時間和消耗時間,當特定的事件發生時會對已消耗掉的時間作校驗,若是超出了配置將拋出異常。

Spring 中數據庫鏈接被保存在線程本地變量(ThreadLocal)中,這被稱做事務同步(Transaction Synchronization)。當數據庫鏈接被保存到 ThreadLocal 時,同時會記錄事務的開始時間和超時時間。因此經過數據庫鏈接的代理建立的 Statement 在執行時就會校驗這個時間。

EJB 的聲明式事務管理的實現也是相似,實現的思路很是簡單。若是事務超時很是重要,但你所使用的容器或框架不提供此功能,你也能夠選擇本身實現,關於事務超時並無制定標準的 API。

Lucy 框架的1.5和1.6版不支持事務超時,但你能夠經過 Spring 的事務管理達到相同的效果。

假設一個事務裏有5條 Statement ,每條 Statement 執行時間是200毫秒,其它業務邏輯或框架操做的執行時間是100毫秒,那事務容許的超時時間至少應該1100毫秒(200 * 5 + 100)。

什麼是 Statement 超時

Statement 超時是用來限制 Statement 的執行時間的,它的具體值是經過 JDBC API 來設置的。JDBC 驅動程序基於這個值進行 Statement 執行時的超時處理。Statement 超時是經過 JDBC API 中java.sql.Statement 類的 setQueryTimeout(int timeout) 方法配置的。不過如今的開發者已經不多直接在代碼中配置它了,更可能是經過框架來進行設置。

以 iBatis 爲例,能夠經過 SqlMapConfig.xml 中的 setting 屬性defaultStatementTimeout 來設置全局的 statement 超時缺省值。你也能夠經過在具體的 sql 映射文件中的 select insert update 標籤的 statement 屬性來覆蓋。

當你用 Lucy 1.5或1.6版時,能夠經過設置 queryTimeout 屬性在數據源層面設置 Statement 超時。

Statement 超時的具體數值須要根據每一個應用自身的狀況而定,並無推薦的配置。

JDBC 驅動中的 Statement 超時處理過程

每一個數據庫和驅動程序的 Statement 超時的處理也是不一樣的。Oracle 和 SQLServer 的工做方式比較像,MySQL 和 CUBRID 比較像。

Oracle 中的 Statement 超時處理

  1. 調用 Connection 的 createStatement() 方法建立一個 Statement 對象
  2. 調用 Statement 的 executeQuery() 方法
  3. Statement 經過內部綁定的 Connection 對象將查詢命令發送到 Oracle 數據庫
  4. Statement 向 Oracle 的超時處理線程 OracleTimeoutPollingThread(每一個類加載器一個該線程)註冊一個 Statement 用於處理超時
  5. 發生超時
  6. Oracle 的 OracleTimeoutPollingThread 調用 OracleStatement 的 cancel() 方法
  7. 經過 Statement 的 Connection 發送一條消息取消還在執行的查詢

圖4 Oracle 的 Statement 超時執行過程

JTDS (MS SQLServer) 中的 Statement 超時處理

1.調用 Connection 的 createStatement() 方法建立一個 Statement 對象 2. 調用 Statement 的 executeQuery() 方法 3. Statement 經過內部的 Connection 將查詢命令發送到 MS SqlServer 數據庫 4. Statement 向 MS SQLServer 的 TimerThread 線程註冊一個 Statement 用於處理超時 5. 發生超時 6. TimerThread 調用 JtdsStatement 內部的 TsdCore.cancel()方法 7. 經過 ConnectionJDBC 發送一條消息取消還在執行的查詢

圖5 MS SQLServer 的 Statement 超時執行過程

MySQL (5.0.8) 中的 Statement 超時處理

  1. 調用 Connection 的 createStatement() 方法建立一個 Statement 對象
  2. 調用 Statement 的 executeQuery() 方法
  3. Statement 經過內部的 Connection 將查詢命令傳輸到 MySqlServer 數據庫
  4. Statement 建立一個新的超時執行線程(timeout-execution)來處理超時
  5. 5.1以上版本改成每一個鏈接分配一個線程
  6. 向 timeout-execution 線程註冊當前的 Statement
  7. 發生超時
  8. timeout-execution 線程建立一個相同配置的 Connection
  9. 用新建立的 Connection 發送取消查詢的命令

圖6 MySQL 的 Statement 超時執行過程

CUBRID中的 Statement 超時處理

  1. 調用 Connection 的 createStatement() 方法建立一個 Statement 對象
  2. 調用 Statement 的 executeQuery() 方法
  3. Statement 經過內部的 Connection 將查詢命令發送到 CUBRID 數據庫
  4. Statement 建立一個新的超時執行線程(timeout-execution)來處理超時
  5. 向 timeout-execution 線程註冊當前的 Statement
  6. 發生超時
  7. timeout-execution 線程建立一個相同配置的Connection
  8. 用新建立的 Connection 發送取消查詢的命令

圖7 CUBRID 的 Statement 超時執行過程

什麼是 Socket 超時

類型4的 JDBC 驅動是用 Socket 方式與數據庫鏈接的,應用程序和數據庫之間的鏈接超時並非由數據庫處理的。

當數據庫忽然宕掉或發生網絡錯誤(設備故障等)時,JDBC 驅動的 Socket 超時的值是必須的。因爲 TCP/IP 的結構,Socket 沒有辦法檢測到網絡錯誤,所以應用不能檢測到與數據庫到鏈接斷開了。若是沒有設置 Socket 超時,應用程序會一直等待數據庫返回結果。(這個鏈接也被叫作「死鏈接」) 爲了不死鏈接,Socket 必需要設置超時時間。Socket 超時能夠經過 JDBC 驅動程序配置。經過設置 Socket 超時,能夠防止出現網絡錯誤時一直等待的狀況並縮短故障時間。

不推薦使用 Socket 超時來限制一個 Statement 的執行時間,所以Socket 超時的值必需要高於 Statement 的超時時間,不然 Socket 超時將會先生效,這樣 Statement 超時就沒有意義,也沒法生效。

下面展現了 Socket 超時設置的連個選項,其配置因不一樣的驅動而異。

  • Socket 鏈接時的超時:經過 Socket 對象的 connect(SocketAddress endpoint, int timeout) 方法來配置
  • Socket 讀寫時的超時:經過 Socket 對象的 setSoTimeout(int timeout) 方法來配置

經過查看CUBRID,MySQL,MS SQL Server (JTDS) 和 Oracle 的JDBC 驅動源碼,咱們確認以上全部驅動都是使用上面的2個 API 來設置socket 超時的。

下面列出瞭如何配置 Socket 超時

JDBC 驅動 鏈接超時配置 Socket 超時配置 JDBC Url 格式 示例
MySQL connectTimeout(默認值:0,單位:毫秒) socketTimeout(默認值:0,單位:ms) jdbc:mysql://[host:port],[host:port].../[database]
[?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]...
jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000
MS-SQL , jTDS loginTimeout(默認值:0,單位:秒) socketTimeout(默認值:0,單位:s) jdbc:jtds:<server_type>://[:][/][;=[;...]] jdbc:jtds:sqlserver://server:port/database;loginTimeout=60;socketTimeout=60
Oracle oracle.net.CONNECT_TIMEOUT (默認值:0,單位:毫秒) oracle.jdbc.ReadTimeout(默認值:0,單位:毫秒) 不支持經過url配置,只能經過OracleDatasource.setConnectionProperties() API設置,使用DBCP時能夠調用BasicDatasource.setConnectionProperties()或BasicDatasource.addConnectionProperties()進行設置 -
CUBRID 無單獨配置項(默認值:5,000,單位:毫秒) 無單獨配置項(默認值:5,000,單位:毫秒) - -
  • connectTimeout 和 socketTimeout 的默認值是 0 ,這意味着不會發生超時。
  • 你也能夠經過屬性進行配置,而無需直接使用 DBCP 的 API 。

經過屬性進行配置時,須要傳入的 key 爲 "connectionProperties",其 value 的格式爲" [propertyName=property;]*"。下面是 iBatis 中經過 xml 文件配置屬性的例子。

<transactionManager type="JDBC">
  <dataSource type="com.nhncorp.lucy.db.DbcpDSFactory">
     ....
     <property name="connectionProperties" value="oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=6000"/> 
  </dataSource>
</transactionManager>
複製代碼

操做系統層面的 Socket 超時配置

若是沒設置 Socket 超時或鏈接超時,應用程序多數狀況下沒法檢測到網絡錯誤。此時,應用程序將一直等待下去,直到鏈接上數據庫或能讀取到數據。然而,若是查看實際服務遇到的實際狀況會發現問題經常在在應用程序(WAS)在30分鐘後嘗試從新鏈接到網絡後被解決了。這是由於操做系統也配置了 Socket 超時時間。我公司使用的 Linux 服務器將 Socket 超時時間設置爲30分鐘。它將在操做系統層面對網絡鏈接作校驗。由於公司的 Linux 服務器的 KeepAlive 檢查週期爲30分鐘,所以即便應用程序裏將 Socket 超時設置爲0,由網絡緣由引發的數據庫網絡鏈接問題也不會超過30分鐘。

一般,應用程序會在調用 Socket 的 read() 方法時因爲網絡問題而阻塞住。然而不多在調用 Socket 的 write() 方法時處於等待狀態,這取決於網絡構成和錯誤類型。當應用程序調用 Socket 的 write() 方法時,數據被記錄到操做系統的內核緩衝區,而後將控制權當即交還給應用程序。所以,一旦數據已經寫入內核緩衝區,write() 的調用始終是成功。可是,若是操做系統內核緩衝區因爲特殊的網絡錯誤而滿了的話,write() 方法也會進入等待狀態。這種狀況下,操做系統會嘗試從新發送數據包一段時間,並在達到超時限制時產生錯誤。 在公司的 Linux服務器上這種狀況的超時時間設置爲15分鐘。

至此,我已經解釋了 JDBC 的內部操做,但願這將幫助你正確的超時配置超時時間從而減小錯誤。

至此,我已經對JDBC的內部操做作了講解,但願可以讓你們學會如何正確的配置超時時間,從而減小錯誤的發生。

相關文章
相關標籤/搜索