问题描述

 

有一个应用,偶尔会出现访问不了的情况。具体表现为,当其它应用调用它的接口时,可能会出现超时,过一段时间后,再调用它的接口可能又正常了。观察应用日志发现,这个被调用的应用会偶发性地连接不上数据库。因为该应用没有办法及时地查询到数据库,应用返回数据时间过长,也就导致调用该应用接口的其它应用超时。这种情况比较偶发,一天大概几次,没什么特别规律。

 

应用日志中只能大概看出,出现问题时该应用查询不了数据库,与数据库的连接丢失,没有太多其它的信息:

2017-06-05 23:23:44,941 INFO  (?:?) - Get a connection...

2017-06-05 23:39:11,670 INFO  (?:?) - SearchPoList() has a Exception: java.sql.SQLException: ORA-03135: connection lost contact

2017-06-05 23:39:11,670 WARN  (HouseKeeper.java:149) - #0001 was active for 324896 milliseconds and has been removed automaticaly. The Thread responsible was named 'http-8086-Processor24', but the last SQL it performed is unknown because the trace property is not enabled.

2017-06-05 23:39:11,671 INFO  (?:?) - Connection closed...

2017-06-05 23:39:11,676 WARN  (RequestProcessor.java:516) - Unhandled Exception thrown: class java.sql.SQLException

2017-06-05 23:39:11,679 ERROR (StandardWrapperValve.java:253) - Servlet.service() for servlet action threw exception

java.sql.SQLException: ORA-03135: connection lost contact

 

       at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:112)

       at oracle.jdbc.driver.T2CConnection.checkError(T2CConnection.java:672)

       at oracle.jdbc.driver.T2CConnection.checkError(T2CConnection.java:598)

       at oracle.jdbc.driver.T2CPreparedStatement.executeForDescribe(T2CPreparedStatement.java:571)

       at oracle.jdbc.driver.OracleStatement.executeMaybeDescribe(OracleStatement.java:1038)

       at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1133)

       at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3285)

       at oracle.jdbc.driver.OraclePreparedStatement.executeQuery(OraclePreparedStatement.java:3329)

       at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

       at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)

       at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

       at java.lang.reflect.Method.invoke(Method.java:585)

       at org.logicalcobwebs.proxool.ProxyStatement.invoke(ProxyStatement.java:100)

       at org.logicalcobwebs.proxool.ProxyStatement.intercept(ProxyStatement.java:57)

       at oracle.jdbc.OracleStatement$$EnhancerByProxool$$e42bfc21.executeQuery(<generated>)

       at com.sssoft.framework.db.DefaultDbManager.executeQuery(Unknown Source)

 

虽然说已经确定是应用连接不上数据库导致的故障,但,为什么该应用会连接不上数据库并且还是偶发性的呢?关于这个问题,开发和DBA等人一同排查了好几天,竟然没有任何头绪,最后找到了我。而我,作为一个懂网络、操作系统、数据库(虽然是MySQL)和略懂程序的,具备全方面知识体系(稍微自夸下,但后面你就会知道,的确涉及到了各方面的知识)的运维工程师,自然不能放过这么有趣的问题了。好吧,其实是因为这个应用当初是我部署上线的,我怎么能允许我自己部署的应用出现问题呢。况且,其它连接同一个数据库的应用也没说过有问题啊,然而,事实证明,这个问题涉及到的水比较深。

 

出现连接不上数据库的那个应用是Java程序来的,它使用了proxool连接池来连接数据库。而数据库本身是Oracle数据库。

 


问题初步排查

 

这个问题排查起来可能不是很容易有思路。当然,作为一个系统运维工程师,我还是习惯于从操作系统层面开始排查起。由于问题是偶发性的,想要复现并不容易。但既然是应用连接不上数据库,那么可以观察下网络连接。

 

经过观察,我发现,在应用启动后的一段时间后,在应用服务器上大概有10多个与oracle数据库服务器的tcp连接:

但同一时刻,在Oracle数据库服务器上,却只能看到2个与应用服务器的tcp连接:

 

