深入理解JDBC的超時設(shè)置
恰當(dāng)?shù)腏DBC超時設(shè)置能夠有效地減少服務(wù)失效的時間。本文將對數(shù)據(jù)庫的各種超時設(shè)置及其設(shè)置方法做介紹。
真實案例:應(yīng)用服務(wù)器在遭到DDos攻擊后無法響應(yīng)
在遭到DDos攻擊后,整個服務(wù)都垮掉了。由于第四層交換機(jī)不堪重負(fù),網(wǎng)絡(luò)變得無法連接,從而導(dǎo)致業(yè)務(wù)系統(tǒng)也無法正常運轉(zhuǎn)。安全組很快屏蔽了所有的DDos攻擊,并恢復(fù)了網(wǎng)絡(luò),但業(yè)務(wù)系統(tǒng)卻還是無法工作。 通過分析系統(tǒng)的thread dump發(fā)現(xiàn),業(yè)務(wù)系統(tǒng)停在了JDBC API的調(diào)用上。20分鐘后,系統(tǒng)仍處于WAITING狀態(tài),無法響應(yīng)。30分鐘后,系統(tǒng)拋出異常,服務(wù)恢復(fù)正常。
為什么我們明明將query timeout設(shè)置成了3秒,系統(tǒng)卻持續(xù)了30分鐘的WAITING狀態(tài)?為什么30分鐘后系統(tǒng)又恢復(fù)正常了? 當(dāng)你對理解了JDBC的超時設(shè)置后,就能找到問題的答案。
為什么我們要了解JDBC?
當(dāng)遇到性能問題或系統(tǒng)出錯時,業(yè)務(wù)系統(tǒng)和數(shù)據(jù)庫通常是我們最關(guān)心的兩個部分。在公司里,這兩個部分是交由兩個不同的部門來負(fù)責(zé)的,因此各個部門都會集中精力地在自身領(lǐng)域內(nèi)尋找問題,這樣的話,在業(yè)務(wù)系統(tǒng)和數(shù)據(jù)庫之間的部分就會成為一個盲區(qū)。對于Java應(yīng)用而言,這個盲區(qū)就是DBCP數(shù)據(jù)庫連接池和JDBC,本文將集中介紹JDBC。
什么是JDBC?
JDBC是Java應(yīng)用中用來連接關(guān)系型數(shù)據(jù)庫的標(biāo)準(zhǔn)API。Sun公司一共定義了4種類型的JDBC,我們主要使用的是第4種,該類型的Driver完全由Java代碼實現(xiàn),通過使用socket與數(shù)據(jù)庫進(jìn)行通信。
第4種類型的JDBC通過socket對字節(jié)流進(jìn)行處理,因此也會有一些基本網(wǎng)絡(luò)操作,類似于HttpClient這種用于網(wǎng)絡(luò)操作的代碼庫。當(dāng)在網(wǎng)絡(luò)操作中遇到問題的時候,將會消耗大量的cpu資源,并且失去響應(yīng)超時。如果你之前用過HttpClient,那么你一定遇到過未設(shè)置timeout造成的錯誤。同樣,第4種類型的JDBC,若沒有合理地設(shè)置socket timeout,也會有相同的錯誤——連接被阻塞。
接下來,就讓我們來學(xué)習(xí)一下如何正確地設(shè)置socket timeout,以及需要考慮的問題。
應(yīng)用與數(shù)據(jù)庫間的timeout層級
上圖展示了簡化后應(yīng)用與數(shù)據(jù)庫間的timeout層級。(譯者注:WAS/BLOC是作者公司的具體應(yīng)用名稱,無需深究)
高級別的timeout依賴于低級別的timeout,只有當(dāng)?shù)图墑e的timeout無誤時,高級別的timeout才能確保正常。例如,當(dāng)socket timeout出現(xiàn)問題時,高級別的statement timeout和transaction timeout都將失效。
我們收到的很多評論中提到:
引用
即使設(shè)置了statement timeout,當(dāng)網(wǎng)絡(luò)出錯時,應(yīng)用也無法從錯誤中恢復(fù)。
statement timeout無法處理網(wǎng)絡(luò)連接失敗時的超時,它能做的僅僅是限制statement的操作時間。網(wǎng)絡(luò)連接失敗時的timeout必須交由JDBC來處理。
JDBC的socket timeout會受到操作系統(tǒng)socket timeout設(shè)置的影響,這就解釋了為什么在之前的案例中,JDBC連接會在網(wǎng)絡(luò)出錯后阻塞30分鐘,然后又奇跡般恢復(fù),即使我們并沒有對JDBC的socket timeout進(jìn)行設(shè)置。
DBCP連接池位于圖2的左側(cè),你會發(fā)現(xiàn)timeout層級與DBCP是相互獨立的。DBCP負(fù)責(zé)的是數(shù)據(jù)庫連接的創(chuàng)建和管理,并不干涉timeout的處理。當(dāng)連接在DBCP中創(chuàng)建,或是DBCP發(fā)送校驗query檢查連接有效性的時候,socket timeout將會影響這些過程,但并不直接對應(yīng)用造成影響。
當(dāng)在應(yīng)用中調(diào)用DBCP的getConnection()方法時,你可以設(shè)置獲取數(shù)據(jù)庫連接的超時時間,但是這和JDBC的timeout毫不相關(guān)。
什么是Transaction Timeout?
transaction timeout一般存在于框架(Spring, EJB)或應(yīng)用級。transaction timeout或許是個相對陌生的概念,簡單地說,transaction timeout就是“statement Timeout * N(需要執(zhí)行的statement數(shù)量) + @(垃圾回收等其他時間)”。transaction timeout用來限制執(zhí)行statement的總時長。
例如,假設(shè)執(zhí)行一個statement需要0.1秒,那么執(zhí)行少量statement不會有什么問題,但若是要執(zhí)行100,000個statement則需要10,000秒(約7個小時)。這時,transaction timeout就派上用場了。EJB CMT (Container Managed Transaction)就是一種典型的實現(xiàn),它提供了多種方法供開發(fā)者選擇。但我們并不使用EJB,Spring的transaction timeout設(shè)置會更常用一些。在Spring中,你可以使用下面展示的XML或是在源碼中使用@Transactional注解來進(jìn)行設(shè)置。
Xml代碼
- <tx:attributes>
- <tx:method name=“…” timeout=“3″/>
- </tx:attributes>
Spring提供的transaction timeout配置非常簡單,它會記錄每個事務(wù)的開始時間和消耗時間,當(dāng)特定的事件發(fā)生時就會對消耗時間做校驗,當(dāng)超出timeout值時將拋出異常。
Spring中,數(shù)據(jù)庫連接被保存在ThreadLocal里,這被稱為事務(wù)同步(Transaction Synchronization),與此同時,事務(wù)的開始時間和消耗時間也被保存下來。當(dāng)使用這種代理連接創(chuàng)建statement時,就會校驗事務(wù)的消耗時間。EJB CMT的實現(xiàn)方式與之類似,其結(jié)構(gòu)本身也十分簡單。
當(dāng)你選用的容器或框架并不支持transaction timeout這一特性,你可以考慮自己來實現(xiàn)。transaction timeout并沒有標(biāo)準(zhǔn)的API。Lucy框架的1.5和1.6版本都不支持transaction timeout,但是你可以通過使用Spring的Transaction Manager來達(dá)到與之同樣的效果。
假設(shè)某個事務(wù)中包含5個statement,每個statement的執(zhí)行時間是200ms,其他業(yè)務(wù)邏輯的執(zhí)行時間是100ms,那么transaction timeout至少應(yīng)該設(shè)置為1,100ms(200 * 5 + 100)。
什么是Statement Timeout?
statement timeout用來限制statement的執(zhí)行時長,timeout的值通過調(diào)用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API進(jìn)行設(shè)置。不過現(xiàn)在開發(fā)者已經(jīng)很少直接在代碼中設(shè)置,而多是通過框架來進(jìn)行設(shè)置。
以iBatis為例,statement timeout的默認(rèn)值可以通過sql-map-config.xml中的defaultStatementTimeout 屬性進(jìn)行設(shè)置。同時,你還可以設(shè)置sqlmap中select,insert,update標(biāo)簽的timeout屬性,從而對不同sql語句的超時時間進(jìn)行獨立的配置。
如果你使用的是Lucy1.5或1.6版本,通過設(shè)置queryTimeout屬性可以在datasource層面對statement timeout進(jìn)行設(shè)置。
statement timeout的具體值需要依據(jù)應(yīng)用本身的特性而定,并沒有可供推薦的配置。
JDBC的statement timeout處理過程
不同的關(guān)系型數(shù)據(jù)庫,以及不同的JDBC驅(qū)動,其statement timeout處理過程會有所不同。其中,Oracle和MS SQLServer的處理相類似,MySQL和CUBRID類似。
Oracle JDBC Statement的QueryTimeout處理過程
1. 通過調(diào)用Connection的createStatement()方法創(chuàng)建statement
2. 調(diào)用Statement的executeQuery()方法
3. statement通過自身connection將query發(fā)送給Oracle數(shù)據(jù)庫
4. statement在OracleTimeoutPollingThread(每個classloader一個)上進(jìn)行注冊
5. 達(dá)到超時時間
6. OracleTimeoutPollingThread調(diào)用OracleStatement的cancel()方法
7. 通過connection向正在執(zhí)行的query發(fā)送cancel消息
JTDS (MS SQLServer) Statement的QueryTimeout處理過程
1. 通過調(diào)用Connection的createStatement()方法創(chuàng)建statement
2. 調(diào)用Statement的executeQuery()方法
3. statement通過自身connection將query發(fā)送給MS SqlServer數(shù)據(jù)庫
4. statement在TimerThread上進(jìn)行注冊
5. 達(dá)到超時時間
6. TimerThread調(diào)用JtdsStatement實例中的TsdCore.cancel()方法
7. 通過ConnectionJDBC向正在執(zhí)行的query發(fā)送cancel消息
MySQL JDBC Statement的QueryTimeout處理過程
1. 通過調(diào)用Connection的createStatement()方法創(chuàng)建statement
2. 調(diào)用Statement的executeQuery()方法
3. statement通過自身connection將query發(fā)送給MySQL數(shù)據(jù)庫
4. statement創(chuàng)建一個新的timeout-execution線程用于超時處理
5. 5.1版本后改為每個connection分配一個timeout-execution線程
6. 向timeout-execution線程進(jìn)行注冊
7. 達(dá)到超時時間
6. TimerThread調(diào)用JtdsStatement實例中的TsdCore.cancel()方法
7. timeout-execution線程創(chuàng)建一個和statement配置相同的connection
8. 使用新創(chuàng)建的connection向超時query發(fā)送cancel query(KILL QUERY “connectionId”)
CUBRID JDBC Statement的QueryTimeout處理過程
1. 通過調(diào)用Connection的createStatement()方法創(chuàng)建statement
2. 調(diào)用Statement的executeQuery()方法
3. statement通過自身connection將query發(fā)送給CUBRID數(shù)據(jù)庫
4. statement創(chuàng)建一個新的timeout-execution線程用于超時處理
5. 5.1版本后改為每個connection分配一個timeout-execution線程
6. 向timeout-execution線程進(jìn)行注冊
7. 達(dá)到超時時間
6. TimerThread調(diào)用JtdsStatement實例中的TsdCore.cancel()方法
7. timeout-execution線程創(chuàng)建一個和statement配置相同的connection
8. 使用新創(chuàng)建的connection向超時query發(fā)送cancel消息
什么是JDBC的socket timeout?
第4種類型的JDBC使用socket與數(shù)據(jù)庫連接,數(shù)據(jù)庫并不對應(yīng)用與數(shù)據(jù)庫間的連接超時進(jìn)行處理。
JDBC的socket timeout在數(shù)據(jù)庫被突然停掉或是發(fā)生網(wǎng)絡(luò)錯誤(由于設(shè)備故障等原因)時十分重要。由于TCP/IP的結(jié)構(gòu)原因,socket沒有辦法探測到網(wǎng)絡(luò)錯誤,因此應(yīng)用也無法主動發(fā)現(xiàn)數(shù)據(jù)庫連接斷開。如果沒有設(shè)置socket timeout的話,應(yīng)用在數(shù)據(jù)庫返回結(jié)果前會無期限地等下去,這種連接被稱為dead connection。
為了避免dead connections,socket必須要有超時配置。socket timeout可以通過JDBC設(shè)置,socket timeout能夠避免應(yīng)用在發(fā)生網(wǎng)絡(luò)錯誤時產(chǎn)生無休止等待的情況,縮短服務(wù)失效的時間。
不推薦使用socket timeout來限制statement的執(zhí)行時長,因此socket timeout的值必須要高于statement timeout,否則,socket timeout將會先生效,這樣statement timeout就變得毫無意義,也無法生效。
下面展示了socket timeout的兩個設(shè)置項,不同的JDBC驅(qū)動其配置方式會有所不同。
- socket連接時的timeout:通過Socket.connect(SocketAddress endpoint, int timeout)設(shè)置
- socket讀寫時的timeout:通過Socket.setSoTimeout(int timeout)設(shè)置
通過查看CUBRID,MySQL,MS SQL Server (JTDS)和Oracle的JDBC驅(qū)動源碼,我們發(fā)現(xiàn)所有的驅(qū)動內(nèi)部都是使用上面的2個API來設(shè)置socket timeout的。
下面是不同驅(qū)動的socket timeout配置方式。
- connectTimeout和socketTimeout的默認(rèn)值為0時,timeout不生效。
- 除了調(diào)用DBCP的API以外,還可以通過properties屬性進(jìn)行配置。
通過properties屬性進(jìn)行配置時,需要傳入key為“connectionProperties”的鍵值對,value的格式為“[propertyName=property;]*”。下面是iBatis中的properties配置。
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>
操作系統(tǒng)的socket timeout配置
如果不設(shè)置socket timeout或connect timeout,應(yīng)用多數(shù)情況下是無法發(fā)現(xiàn)網(wǎng)絡(luò)錯誤的。因此,當(dāng)網(wǎng)絡(luò)錯誤發(fā)生后,在連接重新連接成功或成功接收到數(shù)據(jù)之前,應(yīng)用會無限制地等下去。但是,通過本文開篇處的實際案例我們發(fā)現(xiàn),30分鐘后應(yīng)用的連接問題奇跡般的解決了,這是因為操作系統(tǒng)同樣能夠?qū)ocket timeout進(jìn)行配置。公司的Linux服務(wù)器將socket timeout設(shè)置為了30分鐘,從而會在操作系統(tǒng)的層面對網(wǎng)絡(luò)連接做校驗,因此即使JDBC的socket timeout設(shè)置為0,由網(wǎng)絡(luò)錯誤造成的數(shù)據(jù)庫連接問題的持續(xù)時間也不會超過30分鐘。
通常,應(yīng)用會在調(diào)用Socket.read()時由于網(wǎng)絡(luò)問題被阻塞住,而很少在調(diào)用Socket.write()時進(jìn)入waiting狀態(tài),這取決于網(wǎng)絡(luò)構(gòu)成和錯誤類型。當(dāng)Socket.write()被調(diào)用時,數(shù)據(jù)被寫入到操作系統(tǒng)內(nèi)核的緩沖區(qū),控制權(quán)立即回到應(yīng)用手上。因此,一旦數(shù)據(jù)被寫入內(nèi)核緩沖區(qū),Socket.write()調(diào)用就必然會成功。但是,如果系統(tǒng)內(nèi)核緩沖區(qū)由于某種網(wǎng)絡(luò)錯誤而滿了的話,Socket.write()也會進(jìn)入waiting狀態(tài)。這種情況下,操作系統(tǒng)會嘗試重新發(fā)包,當(dāng)達(dá)到重試的時間限制時,將產(chǎn)生系統(tǒng)錯誤。在我們公司,重新發(fā)包的超時時間被設(shè)置為15分鐘。
至此,我已經(jīng)對JDBC的內(nèi)部操作做了講解,希望能夠讓大家學(xué)會如何正確的配置超時時間,從而減少錯誤的發(fā)生。
最后,我將列出一些常見的問題。
FAQ
Q1. 我已經(jīng)使用Statement.setQueryTimeout()方法設(shè)置了查詢超時,但在網(wǎng)絡(luò)出錯時并沒有產(chǎn)生作用。
➔ 查詢超時僅在socket timeout生效的前提下才有效,它并不能用來解決外部的網(wǎng)絡(luò)錯誤,要解決這種問題,必須設(shè)置JDBC的socket timeout。
Q2. transaction timeout,statement timeout和socket timeout和DBCP的配置有什么關(guān)系?
➔ 當(dāng)通過DBCP獲取數(shù)據(jù)庫連接時,除了DBCP獲取連接時的waitTimeout配置以外,其他配置對JDBC沒有什么影響。
Q3. 如果設(shè)置了JDBC的socket timeout,那DBCP連接池中處于IDLE狀態(tài)的連接是否也會在達(dá)到超時時間后被關(guān)閉?
➔ 不會。socket的設(shè)置只會在產(chǎn)生數(shù)據(jù)讀寫時生效,而不會對DBCP中的IDLE連接產(chǎn)生影響。當(dāng)DBCP中發(fā)生新連接創(chuàng)建,老的IDLE連接被移除,或是連接有效性校驗的時候,socket設(shè)置會對其產(chǎn)生一定的影響,但除非發(fā)生網(wǎng)絡(luò)問題,否則影響很小。
Q4. socket timeout應(yīng)該設(shè)置為多少?
➔ 就像我在正文中提的那樣,socket timeout必須高于statement timeout,但并沒有什么推薦值。在發(fā)生網(wǎng)絡(luò)錯誤的時候,socket timeout將會生效,但是再小心的配置也無法避免網(wǎng)絡(luò)錯誤的發(fā)生,只是在網(wǎng)絡(luò)錯誤發(fā)生后縮短服務(wù)失效的時間(如果網(wǎng)絡(luò)恢復(fù)正常的話)。