池式结构-连接池
分布式协调系统如zookeeper、etcd,客户端都是使用一个长连接。记得之前使用k8s客户端(核心是etcd)时候,需要自己实现close()接口,我在close接口里判断如果连接断了...
分布式协调系统如zookeeper、etcd,客户端都是使用一个长连接。记得之前使用k8s客户端(核心是etcd)时候,需要自己实现close()接口,我在close接口里判断如果连接断了,就重新连接。zookeeper的客户端实现也是大同小异。
但是像数据库连接、MQ连接这种,更常见的实现方式是连接池。本文采用【情境型】逻辑结构来介绍连接池的能力和使用注意事项。
能力-为什么要使用连接池
本文开头提到zookeeper和etcd客户端使用的是单一长连接,而不是连接池。咱们举两个典型场景就能明白这些大牛设计者的苦心。
zookeeper的典型使用场景是服务治理。比如使用dubbo这种服务治理框架,全公司的服务都在向zookeeper读写数据。如果zookeeper的客户端是开放出来的连接池,那zookeeper服务端要维持的连接数就是:全公司的服务数*连接池大小。ulimit(linux系统命令用来查询文件句柄数)数很快就不够用了。
etcd的典型使用场景是k8s。k8s一般用来管理整个公司的服务器集群。同理,全公司的服务器都在向etcd读写数据。如果etcd的客户端是开放出来的连接池,那etcd服务端要维持的连接数就是:全公司的服务器数*连接池大小。ulimit数会更快不够用的。
zookeeper和etcd都是主备类服务,只有一个主服务在提供写能力,不可以横向扩容。所以这种情况下,客户端连接使用连接池,那简直就是连接上的灾难。而由于处理的发布订阅数据传输本身就很小,使用连接池简直是:琉璃瓦盖鸡窝——大材小用。
但是像数据库实例这样,使用方是固定的,很多公司的生产环境一个数据库实例是一个团队单独使用的。使用方数量固定,连接的机器几千台顶天了。而且很多业务逻辑就是CURD,面向数据库编程,传输信息量很大,就不得不使用连接池了。
数据库连接池是个相对复杂的处理,一些开源大牛也会很容易写出来bug。下面咱们通过具体情境来了解一下数据库连接池使用的要点。
情境1-druid 1.0.29版本连接失效bug
上图展示了客户端通过连接池连接服务器的过程。咱们平时使用druid连接池要配置很多的参数,这些参数一部分是控制伸缩性的参数:最小连接数、最大连接数等。另外一部分是探活参数,如:空闲连接触发检查阈值
timeBetweenEvictionRunsMillis
连接池管理的复杂,从上图可以管中窥豹-可见一斑。一个线程专门用来创建和管理连接,一个专门负责取连接。druid 1.0.29版本连接失效bug就发生在图中红框的连接是否可用部分。
DruidDataSource中有个检查连接是否有效的方法:
public void validateConnection(Connection conn) throws SQLException {
String query = this.getValidationQuery();
if (conn.isClosed()) {
throw new SQLException("validateConnection: connection closed");
} else if (this.validConnectionChecker != null) {
boolean result = true;
Exception error = null;
try {
result = this.validConnectionChecker.isValidConnection(conn, this.validationQuery, this.validationQueryTimeout);
} catch (Exception var9) {
error = var9;
}
if (!result) {
SQLException sqlError = error != null ? new SQLException("validateConnection false", error) : new SQLException("validateConnection false");
throw sqlError;
}
} else {
if (null != query) {
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
if (this.getValidationQueryTimeout() > 0) {
stmt.setQueryTimeout(this.getValidationQueryTimeout());
}
rs = stmt.executeQuery(query);
if (!rs.next()) {
throw new SQLException("validationQuery didn't return a row");
}
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(stmt);
}
}
}
}
注意上面代码第6行开始有个bug,result初始为true,检查连接是否异常,抛出异常直接捕获,result还是true,意思是校验没有问题。在服务端重启等场景下,这个检查会抛出socket相关异常。实际上服务端已经断开连接了。这时不会进行连接的销毁重建。造成使用了已经断开的连接来操作数据库,导致执行失败。
这是生产环境真实发生过的案例。这个bug是个低级错误,后续版本已经修复了。建议不要用这个有坑版本。
另外,建议常常做业务开发、使用数据库连接池的朋友结合下面常用参数参考看几遍DruidDataSource的源码。以便更好的了解参数的使用。
情境2-ActiveMQ未使用连接池连挂服务端
activeMQ有个是否使用连接池的配置:
spring.activemq.pool.enabled=false
这里false代表不使用连接池,不使用连接池在activeMQ里表达的意思是:每发送一条数据创建一个连接。于是有个同学进行了这样的配置以后,服务端出现大量的time_wait。time_wait表示服务端和客户端已经在进行四次挥手断开连接中了。
keepalive探活会探测到连接不可用就会进行这个断开连接操作。断连操作有一步很耗时,就是time_wait,因为它有2MSL的等待时间。很多连接的最终释放都卡在这里了。
老连接不释放,服务器资源一直占用。新连接建立就会失败,表现出来就是服务器挂了。连接参数配置需谨慎:
#true表示使用连接池
spring.activemq.pool.enabled=true
#连接池最大连接数
spring.activemq.pool.max-connections=5
#空闲的连接过期时间,默认为30秒
spring.activemq.pool.idle-timeout=30000
#强制的连接过期时间,与idleTimeout的区别在于:idleTimeout是在连接空闲一段时间失效,而expiryTimeout不管当前连接的情况,只要达到指定时间就失效。默认为0,never
spring.activemq.pool.expiry-timeout=0
MSL解释
最后解释一下time_wait的2MSL。MSL是什么:
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即发送了第四次挥手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次挥手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。
参考文章:
https://blog.csdn.net/xiaofei0859/article/details/6044694
更多推荐
所有评论(0)