由于应用采用数据库连接池来连接Oracle数据库,也就是说,应用与数据库之间的连接是tcp长连接。那么正常情况下,在任一时刻,应用与数据库服务器两端的ESTABLISHED状态的tcp连接数量应该都是一致的。然而,我实际观察到的情况,正如上面所示,却不是这样。经过更加细致的观察,可以发现,一开始,在该应用刚刚启动时,两端的tcp连接数量是相同的。但是,过了一段时间后,oracle数据库服务器端与应用服务器的tcp连接数量开始减少,甚至出现为0的现象。这时就出现了我上面所观察到的奇异现象了。

两端的ESTABLISHED状态的tcp连接数量不一致是不是一定就有问题的呢?答案是肯定的。因为tcp是面向连接的协议,连接的两端只有通过"三次握手"正常建立了tcp连接后,它们才能正式通过该tcp连接开始交互数据。如果其中一端不存在该tcp连接的信息,那么即便另一端通过该tcp连接发送带应用数据的数据包过来了,不存在tcp连接信息的这一端收到数据包后也会直接将该数据包丢弃,从而导致两端无法正常交互数据。因此,问题就出在这了,正是因为应用和数据库服务器两端的tcp连接不对称,才导致了应用无法正常访问数据库。

 

但是,为什么会出现这种两端的tcp连接数不对称的情况呢?为了方便排查故障,我另外克隆了一台应用服务器。这台新的应用服务器只有我在使用,当我不访问上面的应用时,上面的应用也就不会访问数据库,这样便于排查。这台新的应用服务器的IP是x.x.x.125,所连接的数据库服务器的IP是y.y.y.27。当经过一段时间后,这台应用服务器与数据库服务器各自上面的tcp连接数也开始出现不对称了。在应用服务器和数据库服务器上使用tcpdump抓包,再下载下来使用wireshark打开,可以发现下面三个比较怪异的现象:

 

1、 在应用刚启动时,保持应用空闲(即我不去访问应用,也就不会触发应用去访问数据库),此时抓包可以发现,虽然应用和数据库之间已正常建立了连接,但它们之间却没有任何数据包交互。

 

2、 当应用启动后经过一段时间后,数据库服务器端查看到的tcp连接数开始下降。如果我不去访问应用的话,数据库服务器端的tcp连接数甚至会降为0。应用服务器端的tcp连接数仍然保持跟应用刚启动时一样,有10多个。此时,我通过网页来访问应用,从而触发应用去访问数据库,我对这个过程进行抓包:

上面是我在应用服务器上的抓包结果。可以发现,应用服务器125的确有去往数据库服务器27的数据包,但却没有来自数据库服务器27的回包。所以应用服务器125一直在进行tcp重传(Retransmission)。此时,在数据库服务器27上面抓包,也的确没有抓到来自应用服务器125的数据包。这说明,因为某种原因,从应用服务器125发出的数据包在网络中丢失了。具体是什么原因,现在还无法确定。

 

3、 当数据库服务器端的tcp连接数开始下降时,此时在数据库服务器端抓包:

可以发现,数据库服务器27尝试发送Keep-Alive包给应用服务器125,但并没有收到应用服务器125的回包。数据库服务器27在后面还发送了一个RST包重置了y.y.y.27:1521  x.x.x.125:46343这个tcp连接,这直接导致了数据库服务器上的这个tcp连接被清理掉。同一时刻,在应用服务器125也没有抓到来自数据库服务器27的包。这也说明,因为某种原因,从数据库服务器27发出的数据包在网络中丢失了。

此外,也还有很多疑问尚待解决。比如,那个Keep-Alive包是干什么的,为什么会有Keep-Alive包?又为什么会有RST重置包?

 

目前还没有明显的解决问题的思路,但我想,如果能搞清楚以上三个怪异现象/疑点,搞清楚哪些是正常的哪些是异常的,应该可以定位到出现异常的哪一部分(源端、目标端、网络还是数据库),从而帮助解决问题。

 


应用的网络架构

 

既然涉及到了数据包在网络中丢失,那么,就有必要介绍一下应用服务器与数据库服务器之间的网络架构。

 

网络拓扑图可简化如下:

