低バージョンの Druid 接続プール + MySQL ドライバー 8.0 により、スレッドがブロックされ、パフォーマンスが制限される

低バージョンの Druid 接続プール + MySQL ドライバー 8.0 により、スレッドがブロックされ、パフォーマンスが制限される

現象

アプリケーションを MySQL ドライバー 8.0 にアップグレードした後、同時実行性が高くなると、監視ポイントがチェックされ、Druid 接続プールが接続を取得して SQL を実行する時間はほとんど 200 ミリ秒を超えます。

システムのストレス テストを行ったところ、多数のスレッドがブロックされていることが判明しました。スレッド ダンプ情報は次のとおりです。

"http-nio-5366-exec-48" #210 デーモン prio=5 os_prio=0 tid=0x00000000023d0800 nid=0x3be9 モニターエントリを待機中 [0x00007fa4c1400000]
   java.lang.Thread.State: BLOCKED (オブジェクト モニター上)
        org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.loadClass(TomcatEmbeddedWebappClassLoader.java:66) で
        - <0x0000000775af0960> (java.lang.Object) のロック待ち
        org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1186) で
        com.alibaba.druid.util.Utils.loadClass(Utils.java:220) で
        com.alibaba.druid.util.MySqlUtils.getLastPacketReceivedTimeMs(MySqlUtils.java:372) で

根本原因分析

パブリッククラスMySqlUtils {

    パブリック静的long getLastPacketReceivedTimeMs(接続conn)はSQLExceptionをスローします{
        (class_connectionImpl == null && !class_connectionImpl_Error) の場合 {
            試す {
                class_connectionImpl = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
            } キャッチ (スロー可能なエラー) {
                クラス接続エラー = true;
            }
        }

        (class_connectionImpl == null)の場合{
            -1 を返します。
        }

        メソッド取得IOがnullの場合、メソッド取得IOエラーが発生します。
            試す {
                メソッド getIO = class_connectionImpl.getMethod("getIO");
            } キャッチ (スロー可能なエラー) {
                メソッド_getIO_error = true;
            }
        }

        メソッドgetIOがnullの場合
            -1 を返します。
        }

        (class_MysqlIO == null && !class_MysqlIO_Error) の場合 {
            試す {
                class_MysqlIO = Utils.loadClass("com.mysql.jdbc.MysqlIO");
            } キャッチ (スロー可能なエラー) {
                クラス_MysqlIO_Error = true;
            }
        }

        (class_MysqlIO == null)の場合{
            -1 を返します。
        }

        (method_getLastPacketReceivedTimeMs == null && !method_getLastPacketReceivedTimeMs_error) の場合 {
            試す {
                メソッド method = class_MysqlIO.getDeclaredMethod("getLastPacketReceivedTimeMs");
                メソッド.setAccessible(true);
                method_getLastPacketReceivedTimeMs = メソッド;
            } キャッチ (スロー可能なエラー) {
                method_getLastPacketReceivedTimeMs_error = true;
            }
        }

        (method_getLastPacketReceivedTimeMs == null)の場合{
            -1 を返します。
        }

        試す {
            オブジェクト connImpl = conn.unwrap(class_connectionImpl);
            connImpl == nullの場合{
                -1 を返します。
            }

            オブジェクト mysqlio = method_getIO.invoke(connImpl);
            Long ms = (Long) method_getLastPacketReceivedTimeMs.invoke(mysqlio);
            ms.longValue() を返します。
        } キャッチ (IllegalArgumentException e) {
            新しい SQLException をスローします ("getLastPacketReceivedTimeMs エラー", e);
        } キャッチ (IllegalAccessException e) {
            新しい SQLException("getLastPacketReceivedTimeMs エラー", e) をスローします。
        } キャッチ (InvocationTargetException e) {
            新しい SQLException("getLastPacketReceivedTimeMs エラー", e) をスローします。
        }
    }

MySqlUtils の getLastPacketReceivedTimeMs() メソッドは com.mysql.jdbc.MySQLConnection クラスをロードしますが、クラス名は MySQL ドライバー 8.0 で com.mysql.cj.jdbc.ConnectionImpl に変更されているため、com.mysql.jdbc.MySQLConnection は MySQL ドライバー 8.0 ではロードできません。

getLastPacketReceivedTimeMs() メソッドの実装で、Utils.loadClass("com.mysql.jdbc.MySQLConnection") がクラスのロードに失敗して例外をスローすると、変数 class_connectionImpl_Error が変更され、次回呼び出されたときにクラスはロードされなくなります。

パブリッククラスUtils{

    パブリック静的クラス<?> loadClass(String className) {
        クラス<?> clazz = null;

        クラス名が null の場合
            null を返します。
        }

        試す {
            Class.forName(className) を返します。
        } キャッチ (ClassNotFoundException e) {
            // スキップ
        }

        クラスローダー ctxClassLoader = Thread.currentThread().getContextClassLoader();
        ctxClassLoader != null の場合 {
            試す {
                clazz = ctxClassLoader.loadClass(クラス名);
            } キャッチ (ClassNotFoundException e) {
                // スキップ
            }
        }

        戻りクラッズ;
    }

