web-dev-qa-db-ja.com

高性能TCP .NET C#でのソケットプログラミング

私はこのトピックがすでに時々尋ねられることを知っています、そして私はほとんどすべてのスレッドとコメントを読みました、しかしそれでも私の問題への答えを見つけていません。

TCPサーバーとクライアントが必要で、30000以上の接続を受け入れることができ、スループットが可能な限り高くなければならない高性能ネットワークライブラリに取り組んでいます。

私はasyncメソッドを使用する必要があることをよく知っていますそして私はすでに見つけてテストしたすべての種類のソリューションを実装しました。

私のベンチマークでは、スコープ内のオーバーヘッドを回避するために最小限のコードのみが使用され、CPU負荷を最小限に抑えるためにプロファイリングを使用しました単純な最適化の余地はありません、受信ソケットのバッファーデータソケットバッファが完全にいっぱいになるのを避けるために、常に読み取られ、カウントされ、破棄されました。

ケースは非常に単純で、1つはTCPソケットはローカルホストでリッスンし、もう1つはTCPソケットはリスニングソケットに接続します(同じからプログラム、同じマシンoc。)の場合、1つの無限ループがクライアントソケットを含む256kBサイズのパケットをサーバーソケットに送信し始めます。

1000ms間隔のタイマーは、両方のソケットからコンソールにバイトカウンターを出力して帯域幅を表示し、次の測定のためにそれらをリセットします。

スイートスポットパケットサイズが256kBの場合およびソケットのバッファサイズが64kBの場合、最大のスループットが得られることに気付きました。

async/awaitタイプのメソッドで到達できました

~370MB/s (~3.2gbps) on Windows, ~680MB/s (~5.8gbps) on Linux with mono

BeginReceive/EndReceive/BeginSend/EndSendタイプのメソッドで到達できました

~580MB/s (~5.0gbps) on Windows, ~9GB/s (~77.3gbps) on Linux with mono

SocketAsyncEventArgs/ReceiveAsync/SendAsyncタイプのメソッドで到達できました

~1.4GB/s (~12gbps) on Windows, ~1.1GB/s (~9.4gbps) on Linux with mono

問題は次のとおりです。

  1. async/awaitメソッドは最も遅いだったので、それらを使用しません
  2. BeginReceive/EndReceiveメソッドは、Linux/monoの下で、BeginAccept/EndAcceptメソッドと一緒に新しい非同期スレッドを開始しましたソケットのすべての新しいインスタンスは非常に遅かったThreadPoolにスレッドがなくなったときmonoは新しいスレッドを起動しましたが、接続の25インスタンスを作成するには約5分、作成50接続は不可能でした(プログラムは停止しました) 〜30接続後のすべて)。
  3. ThreadPoolサイズを変更してもまったく役に立ちませんでしたし、変更しませんでした(単なるデバッグの動きでした)
  4. これまでのところ最良の解決策はSocketAsyncEventArgsであり、これはWindowsで最高のスループットを実現しますが、Linux/monoではWindowsよりも遅く、以前は逆でした。

WindowsとLinuxの両方のマシンを iperf でベンチマークしました。

Windows machine produced ~1GB/s (~8.58gbps), Linux machine produced ~8.5GB/s (~73.0gbps)

奇妙なことにiperfは私のアプリケーションよりも弱い結果になる可能性がありますが、Linuxでははるかに高くなります。

まず、結果が正常かどうかを知りたいのですが、別のソリューションでより良い結果を得ることができますか?

BeginReceive/EndReceiveメソッドを使用することにした場合(Linux/monoで比較的高い結果が得られました)、スレッドの問題を修正して、接続インスタンスの作成を高速化し、複数のインスタンスを作成した後のストール状態を解消するにはどうすればよいですか? ?

私はさらにベンチマークを作成し続け、新しいものがあれば結果を共有します。

=================================更新================ ==================

コードスニペットを約束しましたが、何時間も実験した後、コード全体が混乱しているので、誰かに役立つ場合に備えて、私の経験を共有したいと思います。

ウィンドウ7ではループバックデバイスが遅いiperf または NTttcp 、-で1GB/sよりも高い結果を得ることができなかったことを認識しなければなりませんでしたWindows8以降のバージョンのみが高速ループバックを備えていますので、新しいバージョンでテストできるようになるまで、Windowsの結果についてはもう気にしません。 SIO_LOOPBACK_FAST_PATHSocket.IOControl を介して有効にする必要がありますが、Windows7では例外がスローされます。

最も強力なソリューションは、完了イベントベースの SocketAsyncEventArgs WindowsとLinux/Monoの両方での実装であることが判明しました。クライアントの数千のインスタンスを作成しても、ThreadPoolが台無しになることはありません。前述のように、プログラムは突然停止しませんでした。この実装は、スレッド化に非常に適しています。

リスニングソケットへの接続を10個作成し、ThreadPoolから10個の個別のスレッドからクライアントと一緒にデータを供給すると、Windowsでは~2GB/sデータトラフィックが生成され、Linux/Monoでは~6GB/sが生成される可能性があります。