简单来说,应用服务器(x.x.x.125)与数据库服务器(y.y.y.27)分属不同网段,中间有台防火墙,各个网段的网关(x.x.x.254y.y.y.254)也位于防火墙上。虽然中间还有很多二层交换机、三层交换机等,但对问题的产生没什么影响,所有就省略不表。

 

按理说,数据包在网络中丢失了,那么,就应该找网络工程师一起排查下是什么问题。但无奈,我司的网络工程师水平太低,对网络底层原理、概念等的掌握还不如我(这并不是我自夸自擂)。简单来说,我从网络工程师那里没有获取到任何有价值的信息,他们也不认为网络本身有什么问题,也不愿意协助排查,我也没有权限看不了防火墙,所以我只能自己想办法排查了。

 


tcp连接的保持

 

前面我提到,在抓包时,我发现了3个怪异的现象。其中,第一个现象是,在应用刚启动,应用空闲时(即应用不主动去访问数据库),虽然应用和数据库之间已正常建立了连接,在应用和数据库服务器两端均可看到等量的并对称的ESTABLISHED状态的连接,但抓包却发现它们之间没有传递任何数据包。

 

关于这个问题,我查阅了<<TCP/IP Illustrated, Volume 1: The Protocols>>。其中,TCP Keepalive这章里面有提到:

Many newcomers to TCP/IP are surprised to learn that no data whatsoever flows across an idle TCP connection. That is, if neither process at the ends of a TCP connection is sending data to the other, nothing is exchanged between the two TCP endpoints. There is no polling, for example, as you might find with other networking protocols. This means that we can start a client process that establishes a TCP connection with a server and walk away for hours, days, weeks, or months, and the connection should remain up. In theory, intermediate routers can crash and reboot, data lines may go down and back up, but as long as neither host at the ends of the connection reboots (or changes its IP address), the connection remains established. This is how TCP/IP was designed.

上面这段话的意思是:空闲的TCP连接是没有数据流动的,也就是说,如果TCP连接的两端如果都不发送数据给对方,就没有任何东西在它们之间交互;也不会有定时的数据包轮询;当一个客户端和一台服务器建立TCP连接之后,若服务器离开几个小时、几天或几个月,连接也都还在的,即便中间的路由器和线路挂了、重启了或断了,但只要TCP连接两端的主机都没有重启(或更改它的IP地址),连接会一直保持ESTABLISHED状态;TCP/IP就是这么设计的。

 

也就是说,我观察到的第一个现象(应用空闲时,虽然应用服务器和数据库服务器之间建立了ESTABLISHED状态的连接,但却没有数据包交互),该现象是正常的。TCP/IP就是这么设计的。

但是,在该章的其它地方,它说:存在一种TCP keepalive机制,但它是可选的。TCP keepalive特性并不是TCP规范的一部分(因为可能会有误判及产生额外的流量等),然而,大多数TCP实现都提供了keepalive能力。即便如此,TCP keepalive功能默认也可能是关闭的。

 

TCP keepalive功能可以在TCP连接的其中一端,或两端启用,或两端都不启用。它的运作机制是这样的:如果TCP连接上有一段时间(叫做keepalive time)没有活动,那么,启用了keepalive的一端就会发送存活探测数据包给它的对端。如果没有收到任何响应数据包,该探测包就会周期性地(周期间隔为keepalive interval)重复发送,直到探测次数达到keepalive probes的设置值。当发生这种情况时,对端的系统将被判定为是不可恢复的并且该TCP连接会被终止。

变量keepalive time、keepalive interval和keepalive probes的值通常是可以修改的。一些系统允许在per-connection的基础上进行这些修改,另一些系统则只能在全局范围修改(或可能两种都支持)。在Linux中,这些值可以通过sysctl系统参数来相应修改:net.ipv4.tcp_keepalive_time、net.ipv4.tcp_keepalive_intvl和net.ipv4.tcp_keepalive_probes。默认值分别是7200 (seconds,或2 hours)、75 (seconds)和9 (probes)。

