はじめに
Javaで通信システムを開発しているのですが、通信システムを作るうえで重要だなと感じることが3つあります。
- ソケットプログラミングの知識
- マルチスレッドの知識
- 時間に対する考え方(タイムアウトやインターバル等)
今回は、Java言語でNIO2によるプログラミングについて書いてみたいと思います。
通信(WEBサーバも含めて)システムにおけるソケットプログラミングにおいては、まず、ソケットサーバと、ソケットクライアントの観点から考える必要があると感じています。
私が初めてソケットプログラミングをした時は、java.net.Socketやjava.net.ServerSocketを利用して作成していました。しかし、「C10K問題」が有名になり、従来のソケットプログラミングから新しいソケットプログラミングに変更しなければならないという意識が出てきました。当初は実際我々の管理している通信サーバは、中規模であり、そこまで通信の負荷がかかることもなかったのですが、利用顧客が増えてきて考えざるを得なくなったという経緯があります。
同期・非同期、ブロッキング・ノンブロッキング
この時Java言語にはnioというパッケージが追加されていて、こちらを使うということになったのですが、色々調べていくうちに、nio2と呼ばれるものがあることを知り、この技術で実現しようということになりました。しかし、このNIO2で出てくる「非同期」や「ノンブロッキング」というものがイマイチ理解できずにいました。マルチスレッドでいうブロックの概念もあり、混乱しました。NIO2でソケットプログラミングするには、まず、同期・非同期、ブロッキング・ノンブロッキングについて理解が必要だということが分かったのでした。
そして「Boost application performance using asynchronous I/O」という記事を見つけました。以下Google翻訳の結果と私が作成しなおした図をご紹介します。
基本的なLinux I/O モデルの簡略化されたマトリックス
Asynchronous I/O(以降AIO)の背後にある基本的な考え方は、プロセスがブロックしたり、完了するのを待たずに、多数のI/O操作を開始できるようにすることです。後で、またはI/Oの完了が通知された後、プロセスはI/Oの結果を取得できます。
同期ブロッキングI/O
最も一般的なモデルの1つは、同期ブロッキングI/Oモデルです。このモデルでは、アプリケーションがシステムコールを実行し、その結果、アプリケーションがブロックされます。これは、システムコールが完了するまで(データ転送またはエラー)、アプリケーションがブロックすることを意味します。呼び出し元のアプリケーションは、CPUを消費せず、応答を待つだけの状態であるため、処理の観点から効率的です。
下図は、従来のブロッキングI/Oモデルを示しています。これは、今日のアプリケーションで使用される最も一般的なモデルでもあります。その動作はよく理解されており、その使用法は一般的なアプリケーションで効率的です。「Read」システムコールが呼び出された場合は、アプリケーションブロックとコンテキストがカーネルに切り替わります。次に、「読み取り」が開始され、(読み取り元のデバイスから)応答が返されると、データは利用側のバッファーに移動されます。次に、アプリケーションのブロックが解除されます(そして「Read」呼び出しが戻ります)。
アプリケーションの観点からは、Read呼び出しは長時間にわたって行われます。ただし、実際には、読み取りがカーネル内の他の作業と多重化されている間、アプリケーションは実際にブロックされます。
同期ノンブロッキングI/O
同期ブロッキングの効率の低いモデルが、同期ノンブロッキングI/Oです。このモデルでは、デバイスはノンブロッキングとして開かれます。これは、下図に示すように、I/Oをすぐに完了する代わりにRead、コマンドがすぐに満たされなかったことを示すエラーコード(EAGAINまたは EWOULDBLOCK)を返す場合があることを意味します。
ノンブロッキングの意味するところは、I/Oコマンドがすぐに満たされない可能性があり、アプリケーションが完了を待つために多数の呼び出しを行う必要があるということです。多くの場合、アプリケーションはビジーである必要があるため、これは非常に非効率的です。データが利用可能になるまで待つか、カーネルでコマンドが実行されている間に他の作業を試みます。図にも示されているように、この方法ではI/Oに遅延が発生する可能性があります。これは、カーネルで使用可能になるデータと、利用側のデータ移動の間にギャップがあると 、全体的なデータスループットが低下する可能性があるためです。
非同期ブロッキングI/O
このモデルでは、ノンブロッキングI/Oが構成され、ブロッキング 「select」システムコールを使用して、I/O記述子のアクティビティがあるかどうかを判断します。この「select呼び出し」を興味深いものにしているのは、 1つの記述子だけでなく、多くの記述子に通知を提供するために使用できることです。記述子ごとに、データを書き込む記述子の機能、読み取りデータの可用性、およびエラーが発生したかどうかの通知を要求できます。
「select」の主な問題は、あまり効率的ではないということです。非同期通信には便利なモデルですが、高性能I/Oに使用することはお勧めしません。
非同期ノンブロッキングI/O
最後に、非同期ノンブロッキングI/Oモデルは、I/Oとの重複処理の1つです。読み取り要求はすぐに返され、「read」が正常に開始されたことを示します 。その後、アプリケーションは、バックグラウンド読み取り操作の完了中に他の処理を実行できます。ときにRead応答が到着すると、信号やスレッドベースのコールバック(おそらくこれがJavaのCompletionHandler)は、I/Oトランザクションを完了するために生成することができます。
潜在的に複数のI/O要求に対して単一のプロセスで計算とI/O処理をオーバーラップさせる機能は、処理速度とI/O速度の間のギャップを利用します。1つ以上の低速I/O要求が保留中の間、CPUは他のタスクを実行するか、より一般的には、他のI/Oが開始されている間にすでに完了したI/Oを操作できます。
以上が、同期・非同期、ブロッキング・ノンブロッキングの整理です。
NIO2は何なのか?
Java言語におけるNIO2では、何ができるのか?最終的に我々が選択すべきものは、AIOであると結論付けました。
NIO2では、AsynchronousServerSocketChannelという抽象クラスを利用します。たとえば、Accept()メソッドでは、引数にアタッチと完了ハンドラを渡します。おそらくこの完了ハンドラが非同期ノンブロッキングの説明の最後にあった「スレッドベースのコールバック」であろうということになりました。
この理解であってると思ってます・・多分。