レインボーブリッジアーキテクチャの進化 - パフォーマンス

I.はじめに

1 年前の「レインボー ブリッジ アーキテクチャの進化」では、安定性と機能性の 2 つの方向に焦点を当てました。過去 1 年間、ビジネス需要が継続的に増加し、交通量が数回急増したにもかかわらず、レインボー ブリッジは引き続き無故障の状態を維持しており、これは良好な初期結果です。このアーキテクチャの進化では、主にパフォーマンスに関する最近のアーキテクチャの調整と最適化をいくつか共有します。最大の調整は、Proxy-DB レイヤーのスレッド モードが BIO から NIO に変更され、パフォーマンスが向上したことです。具体的な変換の詳細と最適化については、以下で詳しく紹介します。

この記事を読むのにかかる時間は 20 ~ 30 分と予想されます。全体的な内容は少し退屈でわかりにくいと思います。レインボーブリッジ建築の進化に関する前回の記事 (レインボーブリッジへの道) を読むことをお勧めします。 Rainbow Bridge アーキテクチャの進化) と MySQL プロトコルに関連する基礎知識を読んでから読んでください。

2. 変形前の構造

まず、レインボーブリッジのパノラマ構造図を確認してみましょう。

プロキシ3層モジュール

プロキシ層は、フロントエンド、コア、バックエンドの 3 つの層に大別できます。

  • フロントエンド サービス公開層: Netty をサーバーとして使用し、MySQL プロトコルに従って受信および返されたデータをエンコードおよびデコードします。
  • コア機能とカーネル層: 解析、再書き込み、ルーティングなどのコア機能を通じて、データ シャーディング、読み取り/書き込み分離、シャドウ ライブラリ ルーティングなどのコア機能が実現されます。
  • バックエンド - 基礎となる DB インタラクション レイヤー: JDBC を介して、データベースとの対話、結果セットの列の変更、マージなどの操作を実装します。

BIO モードの問題

ここで、コア層は純粋なコンピューティング操作ですが、フロントエンドとバックエンドの両方には IO 操作が含まれます。フロントエンド層は Netty を使用して NIO モードでサービスを公開しますが、バックエンドはデータベース メーカーが提供する従来の JDBC ドライバー (BIO モード) を使用します。したがって、プロキシの全体的なアーキテクチャは依然として BIO モードです。BIO モデルでは、各接続を処理する独立したスレッドが必要です。このモデルには明らかな欠点がいくつかあります。

  • リソースの消費量が多い: 各リクエストは独立したスレッドを作成し、大量のスレッド オーバーヘッドを伴います。スレッドの切り替えとスケジューリングは追加の CPU を消費します。
  • スケーラビリティの制限: システム スレッドの上限の影響を受け、多数の同時接続を処理するとパフォーマンスが急激に低下します。
  • I/O ブロック: BIO モデルでは、読み取り/書き込み操作がブロックされ、スレッドが他のタスクを実行できなくなり、リソースが無駄になります。
  • 複雑なスレッド管理: スレッド管理と同期の問題により、開発とメンテナンスの難易度が高まります。

最も単純なシナリオを見てみましょう: JDBC がリクエストを開始した後、データベースがデータを返すまで、現在のスレッドはブロックされます。大量の低速クエリが発生するかデータベースに障害が発生すると、多数のスレッドがブロックされ、最終的には雪崩が発生します。前回の Rainbow Bridge アーキテクチャの進化の記事では、単一ライブラリのブロックによって引き起こされるグローバルななだれの問題を解決するためにスレッド プール分離を使用するなど、BIO モデルの下でのいくつかの問題を回避するためにいくつかの改善を行いました。

ただし、論理ライブラリの数が増えると、プロキシ内のスレッドの数も最終的には増加します。システムのスケーラビリティとスループットが課題となっています。したがって、NIO (ノンブロッキング I/O) を使用してデータベースに接続するには、JDBC ドライバーに基づいて既存のブロッキング接続をアップグレードする必要があります。

3. 変形後の構造

  • バイオ -> NIO