Linux并未提供一个原生的工具来为那些未请求启用TCP keepalive的应用强制开启TCP keepalive功能。换句话说,虽然Linux支持TCP keepalive特性,也可以调整相关的定时器设置,但某一个TCP连接究竟是否启用TCP keepalive特性,由上层的应用程序说了算(需要应用程序调用系统的API),Linux本身并没有工具/设置项来让你为所有TCP连接都强制开启TCP keepalive特性。

 

1)    回到问题本身,应用服务器是RHEL系统,属于Linux的一种,自然也支持上面提到的三个属性:

[root@gw ~]# sysctl -a | grep 'keepalive'

net.ipv4.tcp_keepalive_time = 7200

net.ipv4.tcp_keepalive_probes = 9

net.ipv4.tcp_keepalive_intvl = 75

各参数含义:

  • net.ipv4.tcp_keepalive_time:如果TCP连接启用了TCP keepalive特性,那么,当TCP连接空闲时间达到该项设定值时,服务器就会发送存活探测数据包给它的对端。默认值是7200 (seconds,或2 hours)。

  • net.ipv4.tcp_keepalive_probes:发送存活探测数据包后,若未收到来自对端的响应数据包,服务器会尝试重新发送探测数据包,直到总的探测次数达到该项设定值。默认值是9 (probes)次。若最后一次探测时仍未收到对端的响应数据包,服务器会清除掉自己本端的该TCP连接。

  • net.ipv4.tcp_keepalive_intvl:每次发送探测数据包的间隔时间。默认值是75 (seconds)。那么来说,从开始探测到最后清除TCP连接,总共需要经过tcp_keepalive_intvl * tcp_keepalive_probes的时间,若以默认值计算,为75 * 9 = 675秒,约为11分钟多。

 

即便如此,我又怎么知道应用服务器去往数据库服务器的TCP连接是否启用了TCP keepalive呢?毕竟,我无法知道应用程序是否有要求启用TCP keepalive。其实,通过netstat命令的 -o 选项可以查看到,在应用服务器上执行以下命令:

最后一列都是off的,说明在应用服务器125这端,所有与数据库服务器27的TCP连接都是没有启用TCP keepalive特性的。如果有启用的话,那么最后一列会显示keepalive的字样。

 

2)    数据库服务器是AIX系统,它也有一些类似的属性:

bash-4.2# no -a | grep 'tcp_keep'

tcp_keepcnt = 8

tcp_keepidle = 14400

tcp_keepinit = 150

tcp_keepintvl = 150

各参数含义:

  • tcp_keepcnt:等同于Linux的net.ipv4.tcp_keepalive_probes参数。默认值是8次。

  • tcp_keepidle:等同于Linux的net.ipv4.tcp_keepalive_time参数。单位为0.5秒,默认值是14400(即7200秒或2小时)。

  • tcp_keepinit:TCP连接的初始超时时间。AIX的官方文档中是这么说的,但没详细解释。单位为0.5秒,默认值是150(即75秒)。

  • tcp_keepintvl:等同于Linux的net.ipv4.tcp_keepalive_intvl参数。单位为0.5秒,默认值是150(即75秒)。那么来说,从开始探测到最后清除TCP连接,总共需要经过tcp_keepintvl * tcp_keepcnt的时间,若以默认值计算,为75 * 8 = 600秒,即10分钟。

 

在AIX上,也可以使用netstat命令的 -o选项,但需结合awk来处理。想要查看ipv4协议的与某一台服务器(假设为x.x.x.x)的TCP连接是否启用了TCP keepalive特性,可以使用下面的命令,只需要把x.x.x.x替换为相应的IP就行:

netstat -nao -f inet | awk '{ p2line=pline; pline=cline; cline=$0; if(p2line ~ /x.x.x.x/) {print p2line cline}; }'

 

# 解释:变量p2line存储前2行(不包括前1行)的内容,变量pline存储前1行的内容,变量cline存储当前行的内容。当

# p2line的内容匹配到指定IP时,就输出p2line和cline的内容。

命令输出结果如下:

可以看到,在数据库服务器27这端,所有与应用服务器125的TCP连接都有启用TCP keepalive特性。可以看到KEEPALIVE字样。

