08 - ネットワーク通信最適化のための IO モデル: 同時実行性が高い場合の IO ボトルネックを解決するにはどうすればよいですか?

Java I/O に関しては、よくご存じだと思います。I/O 操作を使用してファイルの読み取りと書き込みを行うことも、ソケット情報送信の実装に使用することもできます。これらは、システム内で最も頻繁に遭遇する I/O に関連する操作です。

I/O の速度がメモリの速度よりも遅いことは誰もが知っています。特に現在のビッグデータの時代では、I/O のパフォーマンスの問題が特に顕著であり、I/O の読み取りと書き込みがシステムになっています。多くのアプリケーション シナリオでパフォーマンスのボトルネックを無視することはできません。

今日は、同時実行性の高いビッグデータのビジネスシナリオにおいて Java I/O によって明らかになるパフォーマンスの問題を詳しく見て、ソースから始めて、最適化方法を学びましょう。

1. I/Oとは何ですか

I/O はマシンが情報を取得して交換するための主要なチャネルであり、ストリームは I/O 操作を完了するための主な方法です。

コンピューティングにおいて、ストリームは情報の変換です。ストリームには順序があるため、通常、あるマシンやアプリケーションに比べて、マシンやアプリケーションが外部から受け取る情報を入力ストリーム(InputStream)、マシンやアプリケーションから出力される情報を出力ストリーム( OutputStream)、総称して入力/出力ストリーム (I/O ストリーム) と呼ばれます。

マシンやプログラム間で情報やデータをやり取りする場合、オブジェクトやデータは必ず一定の形式のストリームに変換され、そのストリームが送信されることにより、指定されたマシンやプログラムに到達した後、ストリームはオブジェクトデータに変換されます。したがって、ストリームは、データ交換と送信を実現できるデータキャリアとみなすことができます。

Java の I/O 操作クラスは java.io パッケージの下にあります。InputStream、OutputStream、Reader、および Writer は I/O パッケージ内の 4 つの基本クラスで、それぞれバイト ストリームと文字ストリームを処理します。以下に示すように:

 私の経験を振り返ってみると、初めて Java I/O ストリームのドキュメントを読んだとき、次のような疑問を抱いたことを覚えています。それをここで共有します。つまり、「ファイルの読み取りや書き込み、またはネットワーク送信であるかどうか」ストレージユニットはすべてバイトであるのに、なぜ I/O ストリーム操作がバイト ストリーム操作と文字ストリーム操作に分けられるのですか?

文字をバイトにトランスコードする必要があることはわかっていますが、このプロセスには非常に時間がかかります。エンコードの種類が分からないと、文字化けが発生しやすくなります。したがって、I/O ストリームは文字を直接操作するためのインターフェイスを提供し、通常時に文字に対するストリーム操作を実行するのに便利です。「バイトストリーム」と「文字ストリーム」をそれぞれ理解しましょう。

1.1、バイトストリーム

InputStream/OutputStream はバイト ストリームの抽象クラスであり、これら 2 つの抽象クラスはいくつかのサブクラスを派生しており、サブクラスごとに異なる操作タイプを処理します。ファイルの読み取りおよび書き込み操作の場合は、FileInputStream/FileOutputStream を使用します。配列の読み取りおよび書き込み操作の場合は、ByteArrayInputStream/ByteArrayOutputStream を使用します。通常の文字列の読み取りおよび書き込み操作の場合は、BufferedInputStream/BufferedOutputStream を使用します。具体的な内容を以下の図に示します。

1.2、文字ストリーム

Reader/Writer は、文字ストリームの抽象クラスです。これら 2 つの抽象クラスは、いくつかのサブクラスも派生します。異なるサブクラスは、異なる種類の操作を処理します。具体的な内容を次の図に示します。

2. 従来の I/O のパフォーマンスの問題

I/O 操作はディスク I/O 操作とネットワーク I/O 操作に分けられることがわかっています。前者はディスクからデータソースを読み取ってメモリに入力し、読み取った情報を物理ディスクに永続化します。後者はネットワークから情報を読み取ってメモリに入力し、最後にネットワークに情報を出力します。 。しかし、ディスク I/O であってもネットワーク I/O であっても、従来の I/O には重大なパフォーマンスの問題があります。

2.1、複数のメモリコピー

従来の I/O では、InputStream を通じてソース データからバッファにデータ ストリームを読み取り、OutputStream を通じてそのデータを外部デバイス (ディスクやネットワークなど) に出力できます。次の図に示すように、まずオペレーティング システムでの入力操作の具体的なプロセスを確認します。

  • JVM は read() システム コールを発行し、read システム コールを通じてカーネルへの読み取りリクエストを開始します。
  • カーネルは読み取りコマンドをハードウェアに送信し、読み取りの準備ができるまで待ちます。
  • カーネルは、読み取られるデータを指定されたカーネル キャッシュにコピーします。
  • オペレーティング システム カーネルはデータをユーザー空間バッファにコピーし、read システム コールが戻ります。