クライアント接続数を増やしても全体的なスループットは向上しませんでしたが、合計トラフィックが接続間で分散されました。これは、5、10、または200クライアントでもすべてのコア/スレッドでCPU負荷が100%だったことが原因である可能性があります。

全体的なパフォーマンスは悪くないと思います。100のクライアントがそれぞれ約~500mbit/sのトラフィックを生成する可能性があります。 (もちろん、これはローカル接続で測定されます。ネットワーク上の実際のシナリオは異なります。)

私が共有する唯一の観察結果:ソケットの入出力バッファーサイズとプログラムの読み取り/書き込みバッファーサイズ/ループサイクルの両方を実験すると、パフォーマンスに大きな影響があり、WindowsとLinux/Monoでは大きく異なります。

Windowsの場合128kB socket-receive32kB socket-send16kB program-read、および64kB program-writeバッファで最高のパフォーマンスが達成されました。

Linuxの場合以前の設定ではパフォーマンスが非常に弱くなりましたが、512kB socket-receive and -sendの両方、256kB program-read128kB program-writeのバッファサイズが最適でした。

今私の唯一の問題は、10000の接続ソケットを作成しようとすると、7005前後でインスタンスの作成が停止し、例外がスローされず、問題がなかったためプログラムが実行されていることですが、どうすればよいかわかりませんforなしで特定のbreakループを終了しますが、終了します。

私が話していたことに関して何か助けていただければ幸いです!

5
beatcoder

この質問は多くの意見を集めているので、「回答」を投稿することにしましたが、技術的にはこれは回答ではありませんが、今のところ最終的な結論なので、回答としてマークします。

アプローチについて:

async/await関数は待機可能な非同期を生成する傾向がありますTasks dotnetランタイムのTaskSchedulerに割り当てられるため、数千の同時したがって、接続、つまり数千または読み取り/書き込み操作により、数千のタスクが開始されます。私の知る限り、これにより、RAMに格納された数千のStateMachineと、割り当てられたスレッドでの無数のコンテキストスイッチングが作成され、CPUのオーバーヘッドが非常に高くなります。いくつかの接続/非同期呼び出しを使用すると、バランスが改善されますが、待機可能なタスク数が増えると、指数関数的に遅くなります。

BeginReceive/EndReceive/BeginSend/EndSendソケットメソッドは技術的には非同期メソッドであり、待機可能なタスクはありませんが、呼び出しの最後にコールバックがあり、実際にはマルチスレッドがさらに最適化されますが、これらのソケットメソッドのドットネット設計の制限は私の意見では不十分です、しかし、単純な解決策(または接続の数が限られている)の場合は、それが進むべき道です。

SocketAsyncEventArgs/ReceiveAsync/SendAsyncソケット実装のタイプは、理由からWindowsに最適です。バックグラウンドでWindows IOCPを利用して、最速の非同期ソケット呼び出しを実現し、オーバーラップI/Oと特別なソケットモードを使用します。このソリューションは、Windowsで「最も簡単」で最速です。しかし、mono/linuxでは、monoがlinux epollを使用してWindows IOCPをエミュレートするため、それほど高速になることはありません。これは実際にはIOCPよりもはるかに高速ですが、エミュレートする必要があります。ドットネット互換性を実現するためのIOCP。これによりオーバーヘッドが発生します。

バッファサイズについて:

ソケット上のデータを処理する方法は無数にあります。読み取りは簡単で、データが到着します。データの長さはわかっています。ソケットバッファからアプリケーションにバイトをコピーして処理するだけです。データの送信は少し異なります。

  • 完全なデータをソケットに渡すと、データがチャンクにカットされ、送信するデータがなくなるまでチャックがソケットバッファーにコピーされ、すべてのデータが送信されたとき(またはエラーが発生したとき)にソケットの送信方法が返されます。 )。
  • データを取得してチャンクにカットし、チャンクを使用してソケットsendメソッドを呼び出し、データが戻ったら、次のチャンクがなくなるまで送信します。

いずれの場合も、選択するソケットバッファサイズを検討する必要があります。大量のデータを送信する場合、バッファが大きいほど、送信する必要のあるチャンクが少なくなります。したがって、Your(またはソケットの内部)ループで呼び出す必要のある呼び出しが少なくなり、メモリコピーが少なくなり、オーバーヘッドが少なくなります。ただし、大きなソケットバッファとプログラムデータバッファを割り当てると、特に数千の接続があり、大きなメモリを複数回割り当てる(および解放する)には常にコストがかかる場合に、メモリ使用量が大きくなります。

送信側では、ほとんどの場合、1-2-4-8kBソケットバッファサイズが理想的ですが、大きなファイル(数MB以上)を定期的に送信する準備をしている場合は、16-32-64kBバッファサイズが最適です。 64kBを超えると、通常は意味がありません。

ただし、これには、受信側に比較的大きな受信バッファがある場合にのみ利点があります。