我并没有查到有任何AIX官方资料说,AIX会强制为TCP连接启用TCP keepalive特性,所以建议还是以实际观察为准。

 

经过上述的分析,现在我们可以解答之前观测到的一些现象了。为什么应用启动后经过一段时间后,在数据库服务器端观察到的TCP连接会逐渐减少了,以及,在抓包观察到的第3点里,为什么会出现Keep-Alive包和RST包以及它们的作用了:

总的来说,因为应用服务器与数据库服务器之间的某些TCP连接在建立后的2个小时内(tcp_keepidle值)都没有数据包交互,并且在AIX数据库服务器这端,它与应用服务器125的TCP连接都有启用TCP keepalive特性,所以AIX服务器会尝试发送Keep-Alive包以确定这些空闲的TCP连接是否仍然可用,由于它最终都没有收到对端的回包,所以它就发送了RST包来断开该TCP连接并清除掉本地系统中的该TCP连接的信息。最终就表现为,在数据库服务器端观察到与应用服务器的TCP连接逐渐减少了。

 


防火墙的会话特性

 

经过前面一大堆的分析,我们尚不知道为什么数据包会在网络中丢失呢。根据我的理论知识及过往的经验,我猜想问题应该出在防火墙。虽然我没有权限看不了防火墙的配置,但我知道防火墙是有"会话"这一概念的。

(这部分我没有严格的查询官方文档,但大体应该是没错的)在防火墙中,防火墙会通过五元组来标识一个连接会话,即源IP地址、源端口、目的IP地址、目的端口、协议(tcp或udp)。例如,172.16.1.1 36524 TCP 192.168.1.1 1521就构成了一个五元组,其含义是,IP地址为172.16.1.1的源主机使用源端口36524,使用TCP协议,访问IP地址为192.168.1.1的目标主机的1521端口。但是呢,防火墙中是有会话过期时间的。当一个TCP连接长时间没有数据包交互,防火墙将会将该TCP连接会话清除。会话信息被清除后,当该TCP连接的源端或目标端再发送数据包过来时,防火墙收到后,会直接将该数据包丢弃。除非重新建立TCP连接(经历TCP 3次握手),否则,属于该五元组的TCP连接的数据包将不会被放行。

 

这就解释了现象2和3中为什么数据包会在网络中丢失。当应用服务器或数据库服务器发送数据包时,由于此时防火墙中相应的TCP连接会话已经过期,防火墙收到数据包后就直接把包丢弃了,所以对端就无法收到数据包,它们本身也就无法收到回包。

 

后来查询得知,那个防火墙的会话超时时间是1小时。我后来经过实验证实应该是没错的。那么,整个问题就是这样的:当应用服务器与数据库服务器建立TCP连接之后,某些TCP连接由于长时间没有数据包交互,达到防火墙的会话超时时间1小时,故防火墙清除了这些未活动的TCP连接的会话信息;此时,不管是应用服务器还是数据库服务器通过这些TCP连接来访问对端都是不通的,这就导致了最开始应用服务器访问不了数据库服务器的现象;又由于数据库服务器端对这些TCP连接启用了TCP keepalive特性,那么当时间达到2小时(即防火墙清除会话信息后又过了1小时)后,数据库服务器开始发送Keep-Alive包检测这些空闲的TCP连接是否仍然可用,由于这些连接早在1小时之前就已经不通了,在经过多次尝试后仍然未收到对端的回包,数据库服务器便清除了这些不可用的TCP连接,这就出现了最开始观察到的数据库服务器上TCP连接不断减少的现象。

 

简单来说就是,由于应用与数据库长时间没有流量交互,导致防火墙中的会话超时,就导致了应用访问不了数据库。

 


正确地配置proxool.xml

 

虽然已经找出了问题的根本原因,但这个问题怎么解决呢?有几种解决办法:调整防火墙会话超时时间大于2小时(AIX数据库服务器的tcp_keepidle值);调整AIX数据库服务器的tcp_keepidle值小于1小时(防火墙会话超时时间);调整应用程序。当然,各有优缺啦。

 

