GNU/Linux >> Linux の 問題 >  >> Linux

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

この質問は多くのビューを獲得するため、「回答」を投稿することにしましたが、技術的にはこれは回答ではありませんが、現時点での最終的な結論なので、回答としてマークします.

アプローチについて:

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

BeginReceive/EndReceive/BeginSend/EndSend ソケット メソッドは技術的には待機可能なタスクのない非同期メソッドですが、呼び出しの最後にコールバックがあり、実際にはマルチスレッドをより最適化しますが、それでもこれらのソケット メソッドの dotnet 設計の制限は私の意見では貧弱ですが、単純なソリューションの場合は(または限られた数の接続) それが道です。

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

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

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

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

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

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

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

通常、インターネット接続 (ローカル ネットワークではない) では、32kB を超えるポイントはなく、16kB が理想的です。

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

メッセージが通常 4kB 未満であるか、ごくまれに 4KB を超えることがわかっている場合にのみ、4kB 未満にしてください。

私の結論:

私の実験では、dotnet のビルトイン ソケット クラス/メソッド/ソリューションは問題ありませんが、まったく効率的ではありません。ノンブロッキング ソケットを使用する単純な Linux C テスト プログラムは、ドットネット ソケット (SocketAsyncEventArgs) の最速で「高性能」なソリューションよりも優れたパフォーマンスを発揮する可能性があります。 ).

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

これは、多くの調査、実験、テストを伴う大変な作業でした。自分でやりたい場合は、本当にそれだけの価値があると思う場合にのみ実行してください。 unsafe/unmanage コードとマネージ コードを混在させるのは面倒ですが、最終的にはそれだけの価値があります。なぜなら、このソリューションを使用すると、Windows 7 の 1 ギガビット LAN で、自分の http サーバーで約 36000 http リクエスト/秒に到達できるからです。 i7 4790.

これは非常に高いパフォーマンスであり、dotnet 組み込みソケットでは到達できませんでした。

Windows 10 の i9 7900X で dotnet サーバーを実行し、Linux の 4c/8t Intel Atom NAS に 10 ギガビット LAN 経由で接続すると、帯域幅全体を使用できます (したがって、1GB/秒でデータをコピーします)。 1 または 10000 の同時接続。

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

デザインのヒント:

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

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

不適切なバッファ サイズを選択すると、パフォーマンスが低下します。

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

設定が異なると、異なるマシンやオペレーティング システムで異なるパフォーマンス結果が得られる場合があります!

モノ vs ドットネット コア:

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

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

お使いのネット​​ワーク カードが RSS (Receive Side Scaling) に対応している場合は、Windows の詳細プロパティのネットワーク デバイス設定で有効にし、RSS Queue を 1 からできるだけ高い値に設定します。

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

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

私の経験があなたの助けになることを願っています!


私も同じ問題を抱えていました。以下を調べてください:NetCoreServer

.NET clr スレッドプール内のすべてのスレッドは、一度に 1 つのタスクを処理できます。したがって、より多くの非同期接続/読み取りなどを処理するには、以下を使用してスレッドプール サイズを変更する必要があります:

ThreadPool.SetMinThreads(Int32, Int32)

Windows では、EAP (イベント ベースの非同期パターン) を使用する方法です。あなたが言及した問題のために、私は Linux でもそれを使用し、パフォーマンスを急落させます.

io 完了ポートが最適です ただし、移植性はありません。

PS:オブジェクトをシリアル化する場合は、protobuf-net を使用することを強くお勧めします . .NET バイナリ シリアライザーよりも最大 10 倍高速にオブジェクトをバイナリ シリアル化し、スペースも少し節約します!


Linux
  1. 方法:Pythonでのソケットプログラミング

  2. ソケットプログラミングにおける AF_INET と PF_INET の違いは何ですか?

  3. SO_SNDBUF の効果

  1. c ソケット プログラミングを使用した arp 要求と応答

  2. TCP 経由で仮想シリアル ポート接続を作成する

  3. .NET Core RC2 コンソール アプリ (Linux、Debian 8) で System.Data を使用するにはどうすればよいですか?

  1. Linux で TCP トラフィック データを盗聴する最も簡単な方法は何ですか?

  2. ゲームディレクトリ?

  3. Linux で TCP トンネルを特殊文字デバイスとして公開することはできますか?