ただし、ClassNotFoundException は Utils の loadClass() メソッドでもキャッチされるため、loadClass() はクラスをロードできない場合に例外をスローせず、getLastPacketReceivedTimeMs() メソッドが呼び出されるたびに MySQLConnection クラスが 1 回ロードされることになります。

スレッド ダンプ情報には、TomcatEmbeddedWebappClassLoader の loadClass() メソッドを呼び出すときにスレッドがブロックされていることが示されています。

パブリッククラスTomcatEmbeddedWebappClassLoaderはParallelWebappClassLoaderを拡張します{

 パブリック Class<?> loadClass(String name, boolean resolve) は ClassNotFoundException をスローします {
  同期化 (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
   クラス<?>結果 = findExistingLoadedClass(名前);
   結果 = (結果 != null) ? 結果: doLoadClass(name);
   結果が null の場合
    新しい ClassNotFoundException(名前) をスローします。
   }
   必要であれば解決を返します(結果、解決)。
  }
 }

これは、TomcatEmbeddedWebappClassLoader がクラスをロードするときに同期ロックを追加するためです。その結果、getLastPacketReceivedTimeMs() メソッドが呼び出されるたびに com.mysql.jdbc.MySQLConnection が 1 回ロードされますが、実際にはロードされません。クラスをロードするときに同期ロックが追加されるため、スレッドのブロックとパフォーマンスの低下が発生します。

getLastPacketReceivedTimeMs() メソッドの呼び出し時間

パブリック抽象クラス DruidAbstractDataSource は WrapperAdapter を拡張し、DruidAbstractDataSourceMBean、DataSource、DataSourceProxy、Serializable を実装します {

    保護されたブール型 testConnectionInternal(DruidConnectionHolder ホルダー、接続接続) {
        文字列 sqlFile = JdbcSqlStat.getContextSqlFile();
        文字列 sqlName = JdbcSqlStat.getContextSqlName();

        (sqlFile != null)の場合{
            JdbcSqlStat.setContextSqlFile(null);
        }
        (sqlName != null)の場合{
            JdbcSqlStat.setContextSqlName(null);
        }
        試す {
            有効な接続チェッカーが null の場合
                ブール値有効 = validConnectionChecker.isValidConnection(conn、validationQuery、validationQueryTimeout);
                長いcurrentTimeMillis = System.currentTimeMillis();
                ホルダーが null ではない場合
                    ホルダー.lastValidTimeMillis = 現在のTimeMillis;
                    ホルダー.lastExecTimeMillis = currentTimeMillis;
                }

                if (valid && isMySql) { // 例外のないブランチ
                    長い lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
                    最後のパケット受信時間ミリ秒 > 0 の場合 {
                        長い mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
                        (最後のパケット受信時間Ms > 0の場合 //
                                && mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
                            接続を破棄します(ホルダー);
                            文字列 errorMsg = "長時間受信されていない接続を破棄します。"
                                    + "、jdbcUrl : " + jdbcUrl
                                    + "、jdbcUrl : " + jdbcUrl
                                    + "、lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
                            LOG.error(エラーメッセージ);
                            false を返します。
                        }
                    }
                }

                if (有効 && onFatalError) {
                    ロック。ロック();
                    試す {
                        (致命的なエラーの場合){
                            onFatalError = false;
                        }
                    ついに
                        ロックを解除します。
                    }
                }

                有効な値を返します。
            }

            conn.isClosed() の場合 {
                false を返します。
            }

            if (null == 検証クエリ) {
                true を返します。
            }

            ステートメント stmt = null;
            結果セット rset = null;
            試す {
                stmt = conn.createStatement();
                getValidationQueryTimeout() が 0 以上のとき
                    stmt.setQueryTimeout(検証クエリタイムアウト);
                }
                rset = stmt.executeQuery(検証クエリ);
                if (!rset.next()) {
                    false を返します。
                }
            ついに
                JdbcUtils.close(rset);
                JdbcUtils.close(stmt);
            }

            (致命的なエラーの場合){
                ロック。ロック();
                試す {
                    (致命的なエラーの場合){
                        onFatalError = false;
                    }
                ついに
                    ロックを解除します。
                }
            }

            true を返します。
        } キャッチ (Throwable ex) {
            // スキップ
            false を返します。
        ついに
            (sqlFile != null)の場合{
                JdbcSqlStat.setContextSqlFile(sqlFile);
            }
            (sqlName != null)の場合{
                JdbcSqlStat.setContextSqlName(sqlName);
            }
        }
    }

