1. 背景最近、テスターから非常に多くの問題が報告されていますが、その中でもシステム信頼性テストで発生した問題は非常に厄介です。第一に、このような問題は「散発的」な場合があり、環境ですぐに再現することが困難です。第二に、信頼性の問題のロケーション チェーンが非常に長くなる場合があります。極端な場合には、サービス A からサービス Z まで、またはアプリケーション コードからハードウェア レベルまでトレースする必要がある場合もあります。 今回は、MySQL の高可用性の問題を特定するプロセスを共有します。プロセスには紆余曲折がありましたが、問題自体は非常に代表的なものなので、参考のために記録します。 建築まず、このシステムでは、主要なデータ ストレージ コンポーネントとして MySQL を使用します。全体は典型的なマイクロサービス アーキテクチャ (SpringBoot + SpringCloud) であり、永続化レイヤーでは次のコンポーネントが使用されます。 Mybatis、SQL <-> メソッドマッピングを実現 hikaricp、データベース接続プールを実装する mariadb-java-client、JDBC ドライバーを実装します MySQL サーバー部分では、バックエンドはデュアルマスター アーキテクチャを採用し、フロントエンドは keepalived とフローティング IP (VIP) を組み合わせて高可用性レイヤーを提供します。次のように: 例示する
Keepalived は、VRRP プロトコルに基づいてルーティング レイヤー変換を実装します。同時に、VIP は 1 つの仮想マシン (マスター) のみを指します。マスター ノードに障害が発生すると、他の keepalived が問題を検出し、新しいマスターを再選択し、その後 VIP は別の利用可能な MySQL インスタンス ノードに切り替えます。このように、MySQL データベースには基本的な高可用性機能が備わっています。 もう 1 つのポイントは、Keepalived が MySQL インスタンスに対して定期的なヘルス チェックも実行することです。MySQL インスタンスが利用できないことが判明すると、Keepalived は自身のプロセスを強制終了し、VIP 切り替えアクションをトリガーします。 問題現象このテスト ケースも、仮想マシンの障害のシナリオに基づいて設計されています。
しかし、多くのテストを行った結果、MySQL マスター ノード コンテナを再起動すると、ビジネスにアクセスできなくなる可能性が一定数あることが判明しました。 2. 分析プロセス問題が発生した後、開発者の最初の反応は、MySQL の高可用性メカニズムに問題があるというものでした。過去に、keepalived の設定が不適切だったために VIP が時間内に切り替えられなかったという問題が発生したことがあるため、私たちはすでにその問題に対して警戒しています。 徹底的に調査した結果、keepalived の設定に問題は見つかりませんでした。 その後、他に選択肢がなかったので、数回再テストしましたが、問題は再び発生しました。 そこで私たちはいくつかの質問をしました。 1.Keepalived は MySQL インスタンスの到達可能性に基づいて判断します。ヘルスチェックに問題がある可能性がありますか? ただし、このテスト シナリオでは、MySQL コンテナが破棄されると、keepalived のポート検出が失敗し、keepalived も失敗します。 keepalived も終了した場合、VIP は自動的にプリエンプトされるはずです。 2 つの仮想マシン ノードの情報を比較すると、VIP が実際に切り替えられたことがわかりました。 2. ビジネス プロセスが配置されているコンテナーはネットワーク上でアクセス不可能ですか? コンテナに入り、切り替え後のフローティング IP とポートで Telnet テストを実行してみてください。アクセスがまだ成功していることがわかります。 接続プール前の 2 つの疑わしい点をトラブルシューティングした後は、ビジネス サービスの DB クライアントに注意を向けるしかありません。 ログから、障害が発生したときにビジネス側で次のようないくつかの例外が発生したことがわかります。
ここでのプロンプトは、ビジネス オペレーションが接続を取得するためにタイムアウトした (30 秒を超える) ことを示しています。ということは、接続数が足りないということでしょうか? ビジネス アクセスでは、市場でも非常に人気のあるコンポーネントである hikariCP 接続プールが使用されます。 次に、現在の接続プールの構成を次のように確認しました。 //アイドル接続の最小数 spring.datasource.hikari.minimum-idle=10 //接続プールの最大サイズ spring.datasource.hikari.maximum-pool-size=50 // 接続の最大アイドル時間 spring.datasource.hikari.idle-timeout=60000 //接続の有効期間 spring.datasource.hikari.max-lifetime=1800000 //接続タイムアウトの長さを取得します spring.datasource.hikari.connection-timeout=30000 hikari 接続プールは minimum-idle = 10 に設定されていることに注意してください。つまり、ビジネスがない場合でも、接続プールは 10 個の接続を保証する必要があります。さらに、現状の業務アクセス量は極めて少なく、接続数が不足するような状況は発生しないはずです。 さらに、別の可能性として、「ゾンビ接続」の出現が考えられます。つまり、再起動プロセス中に、接続プールがこれらの利用できない接続を解放しなかったため、利用可能な接続がなくなるのです。 開発者は「ゾンビリンク」理論を信じており、おそらく HikariCP コンポーネントのバグが原因であると考えていました... そこで、HikariCP のソース コードを読み始めたところ、アプリケーション層が接続プールから接続を要求するコードは次のようになっていることがわかりました。 パブリッククラスHikariPool { //接続オブジェクトエントリを取得します。public Connection getConnection(final long hardTimeout) throws SQLException { suspendResumeLock.acquire(); 最終的な長い開始時間 = currentTime(); 試す { // プリセットの 30 秒のタイムアウトを使用します。long timeout = hardTimeout; する { //ループに入り、指定された時間内に利用可能な接続を取得します //connectionBag から接続を取得します PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS); プールエントリが null の場合 break; // タイムアウトしました... break して例外をスローします } 最終的なlong now = currentTime(); //接続オブジェクトがクリア済みとしてマークされているか、存続条件を満たしていない場合は、接続を閉じます if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) { poolEntry が接続を切断した場合、接続が切断されます。 タイムアウト = hardTimeout - elapsedMillis(startTime); } //接続オブジェクトを正常に取得します else { metricsTracker.recordBorrowStats(プールエントリ、開始時間); poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry)、now) を返します。 } } while (タイムアウト > 0L); //タイムアウト、例外をスローします metricsTracker.recordBorrowTimeoutStats(startTime); createTimeoutException をスローします(startTime); } キャッチ(中断された例外e){ スレッド.currentThread().interrupt(); throw new SQLException(poolName + " - 接続取得中に中断されました", e); } ついに { 一時停止解除ロックを解除します。 } } } getConnection() メソッドは、接続を取得するプロセス全体を示します。ここで、connectionBag は接続オブジェクトを格納するためのコンテナ オブジェクトです。 connectionBag から取得した接続が存続条件を満たさなくなった場合は、手動で閉じられます。コードは次のとおりです。 void closeConnection(final PoolEntry poolEntry、final String closureReason) 関数は、 { //接続オブジェクトを削除します if (connectionBag.remove(poolEntry)) { 最終接続 connection = poolEntry.close(); //非同期に接続を閉じる closeConnectionExecutor.execute(() -> { 静かに接続を閉じます(接続、閉鎖の理由); // 利用可能な接続数が減少すると、接続プールを埋めるタスクがトリガーされます if (poolState == POOL_NORMAL) { フィルプール(); } }); } } 接続は、次のいずれかの条件が満たされた場合にのみ閉じられることに注意してください。
idolTimeout と maxLifeTime の両方を非常に大きな値に設定しているため、次のように isConnectionAlive メソッドでの判定のチェックに重点を置く必要があります。 パブリッククラスPoolBase { //接続が生きているかどうかを判断します boolean isConnectionAlive(final Connection connection) { 試す { 試す { //JDBC 接続の実行タイムアウトを設定します。setNetworkTimeout(connection, validationTimeout); 最終的な int 検証秒数 = (int) Math.max(1000L, validationTimeout) / 1000; //TestQueryが設定されていない場合は、JDBC4検証インターフェースを使用します。if (isUseJdbc4Validation) { connection.isValid(validationSeconds) を返します。 } //接続を検出するためにTestQuery(select 1など)ステートメントを使用する try (Statement statement = connection.createStatement()) { if (isNetworkTimeoutSupported != TRUE) { setQueryTimeout(ステートメント、検証秒数); } ステートメントを実行します(config.getConnectionTestQuery()); } } ついに { setNetworkTimeout(接続、ネットワークタイムアウト); if (isIsolateInternalQueries && !isAutoCommit) { 接続.ロールバック(); } } true を返します。 } キャッチ(例外e){ //例外が発生した場合、失敗情報をコンテキストに記録します lastConnectionFailure.set(e); logger.warn("{} - 接続 {} ({}) の検証に失敗しました。maxLifetime 値を短くすることを検討してください。", プール名、接続、e.getMessage()); false を返します。 } } } PoolBase.isConnectionAlive メソッドでは接続に対して一連の検出が実行され、例外が発生した場合は現在のスレッド コンテキストに例外情報が記録されることがわかります。その後、HikariPool が例外をスローすると、次のように、最後に失敗した検出の例外も収集されます。 プライベート SQLException createTimeoutException(long startTime) { logPoolState("タイムアウト失敗 "); metricsTracker.recordConnectionTimeout(); 文字列 sqlState = null; // 最後の接続失敗例外を取得します。final Throwable originalException = getLastConnectionFailure(); if (originalException インスタンス SQLException) { sqlState = ((SQLException) originalException).getSQLState(); } //例外をスローします。final SQLException connectionException = new SQLTransientConnectionException(poolName + " - 接続が利用できません。要求は " + elapsedMillis(startTime) + "ms 後にタイムアウトしました。", sqlState, originalException); if (originalException インスタンス SQLException) { 接続例外。次の例外を設定します ((SQLException) 元の例外); } connectionException を返します。 } ここでの例外メッセージは、基本的にビジネス サービスで表示される例外ログと一致しています。タイムアウトによって生成された「接続が利用できません。要求は xxxms 後にタイムアウトしました」というメッセージに加えて、ログには検証失敗情報も出力されます。
この時点で、アプリケーションが接続を取得するためのコードが大まかに整理されました。全体のプロセスを次の図に示します。 実行ロジックの観点から見ると、接続プールの処理に問題はありません。それどころか、多くの細部が考慮されています。非生存接続が閉じられると、removeFromBag アクションも呼び出され、接続プールから削除されるため、ゾンビ接続オブジェクトの問題は発生しません。 そうなると、私たちのこれまでの推測は間違っていたに違いありません! 不安に陥るコード分析に加えて、開発者は、現在使用されている hikariCP のバージョンが 3.4.5 であるのに対し、環境内で問題が発生しているビジネス サービスはバージョン 2.7.9 であることにも気付きました。これは、何かを示しているようです... ここでも、hikariCP バージョン 2.7.9 に何らかの未知のバグがあり、それが問題の原因になっていると仮定しましょう。 サーバー側の障害に対処する際の接続プールの動作をさらに分析するために、ローカル マシンでシミュレーションしてみました。今回は、テストに hikariCP 2.7.9 を使用し、hikariCP ログ レベルを DEBUG に設定しました。 シミュレーション シナリオでは、ローカル アプリケーションは操作のためにローカル MySQL データベースに接続します。手順は次のとおりです。
結果のログは次のようになります。
ログから、hikariCP が不良接続を正常に検出し、接続プールから追い出すことができることがわかります。MySQL を再起動すると、ビジネス操作は自動的に正常に復元されます。この結果から、hikariCP バージョン問題に基づくアイデアは再び失敗し、R&D チームは再び不安に陥りました。 雲を晴らして光を見よう問題を検証する多くの試みが失敗した後、最終的に、ビジネス サービスが配置されているコンテナー内のパケットをキャプチャして、手がかりが見つかるかどうかを確認しようとしました。 障害のあるコンテナに入り、 tcpdump -i eth0 tcp port 30052を実行してパケットをキャプチャし、サービス インターフェイスにアクセスします。 この時点で、何か奇妙なことが起こり、ネットワーク パケットが生成されませんでした。ビジネス ログには、30 秒後に接続を取得できないという例外も表示されました。 netstat コマンドを使用してネットワーク接続を確認したところ、ESTABLISHED 状態の TCP 接続は 1 つだけであることがわかりました。 つまり、現在のビジネス インスタンスと MySQL サーバーの間には接続が確立されているのに、ビジネス側がまだ利用可能な接続を報告するのはなぜでしょうか。 考えられる理由は 2 つあります。
理由 1 はすぐに反論できます。まず、現在のサービスにはタイマー タスクがありません。次に、接続が占有されている場合でも、接続プールの原則に従って、上限に達しない限り、新しいビジネス要求は接続プールに新しい接続を確立するように促す必要があります。したがって、netstat コマンドのチェックからでも、tcpdump の結果からでも、常に接続が 1 つだけであるとは限りません。 そうすると、状況 2 の可能性が非常に高くなります。この考えを念頭に置いて、Java プロセスのスレッド スタックの分析を続けます。 kill -3 pid を実行してスレッド スタックを出力し、それを分析すると、予想どおり、現在のスレッド スタックに次のエントリが見つかります。
ここでは、HikariPool-1 接続追加スレッドが常に socketRead の実行可能状態にあることが示されています。名前から判断すると、このスレッドは、HikariCP 接続プールが接続を確立するために使用するタスク スレッドです。ソケット読み取り操作は MariaDbConnection.newConnection() メソッドから実行されます。これは、MySQL 接続を確立するための mariadb-java-client ドライバー レイヤーの操作です。ReadInitialHandShakePacket 初期化は、MySQL 接続確立プロトコルのリンクです。 つまり、上記のスレッドはリンク構築の過程にあります。MariaDB ドライバーと MySQL 間のリンク構築のプロセスは次のとおりです。 MySQL リンクを構築する最初のステップは、TCP 接続 (3 ウェイ ハンドシェイク) を確立することです。クライアントは、MySQL プロトコルの初期ハンドシェイク メッセージ パケット (MySQL のバージョン番号、認証アルゴリズムなどの情報を含む) を読み取り、次に ID 認証段階に入ります。 ここでの問題は、ReadInitialHandShakePacket の初期化 (ハンドシェイク メッセージ パケットの読み取り) がソケット読み取り状態になっていることです。 この時点で MySQL リモート ホストに障害が発生すると、操作は停止します。この時点では接続は確立されていますが(ESTABLISHED状態)、プロトコルハンドシェイクとそれに続くID認証プロセスは完了していません。つまり、接続は半完成品としかみなせません(hikariCP接続プールのリストに入ることはできません)。障害のあるサービスの DEBUG ログから、次のように、接続プールに使用可能な接続がないことも確認できます。
説明する必要があるもう 1 つの質問は、このようなソケット読み取り操作をブロックすると、接続プール全体がブロックされるかどうかです。 コードを読んだ後、いくつかのモジュールを含む hikariCP の接続を確立するプロセスを整理しました。
HouseKeeper は、接続プールが初期化されてから 100 ミリ秒後に実行するようにトリガーされます。fillPool() メソッドを呼び出して、接続プールの充填を完了します。たとえば、min-idle が 10 の場合、初期化時に 10 個の接続が作成されます。 ConnectionBag は、現在の接続オブジェクトのリストを保持します。また、このモジュールは、現在の接続要件の数を評価するために、接続要求者 (待機者) のカウンターも保持します。 借用メソッドのロジックは次のとおりです。 パブリック T 借用 (長いタイムアウト、最終 TimeUnit timeUnit) は InterruptedException をスローします { // スレッドローカルから最終的な List<Object> list = threadList.get(); を取得しようとする (int i = list.size() - 1; i >= 0; i--) { ... } // 現在リクエストを待機しているタスクを計算します final int waiting = waiters.incrementAndGet(); 試す { (T bagEntry:sharedList) の場合 { bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)の場合{ //利用可能な接続が取得された場合、充填タスクがトリガーされます if (waiting > 1) { リスナー.addBagItem(待機中 - 1); } bagEntryを返します。 } } //接続が利用できません。最初に充填タスクをトリガーします listener.addBagItem(waiting); // 指定された時間内に利用可能な接続が入るのを待ちます。timeout = timeUnit.toNanos(timeout); する { 最終的な長い開始 = currentTime(); 最終的なT bagEntry = handoffQueue.poll(timeout, NANOSECONDS); bagEntry == null の場合 || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE) { bagEntryを返します。 } タイムアウト -= elapsedNanos(開始); } while (タイムアウト > 10_000); null を返します。 } ついに { ウェイターズ.decrementAndGet(); } } このメソッドは、利用可能な接続があるかどうかに関係なく、listener.addBagItem() メソッドをトリガーすることに注意してください。HikariPool はこのインターフェースを次のように実装します。 パブリック void addBagItem(final int 待機中) { final boolean shouldAdd = waiting - addConnectionQueueReadOnlyView.size() >= 0; // はい、>= は意図的です。 if (shouldAdd) { //AddConnectionExecutor を呼び出して、接続を作成するタスクを送信します。addConnectionExecutor.submit(poolEntryCreator); } それ以外 { logger.debug("{} - 接続を追加 (省略)、待機中 {}、キュー {}", poolName、待機中、addConnectionQueueReadOnlyView.size()); } } PoolEntryCreator は、次のように接続を作成するための特定のロジックを実装します。 パブリッククラスPoolEntryCreator { @オーバーライド パブリックブール呼び出し() { ロングスリープバックオフ = 250L; //接続を確立する必要があるかどうかを判断します while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) { //MySQL 接続を作成する final PoolEntry poolEntry = createPoolEntry(); プールエントリが null の場合 //接続が正常に確立され、直接戻ります。 接続バッグを追加します(プールエントリ)。 logger.debug("{} - 接続 {} を追加しました", poolName, poolEntry.connection); ログ記録プレフィックスが null の場合 logPoolState(ログ記録プレフィックス); } Boolean.TRUE を返します。 } ... } // プールは一時停止またはシャットダウンされているか、最大サイズです ブール値FALSEを返します。 } } AddConnectionExecutor はシングルスレッド設計を採用していることがわかります。新しい接続要求が生成されると、それを補完するために PoolEntryCreator タスクが非同期的にトリガーされます。 PoolEntryCreator.createPoolEntry() は、MySQL ドライバー接続を確立するすべての作業を完了しますが、この場合、MySQL 接続確立プロセスは永続的にブロックされます。したがって、後でどのように接続が取得されたとしても、新しいリンク確立タスクは常にキューに入れられ、ビジネスで利用できる接続がなくなります。 次の図は、hikariCP のリンク構築プロセスを示しています。 さて、信頼性テストに関する前のシナリオを見直してみましょう。 まず、MySQL マスター インスタンスに障害が発生し、その後、hikariCP がデッド接続を検出して解放しました。閉じられた接続を解放する際に、接続数を補充する必要があることが判明し、すぐに新しいリンク確立要求がトリガーされました。 3. 解決策問題の詳細を理解した後、私たちは主に次の 2 つの側面から最適化を検討しました。
最適化ポイント 1 については、あまり役に立たないことは誰もが認めるところです。接続がハングすると、スレッド リソースがリークされたことになり、その後のサービスの安定運用に非常に悪影響を及ぼします。また、hikariCP がすでにここで書いています。したがって、重要な解決策は、通話をブロックしないようにすることです。 mariadb-java-client の公式ドキュメントを参照したところ、ネットワーク IO タイムアウト パラメータは次のように JDBC URL で指定できることがわかりました。 具体的な参考資料: https://mariadb.com/kb/en/about-mariadb-connector-j/ 説明したように、socketTimeout はソケットの SO_TIMEOUT 属性を設定してタイムアウト期間を制御できます。デフォルトは 0 で、タイムアウトがないことを意味します。 次のように、関連するパラメータを MySQL JDBC URL に追加しました。 spring.datasource.url=jdbc:mysql://10.0.71.13:33052/appdb?socketTimeout=60000&connectTimeout=30000&serverTimezone=UTC その後、MySQLの信頼性を何度か検証したところ、接続ハング現象は発生しなくなり、問題は解決しました。 IV. 要約今回は、MySQL 接続デッドロック問題のトラブルシューティングの体験を共有しました。環境設定の作業負荷が膨大で、問題を再現する際のランダム性のため、分析プロセス全体が少し困難でした (いくつかの落とし穴にも遭遇しました)。実際、私たちは表面的な現象に簡単に混乱し、問題を解決するのが難しいと感じると、偏った考え方で問題に対処する可能性が高くなります。たとえば、このケースでは、接続プールに問題があると一般に考えられていましたが、実際には MySQL JDBC ドライバー (mariadb ドライバー) の不正確な構成が原因でした。 原則として、リソースがハングする原因となる可能性のある動作は避ける必要があります。初期段階でコードと関連する構成を徹底的に調査できれば、996 はさらに遠ざかると思います。 上記は、MySQL 接続がハングする理由の詳細な説明です。MySQL 接続がハングする理由の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。 以下もご興味があるかもしれません:
|
<<: JavaScriptの強力な演算子をいくつか見てみましょう
>>: Tomcat で server.xml と content.xml を変更した後の自動復元の問題の解決方法
目次1. typeof演算子2. インスタンスオブ演算子3. typeof と instanceof...
1. Expressライブラリとジェネレータをインストールするcmdを開いて、次のコマンドを入力しま...
準備まず、nodejs をダウンロードする必要がありますが、これは問題ないはずです。原文はwebst...
MySQL での置換例の詳細な説明replace into は insert と似ていますが、rep...
el-form フォームにルールを追加します。データにルールを定義する定義されたルールをel-for...
Ubuntu ではデフォルトで root ログインが許可されていないため、初期の root アカウン...
centos8 ディストリビューションは、BaseOS および AppStream リポジトリを通じ...
序文1.ベンチマークは、テスト オブジェクトのクラスの特定のパフォーマンス指標の定量的、再現可能、比...
Ubuntu 15.04 は MySQL リモート ポート 3306 を開きます。以下の操作はすべて...
目次1 コンテナクラウドとは何ですか? 2 Dockerの紹介3 dockerを使ってMySQLをイ...
遅延読み込み(レイジー読み込み)とプリロードは、Web 最適化によく使用される手段です。 。 1. ...
MySQL SQL ステートメントにコメントを追加できます。MySQL SQL ステートメントのコメ...
目次序文プレビュー文章グラフィックコンポーネントプロパティ機能グリッドを描く軸角度を計算するスケール...
目次概要インデックスデータ構造バイナリツリー赤黒木BツリーB+ツリーハッシュ索引InnoDB インデ...
<br />適度に画像を追加すると、Web ページがより美しくなります。 画像タグ &l...