深入理解JDBC的超時設置

原文地址:https://www.cubrid.org/blog/understanding-jdbc-internals-and-timeout-configuration

<!-- -->

恰當的JDBC超時設置能夠有效地減少服務失效的時間。本文將對數據庫的各種超時設置及其設置方法做介紹。

深入理解JDBC的超時設置

在遭到DDos攻擊後,整個服務都垮掉了。由於第四層交換機不堪重負,網絡變得無法連接,從而導致業務系統也無法正常運轉。安全組很快屏蔽了所有的DDos攻擊,並恢復了網絡,但業務系統卻還是無法工作。 
通過分析系統的thread dump發現,業務系統停在了JDBC API的調用上。20分鐘後,系統仍處於WAITING狀態,無法響應。30分鐘後,系統拋出異常,服務恢復正常。 

爲什麼我們明明將query timeout設置成了3秒,系統卻持續了30分鐘的WAITING狀態?爲什麼30分鐘後系統又恢復正常了? 

當你對理解了JDBC的超時設置後,就能找到問題的答案。 


爲什麼我們要了解JDBC? 

當遇到性能問題或系統出錯時,業務系統和數據庫通常是我們最關心的兩個部分。在公司裏,這兩個部分是交由兩個不同的部門來負責的,因此各個部門都會集中精力地在自身領域內尋找問題,這樣的話,在業務系統和數據庫之間的部分就會成爲一個盲區。對於Java應用而言,這個盲區就是DBCP數據庫連接池和JDBC,本文將集中介紹JDBC。 

什麼是JDBC? 

JDBC是Java應用中用來連接關係型數據庫的標準API。Sun公司一共定義了4種類型的JDBC,我們主要使用的是第4種,該類型的Driver完全由Java代碼實現,通過使用socket與數據庫進行通信。 


Figure 1: JDBC Type 4.

第4種類型的JDBC通過socket對字節流進行處理,因此也會有一些基本網絡操作,類似於HttpClient這種用於網絡操作的代碼庫。當在網絡操作中遇到問題的時候,將會消耗大量的cpu資源,並且失去響應超時。如果你之前用過HttpClient,那麼你一定遇到過未設置timeout造成的錯誤。同樣,第4種類型的JDBC,若沒有合理地設置socket timeout,也會有相同的錯誤——連接被阻塞。 

接下來,就讓我們來學習一下如何正確地設置socket timeout,以及需要考慮的問題。 

應用與數據庫間的timeout層級 


Figure 2: Timeout Class.

上圖展示了簡化後應用與數據庫間的timeout層級。(譯者注:WAS/BLOC是作者公司的具體應用名稱,無需深究) 

高級別的timeout依賴於低級別的timeout,只有當低級別的timeout無誤時,高級別的timeout才能確保正常。例如,當socket timeout出現問題時,高級別的statement timeout和transaction timeout都將失效。 

我們收到的很多評論中提到: 

即使設置了statement timeout,當網絡出錯時,應用也無法從錯誤中恢復。


statement timeout無法處理網絡連接失敗時的超時,它能做的僅僅是限制statement的操作時間。網絡連接失敗時的timeout必須交由JDBC來處理。 
JDBC的socket timeout會受到操作系統socket timeout設置的影響,這就解釋了爲什麼在之前的案例中,JDBC連接會在網絡出錯後阻塞30分鐘,然後又奇蹟般恢復,即使我們並沒有對JDBC的socket timeout進行設置。 


DBCP連接池位於圖2的左側,你會發現timeout層級與DBCP是相互獨立的。DBCP負責的是數據庫連接的創建和管理,並不干涉timeout的處理。當連接在DBCP中創建,或是DBCP發送校驗query檢查連接有效性的時候,socket timeout將會影響這些過程,但並不直接對應用造成影響。 

當在應用中調用DBCP的getConnection()方法時,你可以設置獲取數據庫連接的超時時間,但是這和JDBC的timeout毫不相關。 


