Druid线程池帮我们实现了应用程序和数据之间的长连接管理,一个线上变更引起了我的疑问,如果我们数据库切换到备用集群,怎么变更?
数据库连接,一般都是域名连接,现在将域名和IP的绑定关系变了,更新ngix,通过域名能找到新的Ip,然后期望通过新IP连接数据库。此时应用程序中的数据库连接池中还保持老IP的连接,这样会造成新的连接走到新数据库,老的连接走到老数据库。
为了解决这个问题,我们当时的操作是手动将老的数据库kill了,使连接池中的老的连接失效重连到新的IP,这样操作,目标是实现了,但是风险很大,如果kill了老数据库后新的数据库不能用咋办?业务都停了,连接池中的老的连接失效重连,这个过渡时间能持续好久,我们业务是否能接受,这都是风险!
欢迎小伙伴们提供思路!!!
下面是我翻看Druid的清除连接的源码整理出来的几个点,注意看源码中的几处注释
1.DruidDataSource.shrink(boolean checkTime)方法中异步线程 DestroyTask 对无效的Collection做清除
public void shrink(boolean checkTime) {
final List<DruidConnectionHolder> evictList = new ArrayList<DruidConnectionHolder>();
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
try {
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
if (checkTime) {
## 连接不管是否空闲,存活phyTimeoutMillis后强制回收,用于Destroy线程清理连接的时候的检测时间,如果不配置默认等于-1,也就是此处不检查
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - connection.getTimeMillis();
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictList.add(connection);
continue;
}
}
long idleMillis = currentTimeMillis - connection.getLastActiveTimeMillis();
if (idleMillis < minEvictableIdleTimeMillis) {
break;
}
if (checkTime && i < checkCount) {
evictList.add(connection);
} else if (idleMillis > maxEvictableIdleTimeMillis) {
## 连接的最大存活时间,如果连接的最大时间大于 maxEvictableIdleTimeMillis ,则无视最小连接数强制回收
evictList.add(connection);
}
} else {
if (i < checkCount) {
evictList.add(connection);
} else {
break;
}
}
}
int removeCount = evictList.size();
if (removeCount > 0) {
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
poolingCount -= removeCount;
}
} finally {
lock.unlock();
}
for (DruidConnectionHolder item : evictList) {
Connection connection = item.getConnection();
JdbcUtils.close(connection);
destroyCount.incrementAndGet();
}
}
2.DruidDataSource.public int removeAbandoned() 方法中异步线程 DestroyTask 对无效的Collection做清除
public int removeAbandoned() {
int removeCount = 0;
long currrentNanos = System.nanoTime();
List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();
synchronized (activeConnections) {
Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
for (; iter.hasNext();) {
DruidPooledConnection pooledConnection = iter.next();
if (pooledConnection.isRunning()) {
continue;
}
long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
## 通过datasource.getConnontion() 取得的连接必须在removeAbandonedTimeout这么多秒内调用close()要不我就弄死你.(就是conn不能超过指定的租期);removeAbandonedTimeoutMillis=默认300 * 1000 5分钟
if (timeMillis >= removeAbandonedTimeoutMillis) {
iter.remove();
pooledConnection.setTraceEnable(false);
abandonedList.add(pooledConnection);
}
}
}
if (abandonedList.size() > 0) {
for (DruidPooledConnection pooledConnection : abandonedList) {
synchronized (pooledConnection) {
if (pooledConnection.isDisable()) {
continue;
}
}
JdbcUtils.close(pooledConnection);
pooledConnection.abandond();
removeAbandonedCount++;
removeCount++;
.....................
}
}
return removeCount;
}
3.DruidDataSource. public DruidPooledConnection getConnecltionDirect(long maxWaitMillis) :检查空闲连接是否有效,如果连接无效关闭连接,再新建一个可用连接
if (isTestWhileIdle()) {
final long currentTimeMillis = System.currentTimeMillis();
final long lastActiveTimeMillis = poolableConnection.getConnectionHolder().getLastActiveTimeMillis();
final long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.getTimeBetweenEvictionRunsMillis();
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
## 获取连接的时候,检查连接的空闲时间,如果空间时间大于配置的时间,检测连接是否还有效,如果连接无效关闭连接,无效连接关闭了后再新建一个可用连接
if (idleMillis >= timeBetweenEvictionRunsMillis) {
boolean validate = testConnectionInternal(poolableConnection.getConnection());
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(realConnection);
continue;
}
}
}
还有其它地方会关闭连接,如果空闲连接数量大于配置的数量,这个时候需要关闭多余的连接。未完待续。。。。。。。。。。
当时我们的操作方案:
回滚方案 | | | | |
序号 | 步骤 | 操作人 | 时间 | |
7 | 通过全局锁阻塞新集群数据写入,加锁成功后再进入下一步。 | DBA | 10秒 | |
8 | 记录新集群主库此时binlog的pos点。 | DBA | 3秒 | |
9 | 新集群主库日志刷盘。 | DBA | 3秒 | |
10 | 解析生成新集群已执行过的dml/ddl语句(解析范围从步骤4的pos起始点开始到步骤9的pos结束点结束) | DBA | 以实际执行时间为准 | |
11 | 将sql文件传回原集群主库并执行 | DBA | 以实际执行时间为准 | |
12 | 域名进行回切。 | DBA | 以实际执行时间为准 | |
13 | 迁移完成后验证: | DBA | 10分钟 | |
| | | | |
注: | 其中7到11步骤是为了保证新老集群数据一致性,如果对RTO比较在意,可以容忍小部分数据丢失,建议回退直接从12步开始执行 |