このプロセスでは、データはまず外部デバイスからカーネル空間にコピーされ、次にカーネル空間からユーザー空間にコピーされるため、2 回のメモリ コピー操作が行われます。この操作により、不必要なデータのコピーとコンテキストの切り替えが発生し、I/O パフォーマンスが低下します。

2.2、ブロッキング

従来の I/O では、InputStream の read() は while ループ操作であり、データが読み取られるまで待機し、データの準備ができるまで戻りません。これは、準備ができたデータがない場合、読み取り操作は常に中断され、ユーザー スレッドがブロックされることを意味します。

接続要求が少ない場合にはこの方法でも問題なく、応答速度も速いです。ただし、大量の接続要求が発生した場合には、待機スレッドを大量に作成する必要があり、その際、スレッドにデータが用意されていない場合、スレッドは一時停止され、ブロック状態になります。スレッドがブロックされると、これらのスレッドは CPU リソースを取得し続けるため、多数の CPU コンテキストの切り替えが発生し、システム パフォーマンスのオーバーヘッドが増加します。

3. I/O 操作を最適化する方法

上記の 2 つのパフォーマンスの問題に直面して、プログラミング言語が最適化されただけでなく、各オペレーティング システムの I/O もさらに最適化されました。JDK1.4 は java.nio パッケージ (New I/O の略) をリリースし、NIO のリリースにより、メモリのコピーとブロックによって引き起こされる深刻なパフォーマンス問題が最適化されました。JDK1.7ではNIO2を再度リリースし、オペレーティングシステムレベルで実現する非同期I/Oを提案しました。具体的な最適化の実装を見てみましょう。

3.1. バッファーを使用して読み取りおよび書き込みストリーム操作を最適化する

従来のI/Oでは、データをバイト単位で処理するストリームベースのI/O実装であるInputStreamとOutputStreamが提供されています。

NIOは従来のI/Oとは異なり、ブロック(Block)を基本単位としてデータを処理します。NIO では、2 つの最も重要なコンポーネントはバッファー (Buffer) とチャネル (Channel) です。バッファはメモリの連続ブロックであり、NIO がデータを読み書きする通過点です。チャネルは、バッファされたデータの送信元または宛先を表し、バッファされたデータまたは書き込まれたデータを読み取るために使用され、バッファされたデータにアクセスするためのインターフェイスです。

従来の I/O と NIO の最大の違いは、従来の I/O はストリーム指向であるのに対し、NIO はバッファ指向であることです。従来の方法ではファイルを読み取りながらデータを処理しますが、バッファーはファイルを一度にメモリに読み取り、その後の処理を実行できます。従来の I/O も BufferedInputStream などのバッファ ブロックを使用しますが、それでも NIO に匹敵するものではありません。NIO を使用して従来の I/O 操作を置き換えると、システム全体のパフォーマンスが向上し、その効果はすぐに現れます。

3.2. DirectBuffer を使用してメモリのコピーを削減する

NIO の Buffer は、バッファ ブロックの最適化に加えて、物理メモリに直接アクセスできる DirectBuffer クラスも提供します。Ordinary Buffer は JVM ヒープ メモリを割り当てますが、DirectBuffer は物理メモリを直接割り当てます。

データを外部デバイスに出力するには、まずデータをユーザー空間からカーネル空間にコピーしてから、出力デバイスにコピーする必要があることがわかっていますが、DirectBuffer はカーネル空間から外部デバイスにコピーする手順を直接簡素化し、データのコピーを削減します。

ここでさらに説明すると、DirectBuffer は非 JVM 物理メモリに適用されるため、作成と破棄のコストが非常に高くなります。DirectBuffer によって要求されたメモリは、JVM によるガベージ コレクションに直接関与しませんが、DirectBuffer ラッパー クラスがリサイクルされると、メモリ ブロックは Java リファレンス メカニズムを通じて解放されます。

3.3. ブロックを回避し、I/O 操作を最適化する

NIO では、その特性をよりよく反映できるため、これをノンブロック I/O、つまりノンブロッキング I/O と呼ぶ人も多くいます。なぜそんなことを言うのですか?

従来の I/O がバッファ ブロックを使用する場合でも、ブロッキングの問題は依然として存在します。スレッド プール内のスレッドの数は限られているため、多数の同時リクエストが発生すると、最大数を超えるスレッドは、スレッド プール内に再利用できるアイドル状態のスレッドができるまで待機することしかできません。ソケットの入力ストリームを読み取る場合、次の 3 つの状況のいずれかが発生するまで、読み取りストリームはブロックされます。

  • 読み取るデータがある。
  • 接続の解放。
  • Null ポインターまたは I/O 例外。

ブロッキングの問題は、従来の I/O の最大の欠点です。NIO のリリース後、チャネルとマルチプレクサーの 2 つの基本コンポーネントによって NIO のノンブロッキングが実現されました。これら 2 つのコンポーネントの最適化原理を一緒に見てみましょう。

3.3.1.チャンネル(チャンネル)