Figure 3: Timeout for Each Levels.


什麼是Transaction Timeout?

transaction timeout一般存在於框架(Spring, EJB)或應用級。transaction timeout或許是個相對陌生的概念,簡單地說,transaction timeout就是“statement Timeout * N(需要執行的statement數量) + @(垃圾回收等其他時間)”。transaction timeout用來限制執行statement的總時長。 
例如,假設執行一個statement需要0.1秒,那麼執行少量statement不會有什麼問題,但若是要執行100,000個statement則需要10,000秒(約7個小時)。這時,transaction timeout就派上用場了。EJB CMT (Container Managed Transaction)就是一種典型的實現,它提供了多種方法供開發者選擇。但我們並不使用EJB,Spring的transaction timeout設置會更常用一些。在Spring中,你可以使用下面展示的XML或是在源碼中使用@Transactional註解來進行設置。 
<tx:attributes>  
        <tx:method name="…" timeout="3"/>  
</tx:attributes>  

Spring提供的transaction timeout配置非常簡單,它會記錄每個事務的開始時間和消耗時間,當特定的事件發生時就會對消耗時間做校驗,當超出timeout值時將拋出異常。 
Spring中,數據庫連接被保存在ThreadLocal裏,這被稱爲事務同步(Transaction Synchronization),與此同時,事務的開始時間和消耗時間也被保存下來。當使用這種代理連接創建statement時,就會校驗事務的消耗時間。EJB CMT的實現方式與之類似,其結構本身也十分簡單。 
當你選用的容器或框架並不支持transaction timeout這一特性,你可以考慮自己來實現。transaction timeout並沒有標準的API。Lucy框架的1.5和1.6版本都不支持transaction timeout,但是你可以通過使用Spring的Transaction Manager來達到與之同樣的效果。 

假設某個事務中包含5個statement,每個statement的執行時間是200ms,其他業務邏輯的執行時間是100ms,那麼transaction timeout至少應該設置爲1,100ms(200 * 5 + 100)。 


什麼是Statement Timeout?

statement timeout用來限制statement的執行時長,timeout的值通過調用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API進行設置。不過現在開發者已經很少直接在代碼中設置,而多是通過框架來進行設置。 
以iBatis爲例,statement timeout的默認值可以通過sql-map-config.xml中的defaultStatementTimeout 屬性進行設置。同時,你還可以設置sqlmap中select,insert,update標籤的timeout屬性,從而對不同sql語句的超時時間進行獨立的配置。 
如果你使用的是Lucy1.5或1.6版本,通過設置queryTimeout屬性可以在datasource層面對statement timeout進行設置。 
statement timeout的具體值需要依據應用本身的特性而定,並沒有可供推薦的配置。 


JDBC的statement timeout處理過程 

不同的關係型數據庫,以及不同的JDBC驅動,其statement timeout處理過程會有所不同。其中,Oracle和MS SQLServer的處理相類似,MySQL和CUBRID類似。 

Oracle JDBC Statement的QueryTimeout處理過程 

1. 通過調用Connection的createStatement()方法創建statement 
2. 調用Statement的executeQuery()方法 
3. statement通過自身connection將query發送給Oracle數據庫 
4. statement在OracleTimeoutPollingThread(每個classloader一個)上進行註冊 
5. 達到超時時間 
6. OracleTimeoutPollingThread調用OracleStatement的cancel()方法 
7. 通過connection向正在執行的query發送cancel消息 


Figure 4: Query Timeout Execution Process for Oracle JDBC Statement.


JTDS (MS SQLServer) Statement的QueryTimeout處理過程 

1. 通過調用Connection的createStatement()方法創建statement 
2. 調用Statement的executeQuery()方法 
3. statement通過自身connection將query發送給MS SqlServer數據庫 
4. statement在TimerThread上進行註冊 
5. 達到超時時間 
6. TimerThread調用JtdsStatement實例中的TsdCore.cancel()方法 
7. 通過ConnectionJDBC向正在執行的query發送cancel消息 