getLastPacketReceivedTimeMs() メソッドは、DruidAbstractDataSource の testConnectionInternal() メソッドでのみ呼び出されます。

testConnectionInternal() は、接続が有効かどうかを確認するために使用されます。このメソッドは、Druid が接続が有効かどうかを確認するために使用するパラメータに応じて、接続を取得するとき、または接続を返すときに呼び出される場合があります。

接続が有効かどうかを検出するための Druid のパラメータ:

  • testOnBorrow: 接続が取得されるたびにvalidationQueryを実行し、接続が有効かどうかを確認します(パフォーマンスに影響します)
  • testOnReturn: 接続が返されるたびにvalidationQueryを実行し、接続が有効かどうかを確認します(パフォーマンスに影響します)。
  • testWhileIdle: 接続を申請するときにチェックします。アイドル時間が timeBetweenEvictionRunsMillis より大きい場合は、validationQuery を実行して接続が有効かどうかを確認します。
  • アプリケーションは testOnBorrow=true を設定します。接続が取得されるたびに同期ロックが取得されるため、パフォーマンスが大幅に低下します。

解決

このバグは、Druid 1.x バージョン <= 1.1.22 を使用しているときに発生することが確認されています。解決策は、Druid 1.x バージョン >= 1.1.23 または Druid 1.2.x バージョンにアップグレードすることです。

GitHub の問題: https://github.com/alibaba/druid/issues/3808

これで、低バージョンの Druid 接続プール + MySQL ドライバー 8.0 がスレッド ブロックとパフォーマンス制限を引き起こすことに関するこの記事は終了です。MySQL ドライバー 8.0 低バージョンの Druid 接続プールの詳細については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM を応援してください。

以下もご興味があるかもしれません:
  • MySQL 8.0ドライバとAlibaba Druidバージョン間の互換性の問題を解決
  • MySql 8.0 と対応するドライバー パッケージの一致に関する注意事項
  • MySQL 8.0 バージョンで getTables がすべてのデータベース テーブルを返す問題の簡単な分析
  • MybatisリバースエンジニアリングでMysql8.0バージョンドライバを使用する際に発生する問題の詳細な説明

<<:  Vueはシンプルなメモ帳機能を実装します

>>:  a タグにはテキストと画像があります。テキストを非表示にして画像のみを表示するにはどうすればよいでしょうか?

推薦する

ウェブサイトにファビコンを追加するためのヒント: URLの前の小さなアイコン

いわゆるファビコンは、Favorites Icon の略で、中国語ではウェブサイトアバターと呼ばれて...

js でパズルゲームを実装する

この記事では、パズルゲームを実装するためのjsの具体的なコードを参考までに共有します。具体的な内容は...

Linux で crontab 出力リダイレクトが有効にならない問題の解決方法

質問LINUX では、定期的なタスクは通常、cron デーモン プロセス [ps -ef | gre...

CentOS 8/RHEL 8 に Cockpit をインストールして使用する方法

Cockpit は、CentOS および RHEL システムで使用できる Web ベースのサーバー管...

HTMLタグのフルネームと機能の紹介

アルファベット順DTD: このタグが許可される XHTML 1.0 DTD を示します。 S=厳密、...

無料のパブリック STUN サーバー

無料のパブリック STUN サーバーSIP 端末がプライベート IP アドレスを使用する場合、スタン...

実行後にdocker nginxにアクセスできない問題の解決策

## 1最近、docker デプロイメントを学習しており、当初は nginx を docker 化す...

Iframe 適応高さコードに関する 3 つの議論

B/S システム インターフェースを構築する場合、メイン ページ index.html 内に他のペー...

vue.js ルーターのネストされたルート

序文:ルートでは、主要部分は同じでも、基礎となる構造が異なることがあります。たとえば、ホームページに...

MySQL におけるデフォルトの使用法の詳細な説明

NULL および NOT NULL 修飾子、DEFAULT 修飾子、AUTO_INCREMENT 修...

メモリ構成が過剰でMySQLが起動できない問題の解決方法

問題の説明MySQL の起動時にエラーが報告されます。エラー ログを確認してください。 [エラー] ...

MySQLデータベースが大きすぎる場合にバックアップと復元を行う方法

コマンド: mysqlhotcopyこのコマンドは、ファイルをコピーする前にテーブルをロックし、不完...

Vue+canvas は、ウォーターフォール チャートを上から下までリアルタイムに更新する効果を実現します (QT と同様)

早速ですが、デモ画像をご紹介します。実装されている機能は、左側に凡例、右側にウォーターフォール チャ...

自動行折り返し機能付き CSS Flex レイアウトのサンプル コード

フレックス コンテナーを作成するには、要素に display: flex プロパティを追加するだけで...

Vue3は画像拡大鏡効果を実現します

この記事の例では、画像拡大鏡効果を実現するためのVue3の具体的なコードを参考までに共有しています。...