通常、インターネット接続(ローカルネットワークではない)を介して32kBを超えることはできませんが、16kBでも理想的です。

4〜8 kBを下回ると、読み取り/書き込みループで呼び出しカウントが指数関数的に増加し、CPU負荷が大きくなり、アプリケーションでのデータ処理が遅くなる可能性があります。

メッセージが通常4kBより小さいか、ごくまれに4KBを超えることがわかっている場合にのみ、4kBを下回ります。

私の結論:

私の実験に関しては、dotnetに組み込まれているソケットクラス/メソッド/ソリューションは問題ありませんが、まったく効率的ではありません。非ブロッキングソケットを使用する私の単純なLinuxCテストプログラムは、ドットネットソケット(SocketAsyncEventArgs)の最速で「高性能」なソリューションを上回る可能性があります。

これは、dotnetで高速のソケットプログラミングを行うことが不可能であることを意味するわけではありませんが、Windowsでは、InteropServices/Marshalingを介してWindowsカーネルと直接通信によってWindowsIOCPの独自の実装を作成する必要がありましたWinsock2メソッドを直接呼び出す、多くの安全でないコードを使用して、クラス/呼び出し間のポインターとして接続のコンテキスト構造体を渡し、独自のThreadPoolを作成し、IOイベントハンドラースレッドを作成する、独自のTaskSchedulerを作成して、同時非同期呼び出しの数を制限し、無意味に多くのコンテキスト切り替えを回避します。

これは多くの研究、実験、テストを伴う多くの仕事でした。あなたが自分でそれをしたいのなら、あなたが本当にそれが価値があると思う場合にのみそれをしてください。安全でない/管理されていないコードとマネージコードを混在させることはお尻の痛みですが、このソリューションを使用すると、Windows7の1ギガビットLANで約36000httpリクエスト/秒に自分のhttpサーバーで到達できるため、それだけの価値があります。 i74790。

これは、dotnetの組み込みソケットでは到達できないほどの高性能です。

Windows10のi97900Xでdotnetサーバーを実行し、4c/8tIntelに接続している場合Atom NAS Linuxの場合、10gbit lan経由で、同時接続が1つでも10000でも、完全な帯域幅(したがって、1GB /秒でデータをコピー)。

私のソケットライブラリは、コードがLinuxで実行されているかどうかも検出し、Windows IOCPの代わりに(明らかに)InteropServices/Marshallingを介してLinuxカーネル呼び出しを使用して、ソケットを作成、使用し、Linuxepollで直接ソケットイベントを処理します。テストマシンのパフォーマンスを最大化します。

設計のヒント:

結局のところ、ネットワークライブラリを最初から設計することは困難であり、特に1つは、あらゆる目的で非常に普遍的である可能性があります。多くの設定を持つように、または特に必要なタスクに合わせて設計する必要があります。これは、適切なソケットバッファーサイズ、I/O処理スレッド数、ワーカースレッド数、許可された非同期タスク数を見つけることを意味します。これらはすべて、アプリケーションが実行されているマシンと接続数、およびデータ型に合わせて調整する必要があります。ネットワークを介して転送したい。これが、組み込みソケットがユニバーサルである必要があり、これらのパラメーターを設定できないため、パフォーマンスがそれほど良くない理由です。

私の場合、I/Oイベント処理に2つ以上の専用スレッドを割り当てると、RSSキューが2つしかないため、実際には全体的なパフォーマンスが低下し、理想よりも多くのコンテキスト切り替えが発生します。

間違ったバッファサイズを選択すると、パフォーマンスが低下します。

シミュレートされたタスクのさまざまな実装を常にベンチマークするどのソリューションまたは設定が最適かを見つける必要があります。

設定が異なると、マシンやオペレーティングシステムによってパフォーマンス結果が異なる場合があります。

Mono vs Dotnet Core:

ソケットライブラリをFW/Core互換の方法でプログラムしたので、Linuxでモノラルとコアネイティブコンパイルを使用してテストできました。最も興味深いことに、パフォーマンスの顕著な違いは見られませんでした。どちらも高速でしたが、もちろん、モノラルのままにしてコアでコンパイルすることをお勧めします。

ボーナスパフォーマンスのヒント:

ネットワークカードがRSS(Receive Side Scaling)に対応している場合は、Windowsの詳細プロパティのネットワークデバイス設定でRSSキューを有効にし、RSSキューを1から可能な限り高く設定するか、パフォーマンスに最適です。

ネットワークカードでサポートされている場合、通常は1に設定されます。これにより、カーネルが1つのCPUコアのみで処理するようにネットワークイベントが割り当てられます。このキュー数をより多くの数に増やすことができれば、ネットワークイベントをより多くのCPUコアに分散し、パフォーマンスが大幅に向上します。

Linuxでは、これを設定することもできますが、さまざまな方法で、Linuxディストリビューション/ LANドライバー情報を検索することをお勧めします。

私の経験があなたの一部に役立つことを願っています!

2
beatcoder