其实,在我刚开始发现应用和数据库服务器的TCP连接不对等时,我告知了开发人员,开发同事就尝试修改了应用的数据库连接池配置。结果误打误撞,问题就没有再发生了。当然,他并不清楚为什么会这样。但是,这引出了我这里要讲的一个问题。

 

应用程序使用的是proxool连接池,在最开始,它的配置文件proxool.xml的内容类似于:

<?xml version="1.0" encoding="UTF-8"?>

<something-else-entirely>

       <proxool>

               <alias>defaultDatabase</alias>

               <driver-url>jdbc:oracle:oci8:@exampledb</driver-url>

               <driver-class>oracle.jdbc.driver.OracleDriver</driver-class>

               <driver-properties>

                       <property name="user" value="example_user" />

                       <property name="password" value="example_password" />

               </driver-properties>

               <prototype-count>3</prototype-count>

               <maximum-connection-count>50</maximum-connection-count>

               <minimum-connection-count>4</minimum-connection-count>

       </proxool>

</something-else-entirely>

然后呢,开发人员加了下面两条语句(红色字体的):

<?xml version="1.0" encoding="UTF-8"?>

<something-else-entirely>

       <proxool>

               .  .  .  .  .  .

               <test-before-use>true</test-before-use>

               <house-keeping-test-sql>SELECT 'x' from dual</house-keeping-test-sql>

       </proxool>

</something-else-entirely>

这两条语句呢,其实真正解决了问题的是house-keeping-test-sql这条语句,test-before-use语句则是没有必要的。加上test-before-use语句除了会增加查询数据库所消耗的时间之外,一般来说,真的不会起什么有效的作用。

 

网络上很多文章经常犯的一个错误就是,使用了house-keeping-test-sql语句就会同时使用test-before-use语句,认为前者需要跟后者一同使用才能生效,直观上也很容易搞错。实际上,house-keeping-test-sql语句可以单独使用。使用了test-before-use语句就一定要使用house-keeping-test-sql语句,但反过来就不对了。结合proxool官网中的文档,这两条语句的实际作用为:

  • house-keeping-test-sql语句:当设置该项时,house keeping thread会找出数据库连接池中空闲的TCP连接并逐一地通过这些连接来执行该项指定的SQL语句,根据执行成功与否来确定该连接是否仍然可用。该操作每隔一段时间就会重新进行一次,周期为house keeping thread的睡眠时间间隔(house-keeping-sleep-time属性的值,默认为30秒)。为了减少开销,该SQL语句应该是执行起来非常快速的。当未设置该项时,该检测操作会被省略。

    house keeping thread的检测操作本身可以判定TCP连接是否仍然可用。但更重要的是,如果应用与数据库之间有防火墙,定时的检测可以产生定时的数据包交互,刷新防火墙中该连接的会话的定时器,保证该会话不会超时而被清除。为了起到该作用,house-keeping-sleep-time的值需要小于防火墙会话超时时间(我这里是1小时),但为了避免产生过多网络流量,也为了避免对数据库产生不必要的压力,也不建议设置为太小。可以设置为10分钟或20分钟检测一次之类的。

    另外还需特别说明,这种定时检测操作是属于应用层面的实现,跟之前所说的TCP keepalive特性没有任何关系。

  • test-before-use语句:当设置该项为true时,应用在使用数据库连接池中的某一个连接之前,会先使用house-keeping-test-sql语句中定义的SQL语句来检测该连接的可用性。如果检测失败了,就会选择另外一个连接。如果所有连接都检测失败了,就会创建一个新的连接。如果那个新连接也检测失败了,就会返回SQLException信息。

    可以看到,该语句是依赖于house-keeping-test-sql语句的,反过来则不对。即便test-before-use语句的值被设为false(默认值),house-keeping-test-sql语句也仍然生效,house keeping thread仍然会定期执行house-keeping-test-sql语句中定义的SQL语句(如果有设置的话)。使用test-before-use语句虽可以最大限度保证连接可用性,但会产生额外的延迟(至少是应用服务器到数据库服务器的网络round trip time),而且,程序需要多久才能判定一个连接是不可用的,这也是一个很重要的问题的。因此,我个人是不推荐使用该语句的。

 

