前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >BoneCP连接池重连机制分析

BoneCP连接池重连机制分析

作者头像
心平气和
发布2020-09-11 11:30:42
8150
发布2020-09-11 11:30:42
举报

一、背景

朋友公司Mysql连接池用的BoneCP,应用程序访问Mysql以域名方式,配置如下:

jdbc:mysql://order.mysql.xx.cn:3306/order?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true

所有中间件访问都是通过内网的Dns服务器进行访问。

最近一个应用的Mysql所在机器已经老化严重,宕机几次,准备将数据库迁移到新机器上,为了减化操作,不想修改应用配置(线上机器几十台),问能不能只修改域名,而不用重启机器。

具体操作步骤如下:

1、将域名指向新机器;

2、将老实例关掉。

这里不讨论数据一致性和迁移数据的问题。

二、问题分析

这里有两点需要确认:

1、BoneCP失败了会重连吗?

即BoneCP能否捕捉连接失败/执行异常的错误,然后将连接池中打开的连接关闭。

2、DNS有缓存吗?

因为程序中配的是域名,实际上要通过4层的TCP协议连接Mysql,中间有个DNS解析,但DNS一般是有缓存的。

对于第一个问题,官方说是支持的,倒底是如何支持的,看下代码,关键代码为PreparedStatementHandle::execute方法:

/**
   * {@inheritDoc}
   * 
   * @see java.sql.PreparedStatement#execute()
   */
  // @Override
  public boolean execute() throws SQLException {
    checkClosed();
    try {
      if (this.logStatementsEnabled){
        logger.debug(PoolUtil.fillLogParams(this.sql, this.logParams));
      }
      long queryStartTime = queryTimerStart();

      if (this.connectionHook != null){
        this.connectionHook.onBeforeStatementExecute(this.connectionHandle, this, this.sql, this.logParams);
      }

      boolean result = this.internalPreparedStatement.execute();

      if (this.connectionHook != null){
        this.connectionHook.onAfterStatementExecute(this.connectionHandle, this, this.sql, this.logParams);
      }


      queryTimerEnd(this.sql, queryStartTime);

      return result;
    } catch (SQLException e) {
      throw this.connectionHandle.markPossiblyBroken(e);

    }

  }

重点关注对异常的处理,即markPossiblyBroken的调用

/** 
   * Given an exception, flag the connection (or database) as being potentially broken. If the exception is a data-specific exception,
   * do nothing except throw it back to the application. 
   * 
   * @param e SQLException e
   * @return SQLException for further processing
   */
  protected SQLException markPossiblyBroken(SQLException e) {
      String state = e.getSQLState();
      boolean alreadyDestroyed = false;

    ConnectionState connectionState = this.getConnectionHook() != null ? this.getConnectionHook().onMarkPossiblyBroken(this, state, e) : ConnectionState.NOP; 
    if (state == null){ // safety;
      state = "08999"; 
    }

    if (((sqlStateDBFailureCodes.contains(state) || connectionState.equals(ConnectionState.TERMINATE_ALL_CONNECTIONS)) && this.pool != null) && this.pool.getDbIsDown().compareAndSet(false, true) ){
      logger.error("Database access problem. Killing off this connection and all remaining connections in the connection pool. SQL State = " + state);
      this.pool.connectionStrategy.terminateAllConnections();
      this.pool.destroyConnection(this);
      this.logicallyClosed.set(true);
      alreadyDestroyed = true;

      for (int i=0; i < this.pool.partitionCount; i++) {
        // send a signal to try re-populating again.
        this.pool.partitions[i].getPoolWatchThreadSignalQueue().offer(new Object()); // item being pushed is not important.
      }
    }

   //如果是指定错误码,一般来说是连接超时之类的,会关闭连接池中的连接
    if (state.equals("08003") || sqlStateDBFailureCodes.contains(state) || e.getCause() instanceof SocketException) {
        if (!alreadyDestroyed) {
      this.pool.destroyConnection(this);
      this.logicallyClosed.set(true);
      getOriginatingPartition().getPoolWatchThreadSignalQueue().offer(new Object()); // item being pushed is not important.
        }
    }
    
 
    char firstChar = state.charAt(0);
    if (connectionState.equals(ConnectionState.CONNECTION_POSSIBLY_BROKEN) || state.equals("40001") || 
        state.startsWith("08") ||  (firstChar >= '5' && firstChar <='9') /*|| (firstChar >='I' && firstChar <= 'Z')*/){
      this.possiblyBroken = true;
    }

    // Notify anyone who's interested
    if (this.possiblyBroken  && (this.getConnectionHook() != null)){
      this.possiblyBroken = this.getConnectionHook().onConnectionException(this, state, e);
    }

    return e;
  }

可以看到,代码中会检测是否为连接超时之类的错误,如是则关闭连接池的连接,这样下次就会重新建立新连接了。