Proxy の全体的なアーキテクチャを BIO->NIO に変更したい場合、最も簡単な方法は、従来の BIO データベース ドライバー JDBC を NIO データベース ドライバーに置き換えることですが、調べた結果、オープンソースの NIO ドライバーがあまりないことがわかりました。基本的に最適なものはありません。最後に、ShardingSphere コミュニティ ( https://github.com/apache/shardingsphere/issues/13957 ) によって行われた以前の調査を参照した後、JDBC の代わりに Vertx を使用することにしました。Vertを使用する最初の理由。ただし、最終結果は満足のいくものではなく、Vertx の抽象アーキテクチャにより、リンクが長い場合、呼び出しスタック全体の深さが極端に大きくなってしまいます。最終的なストレス テストでのスループットの向上は 5% 未満にすぎず、多くの互換性の問題がありました。そこで、最初からやり直して、独自のデータベース ドライバーと接続プールを開発することにしました。

  • 不要なエンコードとデコードの段階をスキップする

JDBC ドライバーは MySQL のバイト データを Java オブジェクトに自動的にエンコードおよびデコードするため、プロキシはこれらの結果セットをエンコードし、何らかの処理 (メタ情報の変更、結果セットのマージ) の後に上流に返します。独自のドライバーを開発すると、エンコードとデコードのプロセスをより慎重に制御し、Proxy で処理する必要のないデータを直接上流に転送し、無意味なエンコードとデコードをスキップできます。結果セットを処理するためにプロキシを必要としないシナリオについては後で紹介します。

自社開発のNIOデータベースドライバー

データベース ドライバーは主に DB 層との対話プロトコルをカプセル化し、それを高レベル API にカプセル化します。次の 2 つの図は、java.sql パッケージの Connection と Statement のコア インターフェイスの一部です。

したがって、まずデータベースとの対話方法を理解する必要があります。MySQL を例として、Netty を使用して MySQL に接続します。簡単な対話プロセスは次のとおりです。

Netty を使用して MySQL との接続を確立した後は、MySQL プロトコルで指定されたデータ形式に従い、最初に認証してから特定のコマンド パッケージを送信するだけです。MySQL の公式ドキュメントに記載されている認証プロセスとコマンド実行プロセスは次のとおりです。

以下は MySQL ドキュメントに従ってエンコードとデコードの Handle を実装するもので、実装されたコードを簡単に見てみましょう。

  • デコード デコード

これは、MySQL から返されたデータ パケットをデコードし、長さに応じて Palyload を解析し、それを MySQLPacketPayload にカプセル化し、処理のために対応するハンドルに渡します。

  • エンコードエンコード

特定のコマンド クラスを特定の MySQL データ パッケージに変換する ここでの MySQLPacket には、MySQL のコマンド タイプに 1 対 1 で対応する複数の実装クラスがあります。

ここで、MySQLPacket をアセンブルして Netty チャネルに書き込み、エンコードされた MySQLPacketPayload を解析して ResultSet に変換するには、java.sql.Connection に似た実装クラスも必要になります。

比較的単純に見えます。対話プロセスは従来の JDBC とほぼ同じです。ただし、非同期プロセスであるため、すべての応答がコールバックを通じて返されるため、ここには 2 つの困難があります。

  • MySQL は前のコマンドが終了するまで新しいコマンドを受け入れることができないため、単一接続のコマンドのシリアル化を制御するにはどうすればよいでしょうか?
  • MySQL から返されたデータ パケットを、コマンドを開始したリクエストに 1 つずつバインドするにはどうすればよいですか?

まず、NettyDbConnection では、ロックフリーのノンブロッキング キュー ConcurrentLinkedQueue が導入されています。

コマンド送信時、実行中のコマンドがない場合は直接送信し、実行中のコマンドがある場合は直接キューに投入し、前のコマンドの処理が完了するのを待って次のコマンドの実行を促進します。指示。個々の接続コマンドのシリアル化が保証されています。

次に、NettyDbConnection はコマンドの実行時に Promise を渡します。すべての MySQL データ パケットが返された後、この Promise が設定され、コマンドを開始したリクエストに 1 つずつバインドできます。

自社開発のNIOデータベース接続プール

MySQL との連携を実装し、SQL を実行するための高度な API を提供する NettyDbConnection クラスを以前に紹介しましたが、実際の使用では、毎回接続を作成し、SQL の実行後に接続を閉じることは不可能です。したがって、接続ライフサイクルを均一に管理するには、NettyDbConnection をプールする必要があります。その機能は従来の接続プールHikariCPと似ており、基本機能の完成に基づいてパフォーマンスの最適化が大幅に行われています。

  • 接続ライフサイクルの管理と制御
  • 接続プールの動的なスケーリング
  • 完璧なモニタリング
  • 接続の非同期キープアライブ
  • タイムアウト制御
  • イベントループのアフィニティ

EventLoop アフィニティに加えて、従来のデータベース接続プールを使用したことのある人なら誰でもよく知っている機能が他にもいくつかありますが、ここではあまり詳しく説明しません。ここでは主にEventLoopの親和性について紹介します。

記事の冒頭で、プロキシ、フロントエンド、コア、バックエンドの 3 層モジュールについて説明しましたが、データベースと対話するバックエンド層のコンポーネントを独自開発のドライバーに置き換えると、プロキシはNetty サーバーと Netty クライアントの両方であるため、フロントエンドとバックエンドは EventLoopGroup を共有できます。スレッド コンテキストの切り替えを減らすために、フロントエンドから単一のリクエストを受信し、コア層の計算後に MySQL に転送し、MySQL サービスの応答を受信して​​、最後にクライアントに書き戻すとき、これらの一連の操作を 1 つの EventLoop で処理する必要があります。可能な限りスレております。

具体的な方法は、バックエンドがデータベースへの接続を選択すると、現在の EventLoop にバインドされた接続を優先するというものです。これは、前述の EventLoop アフィニティであり、ほとんどのシナリオで次のリクエストが最初から最後まで同じ EventLoop によって処理されることを保証します。具体的なコードの実装を見てみましょう。

NettyDbConnectionPool クラスのマップを使用して、アイドル状態の接続を接続プールに保存します。キーは EventLoop、値は現在の EventLoop にバインドされたアイドル接続キューです。

取得時は現在のEventLoopにバインドされているコネクションが優先され、現在のEventLoopにバインドされたコネクションがない場合は他のEventLoopのコネクションを借用します。

EventLoop のヒット率を向上させるには、いくつかの構成点に注意する必要があります。

  • EventLoop スレッドの数は、CPU コアの数と一致している必要があります。
  • 接続プール内の最大接続数が EventLoop スレッドの数を超えるほど、EventLoop のヒット率は高くなります。

以下はストレステスト環境(8C16G、接続プールの最大接続数:10~30)でのヒット率監視の図ですが、ほとんどが75%前後に留まっています。

不要なコーデックをスキップする

前述したように、一部の SQL 結果セットは処理にプロキシを必要としません。つまり、MySQL から返されたデータ ストリームをそのまま上流に直接転送できるため、エンコードおよびデコード操作の必要がなくなります。では、処理にプロキシを必要としない SQL にはどのようなものがあるでしょうか? 例を挙げて説明しましょう。

論理ライブラリ A にテーブル User があり、DB1 と DB2 の 2 つのライブラリに分割されているとします。シャーディング アルゴリズムは user_id%2 です。

  • SQL1

‍SELECT id, name FROM user WHERE user_id in (1, 2)

  • SQL 2

‍SELECT id, name FROM user WHERE user_id (1)

明らかに、SQL 1 には 2 つのシャード値があるため、最終的に 2 つのノードと一致しますが、SQL 2 は 1 つのノードのみと一致します。

SQL 1 は結果セットをマージする必要があるため、エンコードとデコードをスキップできません。SQL 2 は結果セットをマージする必要はありません。結果セット内の列定義データを変更するだけで済みます。実際の行データを変更する必要はありません。この場合、Row データを上流に直接転送できます。

完全な非同期リンク

バックエンド層がオリジナルのHikariCP+JDBCを自社開発の接続プール+ドライバーに置き換えた後、フロントエンド-コア-バックエンドリンクからのすべてのブロッキング操作を非同期コーディングに置き換える必要があり、これはNettyのPromise and Futureを通じて実装されます。

シナリオによってはFutureを取得した時点で現在のFutureが完成している場合があり、毎回むやみにListenerを追加するとコールスタックが長くなってしまうため、Futureを扱うための汎用ツールクラス、つまりfutureを定義しました。 isDone() 直接実行されます。それ以外の場合は、addListener が追加され、コール スタック全体の深さが最大化されます。

互換性

上記の基本コードの変換に加えて、多くの互換性作業も行う必要があります。

  • 特殊なデータベース フィールド タイプの処理
  • JDBC URLパラメータの互換性
  • すべての ThreadLocal 関連データは ChannelHandlerContext に移行する必要があります
  • ログ MDC、TraceContext 関連のデータ転送
  • ……

4. パフォーマンス

数回のパフォーマンス ストレス テストの後、NIO アーキテクチャのパフォーマンスは、BIO アーキテクチャと比較して大幅に向上しました。

  • 全体の最大スループットが 67% 増加
  • LOADが約37%減少
  • 高負荷条件下では、BIO はプロセスの輻輳を何度も経験しますが、NIO は比較的安定しています。
  • スレッド数が約98%削減

‍5. 概要

NIO アーキテクチャを変換する作業量は非常に膨大で、その過程では紆余曲折がありましたが、最終的には満足のいく結果が得られました。カーネル レベルでの ShardingShpere 自体の高いパフォーマンスと、この NIO 変換のおかげで、Rainbow Bridge は、基本的に DAL ミドルウェアのパフォーマンスの点で最初の段階とみなすことができます。

*文 / 新一

この記事は Dewu Technology のオリジナルです。さらに興味深い記事については、Dewu Technology 公式 Web サイトを参照してください。

Dewu Technology の許可なく転載することは固く禁じられています。さもなければ、法律に従って法的責任が追及されます。

Alibaba Cloudが深刻な障害に見舞われ、全製品が影響(復旧) Tumblr がロシアのオペレーティングシステムAurora OS 5.0 を冷却新しいUIが公開 Delphi 12とC++ Builder 12、RAD Studio 12多くのインターネット企業がHongmengプログラマーを緊急採用UNIX時間17 億時代に突入しようとしている (すでに突入している) Meituan が兵力を募集し、Hongmeng システム アプリの開発を計画Amazon が Linux 上の .NET 8 への Android の依存を取り除くために Linux ベースのオペレーティング システムを開発独立した規模はFFmpeg 6.1「Heaviside」がリリースされまし
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/5783135/blog/10143389
おすすめ