Figure 5: QueryTimeout Execution Process for JTDS (MS SQLServer) Statement.


MySQL JDBC Statement的QueryTimeout處理過程 

1. 通過調用Connection的createStatement()方法創建statement 
2. 調用Statement的executeQuery()方法 
3. statement通過自身connection將query發送給MySQL數據庫 
4. statement創建一個新的timeout-execution線程用於超時處理 
5. 5.1版本後改爲每個connection分配一個timeout-execution線程 
6. 向timeout-execution線程進行註冊 
7. 達到超時時間 
6. TimerThread調用JtdsStatement實例中的TsdCore.cancel()方法 
7. timeout-execution線程創建一個和statement配置相同的connection 
8. 使用新創建的connection向超時query發送cancel query(KILL QUERY "connectionId") 


Figure 6: QueryTimeout Execution Process for MySQL JDBC Statement (5.0.8).

UBRID JDBC Statement的QueryTimeout處理過程 

1. 通過調用Connection的createStatement()方法創建statement 
2. 調用Statement的executeQuery()方法 
3. statement通過自身connection將query發送給CUBRID數據庫 
4. statement創建一個新的timeout-execution線程用於超時處理 
5. 5.1版本後改爲每個connection分配一個timeout-execution線程 
6. 向timeout-execution線程進行註冊 
7. 達到超時時間 
6. TimerThread調用JtdsStatement實例中的TsdCore.cancel()方法 
7. timeout-execution線程創建一個和statement配置相同的connection 

8. 使用新創建的connection向超時query發送cancel消息 


Figure 7: QueryTimeout Execution Process for CUBRID JDBC Statement.

什麼是JDBC的socket timeout? 

第4種類型的JDBC使用socket與數據庫連接,數據庫並不對應用與數據庫間的連接超時進行處理。 
JDBC的socket timeout在數據庫被突然停掉或是發生網絡錯誤(由於設備故障等原因)時十分重要。由於TCP/IP的結構原因,socket沒有辦法探測到網絡錯誤,因此應用也無法主動發現數據庫連接斷開。如果沒有設置socket timeout的話,應用在數據庫返回結果前會無期限地等下去,這種連接被稱爲dead connection。 
爲了避免dead connections,socket必須要有超時配置。socket timeout可以通過JDBC設置,socket timeout能夠避免應用在發生網絡錯誤時產生無休止等待的情況,縮短服務失效的時間。 


不推薦使用socket timeout來限制statement的執行時長,因此socket timeout的值必須要高於statement timeout,否則,socket timeout將會先生效,這樣statement timeout就變得毫無意義,也無法生效。 


下面展示了socket timeout的兩個設置項,不同的JDBC驅動其配置方式會有所不同。 
  • socket連接時的timeout:通過Socket.connect(SocketAddress endpoint, int timeout)設置
  • socket讀寫時的timeout:通過Socket.setSoTimeout(int timeout)設置

通過查看CUBRID,MySQL,MS SQL Server (JTDS)和Oracle的JDBC驅動源碼,我們發現所有的驅動內部都是使用上面的2個API來設置socket timeout的。 


下面是不同驅動的socket timeout配置方式。 
JDBC DriverconnectTimeoutDefaultUnitApplication Method
socketTimeoutDefaultUnit
MySQL DriverconnectTimeout0ms

Specify the option in the DriverURL.
Format:
jdbc:mysql://[host:port],[host:port].../[database]
[?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]...

Example:

jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000
socketTimeout0ms
MS-SQL Driver
jTDS Driver
loginTimeout0sec

Specify the option in the DriverURL. Format:
jdbc:jtds:<server_type>://<server>[:<port>][/<database>][;<property>=<value>[;...]]

Example:

jdbc:jtds:sqlserver://server:port/database;loginTimeout=60;socketTimeout=60

 

