|
この記事はWeChat公式アカウント「yes' Leveling Guide」(Yes呀著)からの転載です。転載の許可については、WeChat公式アカウント「yes' Leveling Guide」までお問い合わせください。 こんにちは、そうです。 今年最初の技術記事です。以前の記事からの抜粋です。3月と4月は面接のピークシーズンなので、今後の更新は主に面接の質問に焦点を当てます。 この記事は、Reactorが面接で聞かれる可能性のあるトピックの一つなので、実際に面接で取り上げられる可能性があります。ReactorがNettyでどのように実装されているかを見てみましょう。 これにより、Reactor とその進化についての理解が深まります。 ちなみに、Reactorの理解に関する記事を既に書いています。まだ読んでいない方は、まずそちらを読んでからこの記事を読むことをお勧めします。 さあ、始めましょう! ネッティのリアクターNetty には、bossGroup と workerGroup という 2 つのスレッド グループがあることは周知の事実です。 前述のように、bossGroup は主に新しい接続を処理し (上司が作業を引き受けます)、workerGroup は新しい接続の後続のすべての I/O (従業員が作業を実行します) を担当します。 Reactorモデルでは、bossGroup内のeventLoopがメインのReactorです。その役割は、接続イベント(OP_ACCEPT)をリッスンして待機することです。 次にサブチャネルを作成し、workerGroup から eventLoop を選択し、サブチャネルをこの eventLoop にバインドします。すると、eventLoop がこのサブチャネルに対応する I/O イベントを担当するようになります。 この workerGroup 内の eventLoop はいわゆる子 Reactor であり、そのタスクは接続が確立された後にすべての I/O 要求を処理することです。 「eventLoop」という名前が示す通り、その機能はイベントをループすることです。簡単に言うと、イベントの発生を無期限に待機し、イベントの種類に応じて異なる後続処理を実行するスレッドです。たったこれだけです。 通常、bossGroup は 1 つのイベント ループ (1 つのスレッド) のみで構成されます。これは、サービスは通常 1 つのポートのみを公開するため、このポートでリッスンして接続を受け入れる必要があるイベント ループは 1 つだけだからです。 Nettyでは、workerGroupのデフォルト値はCPUコア数の2倍です。例えば、4コアCPUの場合、workerGroupのデフォルト値は8つのeventLoopsで、これにより8つの子Reactorが作成されます。 したがって、通常の Netty サーバー構成は、1 つのマスター Reactor と複数のスレーブ Reactor (いわゆるマスター/スレーブ Reactor) で構成されます。 最近の主流の構成のほとんどは、マスター スレーブ Reactor アーキテクチャを使用しています。 原子炉モデルの進化Netty の Reactor 実装を詳しく検討する前に、まずそれがマスター/スレーブ Reactor に進化した理由を調べてみましょう。 初期のモデルは、単一のReactorと単一のスレッドで構成されていました。これは、新しい接続をリッスンし、古い接続からのリクエストに応答する単一のスレッドと考えることができます。ロジックの処理速度が速い場合は問題ありません。Redisで十分です。しかし、ロジックの処理速度が遅い場合は、他のリクエストをブロックしてしまいます。 そのため、単一のReactorでマルチスレッド化を実現しています。単一のスレッドが基盤となるすべてのソケットをリッスンしますが、時間のかかる操作はビジネス処理用のスレッドプールに割り当てることができるため、ロジック処理の遅延によってReactorがブロックされることはなくなります。 しかし、このモデルには依然としてボトルネックがあります。新しい接続のリッスンと古い接続からのリクエストへの応答の両方が単一のスレッドで処理されるのです。古い接続の数が増えると、応答すべきイベントが増え、新しい接続へのアクセスに影響が出てしまいます。これは理想的とは言えません。それに、今はマルチコアCPUが普及しているのに、本当にもう1つのスレッドが必要なのでしょうか? そのため、マスタースレーブ型のReactorへと進化しました。マスターReactorと呼ばれる1つのスレッドは、新しい接続の確立を待機する専用スレッドとして機能し、既に確立されている古い接続を均等に処理するために、複数のスレッドが子Reactorとして生成されます。これにより、新しい接続の受信速度に影響を与えることなく、マルチコアCPUをより有効に活用して古い接続の要求に応答できるようになります。 これは、Reactor モデルの進化に関するものです。 さて、次はNettyでReactorを実装するコアクラスを見てみましょう。現在は通常NIOを使用しているので、NioEventLoopクラスを見てみましょう。 注意: モバイル フォンでソース コードを表示するのはあまり快適ではないため、可能であれば、次のコンテンツを PC で表示することをお勧めします。 Nioイベントループ前述したように、NioEventLoop はスレッドであり、スレッドの中核はその run メソッドです。 私たちの理解によれば、この run メソッドの主な目的は、間違いなく無限ループし、I/O イベントの発生を待機してから、イベントを処理することであることがわかります。 実際、NioEventLoop は主に次の 3 つのことを行います。
まず、コードを折りたたんでみると、完全な無限ループになっていることがわかります。これはReactorスレッドの標準的な動作です。Reactorスレッドは、イベントの発生を待機し、その処理に全時間を費やしています。 Nettyの実装では、NioEventLoopスレッドはI/Oイベントだけでなく、送信された非同期タスク、時間指定タスク、そして末尾タスクも処理します。そのため、このスレッドはI/Oイベント処理とタスク処理に費やされる時間のバランスを取る必要があります。 そのため、selectStrategyと呼ばれる戦略があります。これは、実行待ちのタスクがあるかどうかを判断します。タスクがある場合は、直ちに非ブロッキングSELECTを実行し、I/Oイベントの取得を試みます。タスクがない場合、SelectStrategy.SELECT戦略が選択されます。 図からわかるように、この戦略は、最後にスケジュールされたタスクの実行時間に基づいて、選択の最長ブロック時間を制御します。 以下のコードからわかるように、スケジュールされたタスクが実行される時刻に基づいて、5マイクロ秒の時間ウィンドウが予約されます。時間が5マイクロ秒以内であれば、ブロックされずに非ブロッキングSELECTが実行され、直ちにI/Oイベントの取得が試みられます。 上記の操作が完了すると、selectは完了です。最終的に、準備完了したI/Oイベントの数がstrategyに割り当てられます。準備完了したI/Oイベントがない場合、strategyは0になります。その後、I/Oイベントとタスクの処理に移ります。 上記のコードの重要な部分をハイライトしました。`select` 操作の数をカウントする `selectCnt` 関数があります。これは、後述の JDK Selector における空のポーリングのバグを修正するために使用されます。 ioRatioパラメータは、I/Oイベント実行時間とタスク実行時間の比率を制御するために使用されます。スレッドは複数の処理を実行する必要があるため、それらすべてを均等に処理し、いずれか1つを無視しないようにする必要があります。 ご覧のとおり、具体的な実装では、I/O イベントの実行時間を記録し、その比率に基づいてタスクの最長実行時間を計算して、タスクの実行を制御します。 I/Oイベントの処理I/O イベントが具体的にどのように処理されるか、具体的には processSelectedKeys メソッドを見てみましょう。 クリックすると、実際には最適化バージョンと標準バージョンの 2 つの処理方法があることがわかります。 どちらのバージョンもロジックは同じです。違いは、最適化されたバージョンではselectedKeysの型が置き換えられる点です。selectedKeysのJDK実装はset型ですが、Nettyはこの型の選択にはまだ最適化の余地があると認識しています。 Netty はセット型を SelectedSelectionKeySet 型に置き換えます。これは基本的にセットを配列に置き換えるものです。 集合型と比較すると、配列の走査はより効率的です。また、集合型でもハッシュ衝突が発生する可能性があるため、配列の末尾への追加は集合への追加よりも効率的です。もちろん、これはNettyが追求する究極の低レベル最適化であり、日常のコードでそこまで細心の注意を払う必要はありません。それほど意味のあることではありません。 では、Netty はどのようにしてこのタイプを置き換えたのでしょうか? 反射。 コードを見てみましょう。それほど複雑ではありません。 これもいくつかのアイデアを与えてくれます。例えば、サードパーティ製のJARファイルを使用していて、そのソースコードを変更できないけれど拡張したい場合は、Nettyのアプローチを模倣し、リフレクションを使って置き換えることができます。 置換前と置換後の `selectedKey` の型を確認するためにブレークポイントを設定しましょう。以前は `HashSet` でした。 置き換え後はSelectedSelectionKeySetになりました。 さて、I/Oイベント処理のトラバーサルメソッドの最適化バージョンを見てみましょう。トラバーサルに配列を使用する点を除けば、ロジックは通常のバージョンと同じです。 GCを助ける部分を除いて、特に言うことはありません。オープンソースソフトウェアをたくさん見てきた方なら、このように値を直接nullに設定する実装がたくさんあることに気づくでしょう。これはGCを助けるためです。 次に、実際に I/O イベントを処理するメソッド、processSelectedKey を見てみましょう。 ご覧のとおり、このメソッドは基本的にイベントごとに異なる処理を実行します。実際には、対応するチャネルのパイプラインに沿ってイベントを伝播し、対応する様々なカスタムイベントをトリガーします。ここでは、OP_ACCEPTイベントを例に分析します。 OP_ACCEPT イベントに応答して、unsafe.read は実際に NioMessageUnsafe#read メソッドを呼び出します。 上記のコードからわかるように、ロジックは複雑ではありません。主に、新しく作成されたサブチャネルをループで読み取り、ChannelReadイベントとChannelReadCompleteイベントをトリガーしてパイプラインに伝播させるというものです。このプロセス中に、先ほど追加したServerBootstrapAcceptor#channelReadがトリガーされ、サブReactorスレッドであるworkerGroupのeventLoopに割り当てられます。 もちろん、カスタム ハンドラーはこれら 2 つのイベント メソッドを実装することもできるため、対応するイベントが到着したときに対応するロジック処理を実行することができます。 これでNettyのOP_ACCEPTイベント処理の分析は終了です。他のイベントも同様で、すべて対応するイベントをトリガーし、パイプラインを通過して、論理処理のための異なるChannelhandlerメソッドをトリガーします。 上記は、Netty に実装されたマスター スレーブ Reactor モデルについて説明しています。 もちろん、Nettyは単一のReactorもサポートしていますが、workerGroupはサポートしていません。スレッド数も設定できるため、非常に柔軟です。ただし、現在ではマスター/スレーブ型のReactorモデルが一般的に使用されています。 やっとこの記事では、Netty の Reactor の実装について説明するだけでなく、Netty が I/O 操作を処理する方法についても説明します。 次の記事では、非常に重要かつ洞察に富んだ Netty のパイプラインのメカニズムについて再度取り上げます。 パイプラインを書き終えれば、Netty全体について比較的明確な理解が得られるはずです。その後、パケットのマージや部分的なパケット化、メモリ管理、そしてNettyの「高度な」使い方といった内容を書き始めます。つまり、まだ半分ほど書き残しているということです。書き終えたら、しっかりと見直してください。そうすれば、Nettyを「自慢」できるはずです。 さて、私のアップデートを待ってください。今のところはこれだけです。 |