池式结构-连接池

分布式协调系统如zookeeper、etcd,客户端都是使用一个长连接。记得之前使用k8s客户端(核心是etcd)时候,需要自己实现close()接口,我在close接口里判断如果连接断了,就重新连接。zookeeper的客户端实现也是大同小异。

但是像数据库连接、MQ连接这种,更常见的实现方式是连接池。本文采用【情境型】逻辑结构来介绍连接池的能力和使用注意事项。

12d727d14618ec2f418b0d437fe3218b.png

能力-为什么要使用连接池

本文开头提到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

cb6ab731538b6adb6354bc5ef7a5de6d.png

上图展示了客户端通过连接池连接服务器的过程。咱们平时使用druid连接池要配置很多的参数,这些参数一部分是控制伸缩性的参数:最小连接数、最大连接数等。另外一部分是探活参数,如:空闲连接触发检查阈值

timeBetweenEvictionRunsMillis

deb8b266f9cf1624072ac4a5556c936c.png

连接池管理的复杂,从上图可以管中窥豹-可见一斑。一个线程专门用来创建和管理连接,一个专门负责取连接。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的源码。以便更好的了解参数的使用。

8b2446df0711848c8d703d948d9f1b6e.png

情境2-ActiveMQ未使用连接池连挂服务端

activeMQ有个是否使用连接池的配置:

spring.activemq.pool.enabled=false

这里false代表不使用连接池,不使用连接池在activeMQ里表达的意思是:每发送一条数据创建一个连接。于是有个同学进行了这样的配置以后,服务端出现大量的time_wait。time_wait表示服务端和客户端已经在进行四次挥手断开连接中了。

keepalive探活会探测到连接不可用就会进行这个断开连接操作。断连操作有一步很耗时,就是time_wait,因为它有2MSL的等待时间。很多连接的最终释放都卡在这里了。

ebb1ec3a8243dba38f91542a83f8fa6c.png

老连接不释放,服务器资源一直占用。新连接建立就会失败,表现出来就是服务器挂了。连接参数配置需谨慎:

#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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值