在工作中,出現使用Druid鏈接Postgresql數據庫,當第一次動態鏈接數據庫源或者連過長時間不使用數據庫時,出現鏈接過程緩慢,甚至hold的情況,鏈接時間以分計算,導致前端接口響應緩慢,影響用戶操作問題。
經過日誌的排查,發現是在DruidDataSource對象鏈接數據源過程中卡主了,並不是執行SQL語句查詢慢的原因,通過網上查詢資料,從一下幾方面進行改進和問題排查。
1、是否存在數據源配置文件鏈接URL地址,host配置問題
2、是否可以通過修改數據源存活時間或者使用定時心跳保證鏈接長時間存活
3、是否能夠通過數據源配置文件鏈接其他參數進行配置,避免該問題
4、需要通過Druid框架源碼分析問題的主要原因,可能更Linux系統、防火牆、數據庫服務相關參數存在一定的關係。
通過Druid社區翻閱相關帖子,發現了和我相同問題的帖子,https://www.oschina.net/question/1450045_2157629?sort=time
帖子中通過程序日誌和druid的統計日誌進行分析鎖定問題在獲取鏈接時或者進行testWhileIdle空閒檢驗過程中。更多參考文章
https://blog.csdn.net/aiyo92/article/details/86540647
https://www.oschina.net/question/1450045_2157629?sort=time
https://www.oschina.net/question/1450045_2157160
https://my.oschina.net/u/3434392/blog/3017866
https://www.oschina.net/question/2433836_2148757
https://www.oschina.net/question/1034044_2162691?sort=default
https://my.oschina.net/u/3434392/blog/3017866
最後發現是socketTimeout的參數設置的問題,可以通過linux服務器設置,數據源鏈接也可以設置
jdbc timeout類別
主要有如下幾個類別
- transaction timeout 設置的是一個事務的執行時間,裏頭可能包含多個statement
- statement timeout(
也相當於result set fetch timeout
) 設置的是一個statement的執行超時時間,即driver等待statement執行完成,接收到數據的超時時間(注意statement的timeout不是整個查詢的timeout,只是statement執行完成並拉取fetchSize數據返回的超時,之後resultSet的next在必要的時候還會觸發fetch數據,每次fetch的超時時間是單獨算的,默認也是以statement設置的timeout爲準
) - jdbc socket timeout 設置的是jdbc I/O socket read and write operations的超時時間,防止因網絡問題或數據庫問題,導致driver一直阻塞等待。(
建議比statement timeout的時間長
) - os socket timeout 這個是操作系統級別的socket設置(
如果jdbc socket timeout沒有設置,而os級別的socket timeout有設置,則使用系統的socket timeout值
)。
上面的不同級別的timeout越往下優先級越高,也就是說如果下面的配置比上面的配置值小的話,則會優先觸發timeout,那麼相當於上面的配置值就”失效”了。
jdbc socket timeout
這個不同數據的jdbc driver實現不一樣
mysql
jdbc:mysql://localhost:3306/ag_admin?useUnicode=true&characterEncoding=UTF8&connectTimeout=60000&socketTimeout=60000
通過url參數傳遞即可
pg
jdbc:postgresql://localhost/test?user=fred&password=secret&&connectTimeout=60&socketTimeout=60
pg也是通過url傳遞,不過它的單位與mysql不同,mysql是毫秒,而pg是秒
oracle
oracle需要通過oracle.jdbc.ReadTimeout參數來設置,連接超時參數是oracle.net.CONNECT_TIMEOUT
- 通過properties設置 Class.forName("oracle.jdbc.driver.OracleDriver"); Properties props = new Properties() ; props.put( "user" , "test_schema") ; props.put( "password" , "pwd") ; props.put( "oracle.net.CONNECT_TIMEOUT" , "10000000") ; props.put( "oracle.jdbc.ReadTimeout" , "2000" ) ; Connection conn = DriverManager.getConnection( "jdbc:oracle:thin:@10.0.1.9:1521:orcl" , props ) ;
- 通過環境變量設置 String readTimeout = "10000"; // ms System.setProperty("oracle.jdbc.ReadTimeout", readTimeout); Class.forName("oracle.jdbc.OracleDriver"); Connection conn = DriverManager.getConnection(jdbcUrl, user, pwd); 注意需要在connection連接之前設置環境變量
- tomcat jdbc pool 一般我們不直接使用jdbc connection,而是使用連接池。由於tomcat jdbc pool是springboot默認使用的數據庫連接池,這裏就講述一下如何在tomcat jdbc pool下設置。 spring.datasource.tomcat.connectionProperties=oracle.net.CONNECT_TIMEOUT=10000;oracle.jdbc.ReadTimeout=60000 注意,這裏是分號分隔,單位是毫秒,這裏可以根據各自的情況配置前綴(
tomcat jdbc連接池的話,默認是spring.datasource.tomcat
),可以自定義,比如 @Bean @Qualifier("writeDataSource") @ConfigurationProperties(prefix = "spring.datasource.write") public DataSource writeDataSource() { return DataSourceBuilder.create().build(); } 假設你這裏是自定義了prefix爲spring.datasource.write,那麼上述配置就變爲 spring.datasource.write.connectionProperties=oracle.net.CONNECT_TIMEOUT=10000;oracle.jdbc.ReadTimeout=60000 oracle.jdbc.ReadTimeout如果沒有設置的話,driver裏頭默認是0
oracle.jdbc.ReadTimeout
driver內部將該值設置到oracle.net.READ_TIMEOUT變量上
- oracle.net.nt.TcpNTAdapter
@Override public void setReadTimeoutIfRequired(final Properties properties) throws IOException, NetException { String s = ((Hashtable<K, String>)properties).get("oracle.net.READ_TIMEOUT"); if (s == null) { s = "0"; } this.setOption(3, s); } public void setOption(int var1, Object var2) throws IOException, NetException { String var3; switch(var1) { case 0: var3 = (String)var2; this.socket.setTcpNoDelay(var3.equals("YES")); break; case 1: var3 = (String)var2; if(var3.equals("YES")) { this.socket.setKeepAlive(true); } case 2: default: break; case 3: this.sockTimeout = Integer.parseInt((String)var2); this.socket.setSoTimeout(this.sockTimeout); } }
可用看到最後設置的是socket的soTimeout
實例
@Test public void testReadTimeout() throws SQLException { Connection connection = dataSource.getConnection(); String sql = "select * from demo_table"; PreparedStatement pstmt; try { pstmt = (PreparedStatement)connection.prepareStatement(sql); ResultSet rs = pstmt.executeQuery(); int col = rs.getMetaData().getColumnCount(); System.out.println("============================"); while (rs.next()) { for (int i = 1; i <= col; i++) { System.out.print(rs.getObject(i)); } System.out.println(""); } System.out.println("============================"); } catch (SQLException e) { e.printStackTrace(); } }
超時錯誤輸出
//部分數據輸出 java.sql.SQLRecoverableException: IO 錯誤: Socket read timed out at oracle.jdbc.driver.T4CPreparedStatement.fetch(T4CPreparedStatement.java:1128) at oracle.jdbc.driver.OracleResultSetImpl.close_or_fetch_from_next(OracleResultSetImpl.java:373) at oracle.jdbc.driver.OracleResultSetImpl.next(OracleResultSetImpl.java:277) at com.example.demo.DemoApplicationTests.testReadTimeout(DemoApplicationTests.java:68) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) Caused by: oracle.net.ns.NetException: Socket read timed out at oracle.net.ns.Packet.receive(Packet.java:339) at oracle.net.ns.DataPacket.receive(DataPacket.java:106) at oracle.net.ns.NetInputStream.getNextPacket(NetInputStream.java:315) at oracle.net.ns.NetInputStream.read(NetInputStream.java:260) at oracle.net.ns.NetInputStream.read(NetInputStream.java:185) at oracle.net.ns.NetInputStream.read(NetInputStream.java:102) at oracle.jdbc.driver.T4CSocketInputStreamWrapper.readNextPacket(T4CSocketInputStreamWrapper.java:124) at oracle.jdbc.driver.T4CSocketInputStreamWrapper.read(T4CSocketInputStreamWrapper.java:80) at oracle.jdbc.driver.T4CMAREngine.unmarshalUB1(T4CMAREngine.java:1137) at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:290) at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:192) at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:531) at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:207) at oracle.jdbc.driver.T4CPreparedStatement.fetch(T4CPreparedStatement.java:1119) ... 35 more
剛開始會有數據輸出,但是到了某個resultSet的next的時候,報了超時(
close_or_fetch_from_next
),這個超時指定的是當result.next方法觸發新的一批數據的拉取(當一個fetchSize的數據消費完之後,接下來的next會觸發新一批數據的fetch
)之後在timeout時間返回內沒有收到數據庫返回的數據。 oracle的jdbc默認的fetchSize爲10,也就是每個fetch,如果超過指定時間沒接收到數據,則拋出timeout異常。
小結
jdbc的socketTimeout值的設置要非常小心,不同數據庫的jdbc driver設置不一樣,特別是使用不同連接池的話,設置也可能不盡相同。對於嚴重依賴數據庫操作的服務來說,非常有必要設置這個值,否則萬一網絡或數據庫異常,會導致服務線程一直阻塞在java.net.SocketInputStream.socketRead0。
- 如果查詢數據多,則會導致該線程持有的data list不能釋放,相當於內存泄露,最後導致OOM
- 如果請求數據庫操作很多且阻塞住了,會導致服務器可用的woker線程變少,嚴重則會導致服務不可用,nginx報504 Gateway Timeout
doc
- oracle.jdbc.ReadTimeout
- 深入理解JDBC的超時設置
- 在Spring中基於JDBC進行數據訪問時如何控制超時
- BugFix-HttpURLConnection