总的来说,我建议在设置proxool连接池至少加上下面两条语句(红色字体的):

<?xml version="1.0" encoding="UTF-8"?>

<something-else-entirely>

       <proxool>

               <alias>defaultDatabase</alias>

               <driver-url>jdbc:oracle:oci8:@exampledb</driver-url>

               <driver-class>oracle.jdbc.driver.OracleDriver</driver-class>

               <driver-properties>

                       <property name="user" value="example_user" />

                       <property name="password" value="example_password" />

               </driver-properties>

               <prototype-count>3</prototype-count>

               <maximum-connection-count>50</maximum-connection-count>

               <minimum-connection-count>4</minimum-connection-count>

               <house-keeping-test-sql>SELECT 'x' from dual</house-keeping-test-sql>

               <house-keeping-sleep-time>600000</house-keeping-sleep-time>

       </proxool>

</something-else-entirely>

house-keeping-test-sql语句可以让house keeping thread定期地使用SQL语句检测连接池中空闲的TCP连接,house-keeping-sleep-time语句则设置这个时间间隔(单位为毫秒),不设置时默认为30秒,建议不要设得太大或太小,但要小于硬件防火墙的会话超时时间。

 


问题总结

 

到这里为止,问题已经很清楚了。这种问题的发生其实并不局限于访问Oracle数据库的情况,只要是长连接,当满足以下所有条件时,这种问题就会发生:

1、 两台服务器之间存在有硬件防火墙。

2、 使用了TCP长连接,比如在使用TCP连接池的场景中。

3、 在TCP连接的两端均未启用TCP keeplive特性,或任意一端或两端启用了TCP keeplive特性,但空闲检测时间(如Linux的net.ipv4.tcp_keepalive_time参数,AIX的tcp_keepidle参数)大于硬件防火墙的会话超时时间。是否有启用TCP keeplive特性通常由上层应用程序决定,但可以通过命令来观察。

4、 不存在应用层的定时轮询,或存在应用层的定时轮询,但轮询间隔大于硬件防火墙的会话超时时间。是否存在应用层的定时轮询通常要根据具体的应用程序及配置(如数据库连接池的配置)而定。

5、 在硬件防火墙的会话超时时间内,长连接中没有数据包传输。通常,如果应用本身没什么访问量,或者是在凌晨等时段访问量比较低,又或者是长连接数量太多,都有可能使某些长连接一直空闲,长时间没有数据包传输。

 


其它问题

 

1、 有时,我们会想,客户端的数据库连接池中有很多与数据库服务器端的连接,假设其中仍然有一部分连接是仍然可以访问到数据库的,那么这时候应用访问数据库会不会有问题呢?

 

仍然是可能有问题的。根据我的观察,当应用访问数据库时,应用可能是从连接池中随机选取一个连接的(具体的选取规则我不是很确定),但重点是,在没有做检测的情况下,应用并不知道连接池中的哪些连接是实际可用的。如果应用选取到的是一个实际不可用的连接,那么这时,由于数据包发送出去了但没有回包,(至少在Linux上)操作系统会一直尝试重传并超过最大重试次数(正如前面所说),这个过程至少也要10分钟以上,而应用层通常早就超时报错了。当确定了连接不可用之后,应用会再从连接池中选取一个连接。若这个连接仍然不可用,应用才会尝试创建一个新连接。整个过程下来,可能要20多分钟,而应用层通常都不会等待这么久的时间。

当然,如果应用最开始从连接池中选取到的那个连接恰好仍然可用,此时应用访问数据库是没有问题的。因此,总的来说,这种情况就是看概率啦。不过,通常,出问题的概率比较大。

 

2、 本文讲的是与Oracle数据库之间的问题,如果换成MySQL数据库,会是什么情况呢?

MySQL数据库有一点不同,据我观察,应用通过MySQL驱动与MySQL数据库建立长连接时,应用这一端是启用了TCP keepalive特性的。可能并不是所有MySQL驱动都是这样的,但假设是这样的话,那么,问题的发生会变得更加隐晦。其实原理都是一样的,套用原理分析一下就行,我就不啰嗦了。