前に説明したように、従来の I/O のデータの読み書きはユーザー空間からカーネル空間にコピーされますが、カーネル空間のデータはオペレーティング システム レベルで I/O インターフェイスを介してディスクから読み書きされます。

アプリケーションプログラムがOSのI/Oインターフェースを呼び出す際、最初はCPUが割り当てを行うのですが、この方式の最大の問題点は「大量のI/Oリクエストが発生するとCPUを非常に消費してしまう」という点です。 ; その後、オペレーティング システムは DMA (ダイレクト メモリ ストレージ) を導入し、カーネル空間とディスク間のアクセスは完全に DMA によって行われますが、この方法でも CPU からの許可を申請する必要があり、DMA バスを使用する必要があります。 DMA バスが多すぎると、バス競合が発生します。

チャネルの出現により、上記の問題は解決され、チャネルには独自のプロセッサがあり、カーネル空間とディスク間の I/O 操作を完了できます。NIO ではチャネルを通じてデータの読み書きを行いますが、チャネルは双方向であるため、読み書きを同時に行うことができます。

3.3.2、マルチプレクサ(セレクタ)

セレクターは Java NIO プログラミングの基礎です。これは、1 つ以上の NIO チャネルのステータスが読み取り可能および書き込み可能かどうかを確認するために使用されます。

Selector はイベント駆動型の実装に基づいています。Selector に accpet および read 監視イベントを登録でき、Selector は登録されているチャネルを継続的にポーリングします。チャネルで監視イベントが発生すると、チャネルは準備完了状態になり、その後、I/O 操作を続行します。

スレッドはセレクターを使用して、ポーリングによって複数のチャネル上のイベントをリッスンします。チャネルの登録時にチャネルを非ブロックに設定できます。チャネル上で I/O 操作がない場合、スレッドは永久に待機するのではなく、ブロックを避けるためにすべてのチャネルを継続的にポーリングします。

現在、オペレーティング システムの I/O 多重化メカニズムには epoll が使用されており、従来の選択メカニズムと比較して、epoll には最大接続ハンドル数 1024 の制限がありません。したがって、Selector は理論的には数千のクライアントをポーリングできます。

3.3.3

現実的なシーンを例として挙げますが、これを読むと、チャネルとセレクターがノンブロッキング I/O でどのような役割と機能を果たしているかがよりよくわかるようになります。

複数の I/O 接続リクエストをリッスンすることを、駅の入り口と比較できます。かつては、事前に駅に入場できるのは最寄りの発車列車の乗客のみで、改札員も1人しかいなかったが、他の列車の乗客が駅に入場したい場合は、駅の入り口に並ばなければならなかった。これは、スレッド プールを実装しなかった最も初期の I/O 操作と同等です。

その後、鉄道駅が改良され、さらにいくつかの改札口が追加され、さまざまな列車の乗客が対応する改札口から駅に入場できるようになりました。これは、マルチスレッドを使用して複数のリスニング スレッドを作成し、各クライアントの I/O 要求を同時に監視することと同じです。

最終的に、より多くの乗客を収容できるように鉄道駅が改修されました。各列車はより多くの乗客を運ぶことができ、列車は合理的に配置されました。乗客はグループで並ぶことがなくなり、大きな統一された改札口から駅に入場できるようになりました。改札口は 1 つです。」複数の列車の切符を同時に確認できます。この大きな改札口がSelector、列車番号がChannel、乗客がI/O flowに相当します。

4. まとめ

Java の従来の I/O は、最初は 2 つの操作ストリーム、InputStream と OutputStream に基づいて実装されます。このストリーム操作はバイト単位です。同時実行性が高く、データが大きいシナリオでは、ブロッキングが発生しやすくなります。そのため、この操作はパフォーマンスが非常に悪いです。さらに、出力データはユーザー空間からカーネル空間にコピーされ、その後出力デバイスにコピーされるため、システムのパフォーマンスのオーバーヘッドが増加します。

従来の I/O はその後、「ブロック化」というパフォーマンスの問題を最適化するためにバッファを使用し、その最小単位としてバッファ ブロックが使用されましたが、全体のパフォーマンスと比較すると、まだ満足のいくものではありませんでした。

そこで NIO がリリースされました。これはバッファ ブロックに基づくストリーム操作です。バッファに基づいて、2 つの新しいコンポーネント「パイプラインとマルチプレクサ」が追加され、ノンブロッキング I/O を実現します。NIO は、多数の I/O に適しています。 I/O 接続リクエストの場合、これら 3 つのコンポーネントを組み合わせることで、I/O の全体的なパフォーマンスが向上します。

5. 考える質問

JDK1.7 バージョンでは、Java は NIO アップグレード パッケージ NIO2 (AIO) をリリースしました。AIO は、本当の意味での非同期 I/O を実装しており、I/O 操作をオペレーティング システムに直接渡して非同期処理を行います。これは I/O 操作の最適化でもありますが、なぜ多くのコンテナ通信フレームワークが依然として NIO を使用しているのでしょうか?

おすすめ

転載: blog.csdn.net/qq_34272760/article/details/132323633