• boneCP原理研究


    连接的生存时间

    MySQL有一个重要的参数wait_timeout,用于规定一个connection最大的idle时间,默认是28800秒,即每个connection连续的sleep状态不能超过该值,否则MySQL会自动回收该connection。

    连接池的作用是管理连接,任何想要请求数据库连接的行为都和连接池发生交互,从连接池里申请连接,使用完成后将连接交还给连接池。

    在一个比较空闲的系统上,连接可能长时间的处于sleep状态,那么一旦达到了MySQL wait_timeout的规定时间,MySQL就要回收连接,这时连接池如果仍然认为该connection可用,待应用向连接池请求时,就会将不存在的connection资源交给应用,这样就会报错。

    因此连接池需要一套机制保证每一个connection在连接池生存期内是始终可用的。

    我推测应该有两种机制:定期检测和申请时检测。

    boneCP采用定期检测机制,即每隔一个时间间隔就会检查其管理的连接处于sleep状态的时间,如果超过了设定的值,则会将此connection重建。

    boneCP中有两个很重要的参数:idleConnectionTestPeriodInSeconds和idleMaxAgeInSeconds,分别是连接探测时间阈值和连接最大空闲时间,默认值为4小时和1小时。

    boneCP会启动三个线程来进行定期任务:

    this.keepAliveScheduler =  Executors.newScheduledThreadPool(this.config.getPartitionCount(), new CustomThreadFactory("BoneCP-keep-alive-scheduler"+suffix, true));
    this.maxAliveScheduler =  Executors.newScheduledThreadPool(this.config.getPartitionCount(), new CustomThreadFactory("BoneCP-max-alive-scheduler"+suffix, true));
    this.connectionsScheduler =  Executors.newFixedThreadPool(this.config.getPartitionCount(), new CustomThreadFactory("BoneCP-pool-watch-thread"+suffix, true));

    其中keepAliveScheduler用来每隔一段时间检查connection的sleep状态时间是否达到了idleMaxAgeInSeconds或者是否已经失效,如果是,则会将连接kill掉,主要的逻辑见ConnectionTesterThread.java。

    需要注意的是,在默认的配置下,该检查的调度是这样定义的:

    this.keepAliveScheduler.scheduleAtFixedRate(connectionTester,delayInSeconds, delayInSeconds, TimeUnit.SECONDS);

    此时delayInSeconds的值是取idleMaxAgeInSeconds值,即1小时,实际上只要idleMaxAgeInSeconds小于idleConnectionTestPeriodInSeconds,则delayInSeconds一定是idleMaxAgeInSeconds。这样,该线程每1小时就会启动一次,同时要检查时间阈值就会变成1小时。

    keepalive线程要执行的命令是这样的:

    final Runnable connectionTester = new ConnectionTesterThread(connectionPartition, this, this.config.getIdleMaxAge(TimeUnit.MILLISECONDS), this.config.getIdleConnectionTestPeriod(TimeUnit.MILLISECONDS), queueLIFO);

    下面是对ConnectionTesterThread类的解析:

        protected ConnectionTesterThread(ConnectionPartition connectionPartition,
                BoneCP pool, long idleMaxAgeInMs, long idleConnectionTestPeriodInMs, boolean lifoMode){
            this.partition = connectionPartition;
            this.idleMaxAgeInMs = idleMaxAgeInMs; //60 * 60 * 1000
            this.idleConnectionTestPeriodInMs = idleConnectionTestPeriodInMs; //240 * 60 * 1000
            this.pool = pool;
            this.lifoMode = lifoMode;
        }

    上面代码中,两个主要时间用注释标明。

    在该类中最重要的两段代码是用来判断connection的sleep时间是否超标的,只需要对比现在时间和connection最后一次使用时间中间的时间间隔就可以:

    // check if connection has been idle for too long (or is marked as broken)
    if (connection.isPossiblyBroken() || ((this.idleMaxAgeInMs > 0) && ( System.currentTimeMillis()-connection.getConnectionLastUsedInMs() > this.idleMaxAgeInMs))){
    // kill off this connection - it's broken or it has been idle for too long
        closeConnection(connection);
        continue;
    }

    上一段代码判断connection是否sleep时间超过了idleMaxAgeInMs的规定(默认3600000ms)。

    if (this.idleConnectionTestPeriodInMs > 0 && (currentTimeInMs-connection.getConnectionLastUsedInMs() > this.idleConnectionTestPeriodInMs) &&
            (currentTimeInMs-connection.getConnectionLastResetInMs() >= this.idleConnectionTestPeriodInMs)) {
        // send a keep-alive, close off connection if we fail.
        if (!this.pool.isConnectionHandleAlive(connection)){
            closeConnection(connection);
            continue; 
        }
        // calculate the next time to wake up
        tmp = this.idleConnectionTestPeriodInMs;
        if (this.idleMaxAgeInMs > 0){ // wake up earlier for the idleMaxAge test?
            tmp = Math.min(tmp, this.idleMaxAgeInMs);
        }
    } 

    如果保持默认值,或者设置的值中idleConnectionTestPeriodInMs大于idleMaxAgeInMs的,则这段代码理论上讲不会执行。但是无论如何,这个类都是用来杀死connection的。

    接下来继续看BoneCP类,在确定了keepalive调度之后(没有设置maxConnectionAgeInSeconds参数),程序会继续执行到

    this.connectionsScheduler.execute(new PoolWatchThread(connectionPartition, this));

    这段代码最主要的目的就是新建connection。如果没有设置,则会根据partition最大连接数和已创建连接数进行计算,决定下次新建的连接数。

    至于一次新建多少连接,由该类PoolWatchThread.java中的这句决定:

    fillConnections(Math.min(maxNewConnections, this.partition.getAcquireIncrement()));

    而重要的参数maxNewConnections则是这样计算出来的:

    maxNewConnections = this.partition.getMaxConnections()-this.partition.getCreatedConnections();

    根据我对代码debug的情况看,连接新建无非分两个情况:keepalive杀死超时连接后和调用getConnection方法后。

    让人奇怪的是为什么连接里明明有sleep的连接,却非要新建一个给客户端用?

    跟代码发现了这个类:DefaultConnectionStrategy.java

    if (!connectionPartition.isUnableToCreateMoreTransactions()){ // unless we can't create any more connections...
          this.pool.maybeSignalForMoreConnections(connectionPartition);  // see if we need to create more
        }

    注释中写了,除非是没办法新建连接了(因为到了上限),否则就会新建连接,此时需要了解maybeSignalForMoreConnections这个方法是干什么的?

    于是又要回到BoneCP类:

    /**
    * Tests if this partition has hit a threshold and signal to the pool watch thread to create new connections
    * @param connectionPartition to test for.
    */
    protected void maybeSignalForMoreConnections(ConnectionPartition connectionPartition) {
    
        if (!connectionPartition.isUnableToCreateMoreTransactions() 
                && !this.poolShuttingDown &&
            connectionPartition.getAvailableConnections()*100/connectionPartition.getMaxConnections() <= this.poolAvailabilityThreshold){
                connectionPartition.getPoolWatchThreadSignalQueue().offer(new Object()); // item being pushed is not important.
            }
        }

    好了,发现问题关键了:

    connectionPartition.getAvailableConnections()*100/connectionPartition.getMaxConnections() <= this.poolAvailabilityThreshold

    这个判断很重要,poolAvailabilityThreshold这个值是0,这是默认值,也没设置过,由此可以推断connectionPartition.getAvailableConnections()一定为0(分母不可能为0,所以分子肯定是0)。

    再看看connectionPartition.getAvailableConnections()是干什么的,从名字上看是获得可用连接的,那么明显没有获得可用连接:

    protected int getAvailableConnections() {
        return this.freeConnections.size();
    }

    freeConnections是一个Queue,它的size现在来看应该是0,也就是说没有free的connection。

    但是明明初始化连接池的时候已经建立了连接的,而且我是调试程序,根本没有可能使用到已经建立的连接,为什么呢?

    这个情况只在只建立了一个连接的时候才会出现,这是因为这个类DefaultConnectionStrategy.java的这句:

    result = connectionPartition.getFreeConnections().poll();

    在这一句执行之前,connectionPartition中的freeConnection属性中是有一个连接的,但是在poll执行之后,该连接被取出,因此freeConnection属性变成了一个空的Queue,size自然就是0了。所以该情况在最开始有一个以上连接的时候就不再存在了。

    这也是正常的,因为只有一个connection对于pool来说是不安全的,请求了一个之后新建一个给之后的请求做准备。

  • 相关阅读:
    Java并发(5)- ReentrantLock与AQS
    Java并发(4)- synchronized与CAS
    Windows cmd 查看文件MD5 SHA1 SHA256
    进程、线程、协程概念理解
    Python学习--Python运算符
    Python学习--Python变量类型
    MySQL性能优化
    Docket学习--Docker入门
    Python学习--Python基础语法
    Python学习--Python 环境搭建
  • 原文地址:https://www.cnblogs.com/hehheai/p/6506861.html
Copyright © 2020-2023  润新知