问题描述
最近遇到几个生产环境的系统连接psql数据库超时的问题,系统在刚启动后是可以正常访问的,但过一段时间后系统不能访问,系统日志报如下错误。
2020-02-12 15:36:11 [ERROR] [http-nio-8081-exec-2] [] jdbc.audit - 60.
com.PSQLException: An I/O error occurred while sending to the backend.
at
Caused by: java.net.SocketException: 连接超时 (Read failed)
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
......
问题分析
经查,原来防火墙有一个TCP超时时间,默认设置的为半小时,其意义是,对于通过防火墙的所有TCP连接,如果在半小时内没有任何活动,就会被防火墙拆除,这样就会导致连接中断。在拆除连接时,也不会向连接的两端发送任何数据来通知连接已经拆除。
这下数据库连接断开的原因找到了,那么这就是一个应用与数据库在不同的网络中,连接需要经过防火墙的场景中会遇到的一个典型问题,怎么能够使应用和数据库之间即使比较空闲也能够保持一定数量的长连接,是亟待解决的。
解决方案
解决的思路很简单,就是定时保持连接通信,防止连接超时。
方案一: 连接池参数配置
连接池有一些参数可以配置心跳,下面以druid为例。其他连接池也有类似参数可以配置。
Druid 数据库连接池提供一些参数,testWhileIdle设置为true后,间隔timeBetweenEvictionRunsMillis配置的时间,就执行一下validationQuery参数配置的sql,保持连接可用,防止超时中断。
参数 | 默认值 | 注解 |
---|---|---|
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 有两个含义: 1) Destroy线程会检测连接的间隔时间2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 | |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 | |
minEvictableIdleTimeMillis | 连接保持空闲而不被驱逐的最长时间 | |
keepAlive(仅DRUID有) | false | 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。 |
现场连接池配置如下
<property name="validationQuery" value="select 1" />
<property name="testWhileIdle" value="true" />
<property name="timeBetweenEvictionRunsMillis" value="120000" />
<property name="minEvictableIdleTimeMillis" value="600000" />
<property name="keepAlive" value="true" />
方案二:操作系统的tcp keepalive功能
centos系统提供keepalive功能,但默认是关闭的,需要jdbc连接增加参数tcpKeepAlive=true设置来启用。
tcp的keepalive,其实就是用来保持tcp连接的,其原理简单说就是如果一个TCP连接在指定的时间内没有任何活动,会发送一个探测包到连接的对端,检测连接的对端是否仍然存在,如果对端一定时间内仍没有对探测的响应,会再次发送探测包,发送几次后,仍然没有响应,就认为连接已经失效,关闭本地连接。
当设置了tcp keepalive之后,只要tcp探测包发送的时间小于防火墙的连接超时时间,防火墙就会检查到连接中仍然有数据传输,就不会断开这个连接。
参数 | 默认值 | 注解 |
---|---|---|
net.ipv4.tcp_keepalive_time | 7200 | 在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h) |
net.ipv4.tcp_keepalive_intvl | 75 | 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s |
net.ipv4.tcp_keepalive_probes | 9 | 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次) |
将linux系统参数net.ipv4.tcp_keepalive_time设置为600,在连接串中增加参数tcpKeepAlive=true,重启应用系统。使用命令netstat -ano查看keepAlive信息
tcp 0 0 172.16.57.206:38340 172.16.33.55:5432 ESTABLISHED keepalive (578.08/0/0)
tcp 0 0 172.16.57.206:54715 172.16.33.33:5432 ESTABLISHED keepalive (588.82/0/0)
tcp 0 0 172.16.57.206:49328 172.16.33.32:5432 ESTABLISHED keepalive (572.05/0/0)
tcp 0 0 172.16.57.206:41702 172.16.32.240:5432 ESTABLISHED keepalive (573.13/0/0)
tcp 0 0 172.16.57.206:49334 172.16.33.32:5432 ESTABLISHED keepalive (577.09/0/0)
方案三:数据库参数
psql数据库提供了类似linux系统tcpKeepAlive的参数,数据库的keepAlive是默认开启的,但默认时间与操作系统默认时间一样(2小时)。数据库检测每个连接是否active,如果检测失败则回收该进程,打印如下日志。我们可以通过数据库日志中是否存在如下错误,来判断应用连接是否有异常断开。
2020-02-12 19:25:24.166 CST [10125] LOG: could not receive data from client: Connection timed out
2020-02-12 20:01:35.031 CST [10375] LOG: could not receive data from client: Connection timed out
经验证,生产环境验证设置tcp_keepalives_idle=1200,系统能够正常运行,连接不再中断。
参数 | 默认值 | 注解 |
---|---|---|
tcp_keepalives_idle | 7200 | 指定不活动多少秒之后通过 TCP 向客户端发送一个 keepalive 消息。 0 值表示使用默认值。这个参数只有在支持TCP_KEEPIDLE或TCP_KEEPALIVE符号的系统或 Windows 上才可以使用。在其他系统上,它必须为零。在通过 Unix 域套接字连接的会话中,这个参数被忽略并且总是读作零。 |
tcp_keepalives_interval | 75 | 指定在多少秒之后重发一个还没有被客户端告知已收到的 TCP keepalive 消息。0 值表示使用系统默认值。这个参数只有在支持TCP_KEEPINTVL符号的系统或 Windows 上才可以使用。在其他系统上,必须为零。在通过 Unix域套接字连接的会话中,这个参数被忽略并总被读作零。 |
tcp_keepalives_count | 9 | 指定与客户端的服务器连接被认为死掉之前允许丢失的 TCP keepalive 数量。0 值表示使用系统默认值。这个参数只有在支持TCP_KEEPCNT符号的系统上才可以使用。在其他系统上,必须为零。在通过 Unix 域套接字连接的会话中,这个参数被忽略并总被读作零。 |
总结
- 修改连接池参数只作用于单个应用,数据库日志、程序日志会记录大量无效sql。
- 修改操作系统参数,需要同时修改每个应用的jdbcurl。
- 修改数据库参数,可作用于所有连接该数据库连接,包括navicat、psql等连接。非常方便,但只部分数据库支持。