第二个是关于DNS解析的问题,通过分析代码,BoneCP的连接复用的Jdbc的代码,连接的建立是由Jdbc包里的StandardSocketFactory类来完成的:

/**
   * @see com.mysql.jdbc.SocketFactory#createSocket(Properties)
   */
  public Socket connect(String hostname, int portNumber, Properties props)
      throws SocketException, IOException {

 

      if (this.host != null) {
        if (!(wantsLocalBind || wantsTimeout || needsConfigurationBeforeConnect)) {
          //根据域名查找IP
          InetAddress[] possibleAddresses = InetAddress
              .getAllByName(this.host);

          Throwable caughtWhileConnecting = null;

          // Need to loop through all possible addresses, in case
          // someone has IPV6 configured (SuSE, for example...)

          for (int i = 0; i < possibleAddresses.length; i++) {
            try {
              this.rawSocket = new Socket(possibleAddresses[i],
                  port);

              configureSocket(this.rawSocket, props);

              break;
            } catch (Exception ex) {
              caughtWhileConnecting = ex;
            }
          }

          if (rawSocket == null) {
            unwrapExceptionToProperClassAndThrowIt(caughtWhileConnecting);
          }
        } else {
          
        }

        return this.rawSocket;
      }
    }

    throw new SocketException("Unable to create socket");
  }

可以看到最终是调用 InetAddres.getAllByName 来根据域名获取IP地址的。

getAllByName会调用getAddressesFromNameService来获取IP:

 private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
        throws UnknownHostException
    {
        InetAddress[] addresses = null;
        boolean success = false;
        UnknownHostException ex = null;

      
        if ((addresses = checkLookupTable(host)) == null) {
            try {
                // This is the first thread which looks up the addresses
                // this host or the cache entry for this host has been
                // expired so this thread should do the lookup.
                for (NameService nameService : nameServices) {
                    try {
                        /*
                         * Do not put the call to lookup() inside the
                         * constructor.  if you do you will still be
                         * allocating space when the lookup fails.
                         */

                        addresses = nameService.lookupAllHostAddr(host);
                        success = true;
                        break;
                    } catch (UnknownHostException uhe) {
                        if (host.equalsIgnoreCase("localhost")) {
                            InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
                            addresses = local;
                            success = true;
                            break;
                        }
                        else {
                            addresses = unknown_array;
                            success = false;
                            ex = uhe;
                        }
                    }
                }

                // More to do?
                if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
                    // Find it?
                    int i = 1;
                    for (; i < addresses.length; i++) {
                        if (addresses[i].equals(reqAddr)) {
                            break;
                        }
                    }
                    // Rotate
                    if (i < addresses.length) {
                        InetAddress tmp, tmp2 = reqAddr;
                        for (int j = 0; j < i; j++) {
                            tmp = addresses[j];
                            addresses[j] = tmp2;
                            tmp2 = tmp;
                        }
                        addresses[i] = tmp2;
                    }
                }
                //关键代码,缓存查询结果
                cacheAddresses(host, addresses, success);

                if (!success && ex != null)
                    throw ex;

            } finally {
                // Delete host from the lookupTable and notify
                // all threads waiting on the lookupTable monitor.
                updateLookupTable(host);
            }
        }

        return addresses;
    }

其中会调用cacheAddresses将地址保存到缓存中,倒底缓冲多久呢?

代码就不再贴了,是通过调用InetAddressCachePolicy来获取缓存时间,

 static {
        Integer var0 = (Integer)AccessController.doPrivileged(new PrivilegedAction<Integer>() {
            public Integer run() {
                String var1;
                try {
                    var1 = Security.getProperty("networkaddress.cache.ttl");
                    if(var1 != null) {
                        return Integer.valueOf(var1);
                    }
                } catch (NumberFormatException var3) {
                    ;
                }

                try {
                    var1 = System.getProperty("sun.net.inetaddr.ttl");
                    if(var1 != null) {
                        return Integer.decode(var1);
                    }
                } catch (NumberFormatException var2) {
                    ;
                }

                return null;
            }
        });
        if(var0 != null) {
            cachePolicy = var0.intValue();
            if(cachePolicy < 0) {
                cachePolicy = -1;
            }

            propertySet = true;
        } else if(System.getSecurityManager() == null) {
            cachePolicy = 30;
        }

可以看到InetAddressCachePolicy会从几个配置中读,如果读不到,也没有配置SecurityManager,则默认是30秒,也就是本例中的情况。

四、实际验证

上面是我们的分析过程,如何验证呢?

1、将程序跑起来;

2、将域名order.mysql.xx.cn指向新机器;

3、在老的mysql机器上用show processlist显示连接,然后用kill杀掉这些连接;

4、观察新的mysql机器上有没连接过来,程序有没报错

不出意外的话,程序会有一段小报错,然后恢复正常了,所有mysql连接都指向新机器了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员升级之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档