socketTimeout0sec
Oracle Thin Driveroracle.net.CONNECT_TIMEOUT0msNot possible with the driverURL. Must be delivered to the properties object via OracleDatasource.setConnectionProperties() API. When DBCP is used, use the following APIs:
  • BasicDatasource.setConnectionProperties()
  • BasicDatasource.addConnectionProperties()
oracle.jdbc.ReadTimeout0ms
CUBRID Thin DriverNo separate configuration5,000msNot possible with the driverURL. Timeout occurs in 5 seconds.
  • Note 1: When timeout occurs with althost option specified in the URL, it can be connected to the designated host.
  • Note 2: C API can be used to state the login_time option in ms in the URL.
  • connectTimeout和socketTimeout的默認值爲0時,timeout不生效。
  • 除了調用DBCP的API以外,還可以通過properties屬性進行配置。

通過properties屬性進行配置時,需要傳入key爲“connectionProperties”的鍵值對,value的格式爲“[propertyName=property;]*”。下面是iBatis中的properties配置。 

<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 timeout配置 

如果不設置socket timeout或connect timeout,應用多數情況下是無法發現網絡錯誤的。因此,當網絡錯誤發生後,在連接重新連接成功或成功接收到數據之前,應用會無限制地等下去。但是,通過本文開篇處的實際案例我們發現,30分鐘後應用的連接問題奇蹟般的解決了,這是因爲操作系統同樣能夠對socket timeout進行配置。公司的Linux服務器將socket timeout設置爲了30分鐘,從而會在操作系統的層面對網絡連接做校驗,因此即使JDBC的socket timeout設置爲0,由網絡錯誤造成的數據庫連接問題的持續時間也不會超過30分鐘。 


通常,應用會在調用Socket.read()時由於網絡問題被阻塞住,而很少在調用Socket.write()時進入waiting狀態,這取決於網絡構成和錯誤類型。當Socket.write()被調用時,數據被寫入到操作系統內核的緩衝區,控制權立即回到應用手上。因此,一旦數據被寫入內核緩衝區,Socket.write()調用就必然會成功。但是,如果系統內核緩衝區由於某種網絡錯誤而滿了的話,Socket.write()也會進入waiting狀態。這種情況下,操作系統會嘗試重新發包,當達到重試的時間限制時,將產生系統錯誤。在我們公司,重新發包的超時時間被設置爲15分鐘。 

至此,我已經對JDBC的內部操作做了講解,希望能夠讓大家學會如何正確的配置超時時間,從而減少錯誤的發生。 

最後,我將列出一些常見的問題。 


FAQ 

Q1. 我已經使用Statement.setQueryTimeout()方法設置了查詢超時,但在網絡出錯時並沒有產生作用。 
➔ 查詢超時僅在socket timeout生效的前提下才有效,它並不能用來解決外部的網絡錯誤,要解決這種問題,必須設置JDBC的socket timeout。 

Q2. transaction timeout,statement timeout和socket timeout和DBCP的配置有什麼關係? 
➔ 當通過DBCP獲取數據庫連接時,除了DBCP獲取連接時的waitTimeout配置以外,其他配置對JDBC沒有什麼影響。 

Q3. 如果設置了JDBC的socket timeout,那DBCP連接池中處於IDLE狀態的連接是否也會在達到超時時間後被關閉? 
➔ 不會。socket的設置只會在產生數據讀寫時生效,而不會對DBCP中的IDLE連接產生影響。當DBCP中發生新連接創建,老的IDLE連接被移除,或是連接有效性校驗的時候,socket設置會對其產生一定的影響,但除非發生網絡問題,否則影響很小。 

Q4. socket timeout應該設置爲多少? 
➔ 就像我在正文中提的那樣,socket timeout必須高於statement timeout,但並沒有什麼推薦值。在發生網絡錯誤的時候,socket timeout將會生效,但是再小心的配置也無法避免網絡錯誤的發生,只是在網絡錯誤發生後縮短服務失效的時間(如果網絡恢復正常的話)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章