深入理解 JDBC 的超時

預流發表於2018-01-24

這是最近讀到的講關於 JDBC 的超時問題最透徹的文章,原文是http://www.cubrid.org/blog/understanding-jdbc-internals-and-timeout-configuration ,網上現有的翻譯感覺磕磕絆絆的,很多上下文資訊丟失了,這裡用我的理解重新翻譯一下。

應用程式中配置恰當的 JDBC 超時時間能減少服務失敗的時間,這篇文章我們將討論不同種類的超時和推薦的配置。

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

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

在 DDoS 攻擊之後,整個服務都不能正常工作了,因為第四層交換機不能工作,網路連線斷開了,這也導致 WAS (可以將 WAS 理解為作者公司的應用程式)不能正常工作。攻擊發生後不久,安全團隊攔截了所有 DDoS 攻擊,然後網路恢復正常,但 WAS 還是不能工作。

通過分析系統的 dump 日誌發現,業務系統停在了 JDBC API 的呼叫上。20分鐘後系統仍處於等待狀態無法響應,大概過了30分鐘,系統突然發生異常,然後服務恢復正常。

為什麼已經將查詢超時時間設定成3秒, WAS 卻等待了30分鐘?為什麼30分鐘後 WAS 又開始工作了?

如果理解了 JDBC 的超時機制就能找到答案。

為什麼我們需要知道 JDBC 驅動

當有效能問題或系統級錯誤時,WAS 和資料庫是我們關注的兩個重要層面。在我公司 WAS 和資料庫通常由不同的部門負責,因此每個部門聚焦在各自負責的領域來設法弄清楚狀況。此時 WAS 和資料庫之間的部分會因為得不到足夠的關注而產生盲區。對於 Java 應用,這個盲區在資料庫連線池和 JDBC 之間,本文我們將重點討論 JDBC。

什麼是 JDBC 驅動

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

圖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的內部操作做了講解,希望能夠讓大家學會如何正確的配置超時時間,從而減少錯誤的發生。

相關文章