動機:
プロジェクトの一部の機能ページのアクセス速度が非常に遅いので、最適化したいと思います。これらの関数ページは一般的に使用されるのではなく、単純なクエリ表示だけです。しかし、多くの場合、最初のアクセス速度は非常に遅い(> 10秒)。一度アクセスしてから、非常に高速(<100ms)でアクセスします。これはマルチデータソースプロジェクトであるため、これは複数のページに当てはまり、これらのページはすべて同じデータベースを使用します。そのため、本機にネットワークアクセス管理システムがあり、長期間使用しないと接続が切断される可能性があるため、データベース接続プールが接続を取得します。接続プールへの最初のアクセスでは、接続を取得するために新しい接続を作成する必要があります。
testWhileIdle、timeBetweenEvictionRunsMillis、removeAbandoned、removeAbandonedTimeoutMillis、initialSize、minIdle、maxActiveなど、多くのドルイド構成を試しましたが、この問題は引き続き発生します。
ドルイドモニタリングを通じて、次のことがわかりました。
データソースは長期間使用されておらず、データソース情報(物理オープン番号:1、物理クローズ番号:0、論理オープン番号:1、論理クローズ番号:1、オープン接続番号:0、接続プールアイドル番号:1) 、現時点では、接続が長時間アイドル状態であるため、使用できなくなりました。ここに特別な注意があります、私のminIdle設定は明らかに10です、なぜアイドル接続プールの数が10未満なのですか?
次に、ビジネスはデータソースのgetConnection()メソッドを呼び出して接続を取得します。最初に、アイドル接続が取得され(実際には使用できなくなります)、次にtestWhileIdleが構成されるため、テストで使用できないことが検出され、新しい接続が作成されます。その結果、ページの最初のアクセス速度が遅くなります。
理由が見つかりました。次のステップは解決策を見つけることです。
Baiduでドルイド関連の構成情報を検索しましたが、これらすべての構成を試しても機能しませんでした。ドルイドバージョンの問題かどうか疑問に思ったため、最新バージョン1.1.23へのアップグレードは機能しませんでした。最終的に、ドルイドのソースコードを分析することにしました。
ソースコードをデバッグして解釈します。
1.getConnectionを呼び出して接続を取得します。
dataSource.getConnection()
2.入入com.alibaba.druid.pool.DruidDataSource.getConnection
3.init()は初期化されているはずなので、心配する必要はありません。druidモニタリングとファイアウォールが構成されているため、filters.size()は2です。
4.com.alibaba.druid.filter.FilterChainImpl.dataSource_connectと入力します。この時点でthis.pos = 0、filterSize = 2
5. nextFilter()メソッドを確認します。最初にStatFilterフィルターを返し、次にposを1ずつインクリメントします。
6.入入統計フィルター.dataSource_getConnection
7.chain.dataSource_connectは上記の4つのステップに入りますが、この時点での位置は1です。したがって、最後にWallFilter.dataSource_getConnectionと入力すると、WallFilterは継承されたcom.alibaba.druid.filter.FilterAdapterであり、dataSource_getConnectionを書き換えません。方法。したがって、実際にはFilterAdapter.dataSource_getConnectionに入りました。
8.この時点でchain.dataSource_connect(dataSource、maxWaitMillis)を呼び出した後、上記の4つのステップに再び入りますが、この時点での位置は2です。したがってthis.pos <filterSizeはfalseであり、if実行には入りません。
9. dataSource.getConnectionDirect(maxWaitMillis)を実行します。これは少し長いので、スクリーンショットは撮りません。
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
int notFullTimeoutRetryCnt = 0;
for (;;) {
// handle notFullTimeoutRetry
DruidPooledConnection poolableConnection;
try {
poolableConnection = getConnectionInternal(maxWaitMillis);
} catch (GetConnectionTimeoutException ex) {
if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
notFullTimeoutRetryCnt++;
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
if (testOnBorrow) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
} else {
if (poolableConnection.conn.isClosed()) {
discardConnection(poolableConnection.holder); // 传入null,避免重复关闭
continue;
}
if (testWhileIdle) {
final DruidConnectionHolder holder = poolableConnection.holder;
long currentTimeMillis = System.currentTimeMillis();
long lastActiveTimeMillis = holder.lastActiveTimeMillis;
long lastExecTimeMillis = holder.lastExecTimeMillis;
long lastKeepTimeMillis = holder.lastKeepTimeMillis;
if (checkExecuteTime
&& lastExecTimeMillis != lastActiveTimeMillis) {
lastActiveTimeMillis = lastExecTimeMillis;
}
if (lastKeepTimeMillis > lastActiveTimeMillis) {
lastActiveTimeMillis = lastKeepTimeMillis;
}
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
if (removeAbandoned) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
activeConnectionLock.lock();
try {
activeConnections.put(poolableConnection, PRESENT);
} finally {
activeConnectionLock.unlock();
}
}
if (!this.defaultAutoCommit) {
poolableConnection.setAutoCommit(false);
}
return poolableConnection;
}
}
10.getConnectionInternalメソッドを呼び出して接続を取得します。
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
if (closed) {
connectErrorCountUpdater.incrementAndGet(this);
throw new DataSourceClosedException("dataSource already closed at " + new Date(closeTimeMillis));
}
if (!enable) {
connectErrorCountUpdater.incrementAndGet(this);
if (disableException != null) {
throw disableException;
}
throw new DataSourceDisableException();
}
final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
final int maxWaitThreadCount = this.maxWaitThreadCount;
DruidConnectionHolder holder;
for (boolean createDirect = false;;) {
if (createDirect) {
createStartNanosUpdater.set(this, System.nanoTime());
if (creatingCountUpdater.compareAndSet(this, 0, 1)) {
PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection();
holder = new DruidConnectionHolder(this, pyConnInfo);
holder.lastActiveTimeMillis = System.currentTimeMillis();
creatingCountUpdater.decrementAndGet(this);
directCreateCountUpdater.incrementAndGet(this);
if (LOG.isDebugEnabled()) {
LOG.debug("conn-direct_create ");
}
boolean discard = false;
lock.lock();
try {
if (activeCount < maxActive) {
activeCount++;
holder.active = true;
if (activeCount > activePeak) {
activePeak = activeCount;
activePeakTime = System.currentTimeMillis();
}
break;
} else {
discard = true;
}
} finally {
lock.unlock();
}
if (discard) {
JdbcUtils.close(pyConnInfo.getPhysicalConnection());
}
}
}
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("interrupt", e);
}
try {
if (maxWaitThreadCount > 0
&& notEmptyWaitThreadCount >= maxWaitThreadCount) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "
+ lock.getQueueLength());
}
if (onFatalError
&& onFatalErrorMaxActive > 0
&& activeCount >= onFatalErrorMaxActive) {
connectErrorCountUpdater.incrementAndGet(this);
StringBuilder errorMsg = new StringBuilder();
errorMsg.append("onFatalError, activeCount ")
.append(activeCount)
.append(", onFatalErrorMaxActive ")
.append(onFatalErrorMaxActive);
if (lastFatalErrorTimeMillis > 0) {
errorMsg.append(", time '")
.append(StringUtils.formatDateTime19(
lastFatalErrorTimeMillis, TimeZone.getDefault()))
.append("'");
}
if (lastFatalErrorSql != null) {
errorMsg.append(", sql \n")
.append(lastFatalErrorSql);
}
throw new SQLException(
errorMsg.toString(), lastFatalError);
}
connectCount++;
if (createScheduler != null
&& poolingCount == 0
&& activeCount < maxActive
&& creatingCountUpdater.get(this) == 0
&& createScheduler instanceof ScheduledThreadPoolExecutor) {
ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
if (executor.getQueue().size() > 0) {
createDirect = true;
continue;
}
}
if (maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
if (holder != null) {
if (holder.discard) {
continue;
}
activeCount++;
holder.active = true;
if (activeCount > activePeak) {
activePeak = activeCount;
activePeakTime = System.currentTimeMillis();
}
}
} catch (InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException(e.getMessage(), e);
} catch (SQLException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw e;
} finally {
lock.unlock();
}
break;
}
if (holder == null) {
long waitNanos = waitNanosLocal.get();
StringBuilder buf = new StringBuilder(128);
buf.append("wait millis ")//
.append(waitNanos / (1000 * 1000))//
.append(", active ").append(activeCount)//
.append(", maxActive ").append(maxActive)//
.append(", creating ").append(creatingCount)//
;
if (creatingCount > 0 && createStartNanos > 0) {
long createElapseMillis = (System.nanoTime() - createStartNanos) / (1000 * 1000);
if (createElapseMillis > 0) {
buf.append(", createElapseMillis ").append(createElapseMillis);
}
}
if (createErrorCount > 0) {
buf.append(", createErrorCount ").append(createErrorCount);
}
List<JdbcSqlStatValue> sqlList = this.getDataSourceStat().getRuningSqlList();
for (int i = 0; i < sqlList.size(); ++i) {
if (i != 0) {
buf.append('\n');
} else {
buf.append(", ");
}
JdbcSqlStatValue sql = sqlList.get(i);
buf.append("runningSqlCount ").append(sql.getRunningCount());
buf.append(" : ");
buf.append(sql.getSql());
}
String errorMessage = buf.toString();
if (this.createError != null) {
throw new GetConnectionTimeoutException(errorMessage, createError);
} else {
throw new GetConnectionTimeoutException(errorMessage);
}
}
holder.incrementUseCount();
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
return poolalbeConnection;
}
11.デバッグを続行し、論理オープンカウントがカウントされる場所を見つけます。
12.デバッグを続行し、接続カウントが開かれている場所を見つけます。ホルダーには、druidによってカプセル化されたデータベース接続オブジェクトconnが含まれています。
13.次に、デバッグし、1692行目のbreakコマンドまで実行して、forループからジャンプします。
14. poolableConnectionは、ステップ10でpoolableConnectionに返されます。次に、デバッグに進みます。testOnBorrowは通常デフォルトでfalseであり、チェックを開くとパフォーマンスに影響します。
15.次に、接続が閉じているかどうかをテストし、アイドルチェックを行います。testWhileIdle
16.DruidAbstractDataSource.testConnectionInternalと入力しました
protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {
String sqlFile = JdbcSqlStat.getContextSqlFile();
String sqlName = JdbcSqlStat.getContextSqlName();
if (sqlFile != null) {
JdbcSqlStat.setContextSqlFile(null);
}
if (sqlName != null) {
JdbcSqlStat.setContextSqlName(null);
}
try {
if (validConnectionChecker != null) {
boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
long currentTimeMillis = System.currentTimeMillis();
if (holder != null) {
holder.lastValidTimeMillis = currentTimeMillis;
holder.lastExecTimeMillis = currentTimeMillis;
}
if (valid && isMySql) { // unexcepted branch
long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
if (lastPacketReceivedTimeMs > 0) {
long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
if (lastPacketReceivedTimeMs > 0 //
&& mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
discardConnection(holder);
String errorMsg = "discard long time none received connection. "
+ ", jdbcUrl : " + jdbcUrl
+ ", jdbcUrl : " + jdbcUrl
+ ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
LOG.error(errorMsg);
return false;
}
}
}
if (valid && onFatalError) {
lock.lock();
try {
if (onFatalError) {
onFatalError = false;
}
} finally {
lock.unlock();
}
}
return valid;
}
if (conn.isClosed()) {
return false;
}
if (null == validationQuery) {
return true;
}
Statement stmt = null;
ResultSet rset = null;
try {
stmt = conn.createStatement();
if (getValidationQueryTimeout() > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
rset = stmt.executeQuery(validationQuery);
if (!rset.next()) {
return false;
}
} finally {
JdbcUtils.close(rset);
JdbcUtils.close(stmt);
}
if (onFatalError) {
lock.lock();
try {
if (onFatalError) {
onFatalError = false;
}
} finally {
lock.unlock();
}
}
return true;
} catch (Throwable ex) {
// skip
return false;
} finally {
if (sqlFile != null) {
JdbcSqlStat.setContextSqlFile(sqlFile);
}
if (sqlName != null) {
JdbcSqlStat.setContextSqlName(sqlName);
}
}
}
17. sqlを送信して接続をテストしますが、テスト方法は詳細ではありません。ここで、テストが正常な場合はtrueを返し、そうでない場合はfalseを返すか、例外をスローします。
18.以下は、主にデバッグによってスローされた例外の状況を分析します。これは、ネットワークアクセス仕様管理システムを使用しており、時間の経過とともにアイドル状態になると接続が切断されるためです。
(追記:データベースサーバーはアクティブに接続を切断します。testWhileIdleの前に接続が閉じられているかどうかがチェックされる場合があり、この時点まで実行されません。)
18.次に、false値をステップ16に返します。つまり、validate = falseです。次に降ります:
19.discardConnectionは接続を破棄します。
public void discardConnection(DruidConnectionHolder holder) {
if (holder == null) {
return;
}
//这里是获取jdbc的conn(即不是被druid封装的conn)
Connection conn = holder.getConnection();
if (conn != null) {
JdbcUtils.close(conn);//物理关闭连接
}
lock.lock();
try {
if (holder.discard) {
return;
}
if (holder.active) {
activeCount--;//正在打开连接数-1
holder.active = false;
}
discardCount++;//丢弃连接数+1
holder.discard = true;
if (activeCount <= minIdle) {//正在打开连接数小于等于最小空闲数
emptySignal();//唤醒CreateConnectionThread线程。
}
} finally {
lock.unlock();
}
}
20. CreateConnectionThreadスレッドを起動し、接続プールへの新しい接続を作成します。CreateConnectionThreadスレッドが行うことは、関連する分析が将来共有する時間があるでしょう。
21.使用できない接続を破棄した後、continueを実行し、forループの先頭にジャンプします。つまり、ステップ10から始まります。
22. takeList()を介してgetConnectionInternalで接続を取得します。ここでの取得ロジックの推測では、接続プールにアイドル接続がある場合、アイドル接続が直接返されます。アイドル接続がない場合、使用可能な接続を取得する前に、CreateConnectionThreadスレッドが新しいスレッドを作成するのを待つ必要があります。
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
while (poolingCount == 0) {
emptySignal(); // send signal to CreateThread create connection
if (failFast && isFailContinuous()) {
throw new DataSourceNotAvailableException(createError);
}
notEmptyWaitThreadCount++;
if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
}
try {
notEmpty.await(); // signal by recycle or creator
} finally {
notEmptyWaitThreadCount--;
}
notEmptyWaitCount++;
if (!enable) {
connectErrorCountUpdater.incrementAndGet(this);
if (disableException != null) {
throw disableException;
}
throw new DataSourceDisableException();
}
}
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to non-interrupted thread
notEmptySignalCount++;
throw ie;
}
decrementPoolingCount();
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
return last;
}
23.最後に、getConnectionDirectは最終的に使用可能な接続を取得し、それをdataSource.getConnection()に返します。
総括する
分析を通じて、次の3つのオプションを選択できます。
1.ドルイドを徹底的に研究し続けます。データベース接続プール内のアイドル接続数の定期的な検出を実現し、アイドル接続に使用できない接続が破棄されるようにして、新しい接続を作成します。この機能はremoveAbandoned構成で実現できるようですが、構成したので効果がないようです。勉強をしなければならない。接続プール内のアイドル接続の数<minIdle番号も調査する必要があります。
利点:ページアクセス速度を最適化し、ユーザーエクスペリエンスを向上させます。
短所:1。パフォーマンスに影響を与えます。長期間使用しない場合は自動的に接続を作成し、接続を破棄します。無限のループ。2.ドルイドの研究にエネルギーを費やします。
2.ネットワークアクセス標準管理システム(アクセスシステム)を構成します。サーバーのデータベース接続のアイドル時間を、たとえば3日または7日に延長します。
利点:1。ページアクセス速度を最適化し、ユーザーエクスペリエンスを向上させます。2.プロジェクトを変更する必要はありません。
短所:1。パフォーマンスに影響を与えます。接続を長期間維持するために、アライメントシステムのパフォーマンスに影響があります。2.設定するには、アドミッションシステム管理者に連絡する必要があります。
3.後の処理のために問題を保持します。
利点:脳は休むことができます最近、私はこの脱毛を再び研究しました。
短所:1。初めてページにアクセスするとき、ユーザーが少し遅くなる可能性があり、エクスペリエンスが良くありません。
結局、3番目のオプションを採用することにしました。とにかく、これらのページはあまり使用されておらず、最初の訪問後の速度は非常に速く、ほとんど効果がありません。将来的にデータソースの関連機能を拡張した後、状況が最適化されているかどうかを確認します。