クエリのために何千万ものオブジェクトを格納するための効率的な方法で、1秒あたりの挿入数が多いですか?


15

これは基本的に、p2pチャットネットワークでパケットの数をカウントし、パケットのタイプなどをカウントしているロギング/カウントアプリケーションです。これは、5分間で約400〜600万パケットに相当します。また、この情報の「スナップショット」のみを取得するため、5分ごとに5分以上経過したパケットのみを削除しています。したがって、このコレクションに含まれるアイテムの最大数は1,000万から1200万です。

異なるスーパーピアに300接続する必要があるため、各パケットが少なくとも300回挿入されようとしている可能性があります(おそらく、このデータをメモリに保持することが唯一の妥当なオプションである理由です)。

現在、私はこの情報を保存するために辞書を使用しています。しかし、大量のアイテムを保存しようとしているため、大きなオブジェクトヒープで問題が発生し、メモリの使用量は時間とともに継続的に増加します。

Dictionary<ulong, Packet>

public class Packet
{
    public ushort RequesterPort;
    public bool IsSearch;
    public string SearchText;
    public bool Flagged;
    public byte PacketType;
    public DateTime TimeStamp;
}

mysqlを使用しようとしましたが、挿入する必要があるデータ量に対応できませんでした(重複していないことを確認しながら)。それはトランザクションの使用中です。

私はmongodbを試しましたが、そのためのCPU使用は非常識で、どちらも保持しませんでした。

5分以上経過したすべてのパケットを削除し、このデータの「スナップショット」を取得するため、私の主な問題は5分ごとに発生します。LINQクエリを使用して、特定のパケットタイプを含むパケットの数をカウントしているため。また、データのdistinct()クエリを呼び出しています。ここでは、keyvaluepairのキーから4バイト(IPアドレス)を取り除き、keyvalupairのValueのrequestingport値と組み合わせて、それを使用して、すべてのパケットからのピア。

アプリケーションは現在、約1.1GBのメモリ使用量を保持しており、スナップショットが呼び出されると、使用量を2倍にまで増やすことができます。

これで、異常な量のRAMがあれば問題になりませんが、現在実行しているvmは2GBのRAMに制限されています。

簡単な解決策はありますか?


非常にメモリを集中的に使用するシナリオであり、さらに、アプリケーションの実行にvmを使用しています。とにかく、パケットを保存するためにmemcachedを調べましたか。基本的に、memcachedは別のマシンで実行でき、アプリケーションはvm自体で実行し続けることができます。

すでにMySQLとMongoDBの両方を試したことがあるので、アプリケーションの要件により(適切に実行したい場合)、単により多くの処理能力が必要であると思われるかもしれません。アプリケーションが重要な場合は、サーバーを強化してください。また、「パージ」コードを再検討することもできます。アプリを使用できなくしない限り、より最適化された処理方法を見つけることができると確信しています。
マットベックマン

4
プロファイラーから何がわかりますか?
ジャソンク

ローカルヒープより速くなることはありません。私の提案は、パージ後にガベージコレクションを手動で呼び出すことです。
バルテック

@vartec-実際のところ、一般的な信念に反して、手動でガベージコレクターを呼び出すことは、実際に即時の適切なガベージコレクションを保証するものではありません。GCは、独自のgcアルゴリズムに従ってアクションを後の期間に延期する場合があります。5分ごとに呼び出すと、負担が軽減される代わりに、負担が増える可能性があります。ちょうど言って;)
Jas

回答:


12

1つの辞書を持ち、その辞書を検索して古すぎるエントリを探す代わりに、10個の辞書があります。30秒ごとに新しい「現在の」辞書を作成し、検索をまったく行わずに最も古い辞書を破棄します。

次に、最も古い辞書を破棄するときは、すべての古いオブジェクトを後でFILOキューに入れ、「new」を使用して新しいオブジェクトを作成する代わりに、FILOキューから古いオブジェクトを引き出し、古いオブジェクトを再構築する方法を使用しますオブジェクト(古いオブジェクトのキューが空でない場合)。これにより、大量の割り当てとガベージコレクションのオーバーヘッドを回避できます。


1
タイムスライスによるパーティション分割!ちょうど私が提案しようとしていたもの。
ジェームズアンダーソン

これに関する問題は、最後の5分間に作成されたすべての辞書を照会する必要があることです。300の接続があるため、同じパケットが少なくとも1回はそれぞれに到着します。したがって、同じパケットを複数回処理しないようにするには、少なくとも5分間はそれらを保持する必要があります。
ジョシュ

1
汎用構造の問題の一部は、特定の目的のためにカスタマイズされていないことです。おそらく、「nextItemForHash」フィールドと「nextItemForTimeBucket」フィールドをPacket構造に追加し、独自のハッシュテーブルを実装して、Dictionaryの使用を停止する必要があります。これにより、古すぎるすべてのパケットをすばやく見つけ、パケットが挿入されたときに1回だけ検索できます(つまり、ケーキを食べて食べます)。また、メモリ管理のオーバーヘッドにも役立ちます(「ディクショナリ」がディクショナリ管理用の追加のデータ構造を割り当てたり解放したりしないため)。
ブレンダン

@Joshは、以前に何かを見たことがあるかどうかを判断する最も速い方法がhashsetです。タイムスライスハッシュセットは高速で、古いアイテムを削除するために検索する必要はありません。まだ見たことがない場合は、辞書に保存できます。
基本的な


3

頭に浮かぶ最初の考えは、5分間待つ理由です。スナップショットをより頻繁に行い、5分の境界で見られる大きな過負荷を軽減できますか?

第二に、LINQは簡潔なコードには最適ですが、実際にはLINQは「通常の」C#の構文糖衣であり、最適なコードを生成する保証はありません。LINQを使用せずにホットスポットを書き換えることができる練習として、パフォーマンスを向上させることはできませんが、何をしているかを明確に把握でき、プロファイリング作業が容易になります。

もう1つ注目すべきは、データ構造です。データで何をするのかわかりませんが、保存するデータを単純化できますか?文字列またはバイト配列を使用し、必要に応じてそれらの項目から関連部分を抽出できますか?クラスの代わりに構造体を使用し、stackallocで何か悪いことをしてメモリを確保し、GCの実行を回避することさえできますか?


1
BitArrayのような文字列/バイト配列、使用のものを使用しないでください:msdn.microsoft.com/en-us/library/...を手動でビット責めすることを避けるために。それ以外の場合、これは良い答えです。より良いアルゴリズム、より多くのハードウェア、またはより良いハードウェア以外の本当に簡単なオプションはありません。
エドジェームズ

1
5分間は、これらの300の接続が同じパケットを受信する可能性があるためです。そのため、すでに処理した内容を追跡する必要があります。5分は、この特定のネットワーク上のすべてのノードにパケットが完全に伝播するのにかかる時間です。
ジョシュ

3

簡単なアプローチ:memcachedを試してください。

  • このようなタスクを実行するように最適化されています。
  • 専用のボックスだけでなく、使用率の低いボックスでも予備メモリを再利用できます。
  • これには、キャッシュの有効期限メカニズムが組み込まれています。

欠点は、メモリベースであり、永続性がないことです。インスタンスがダウンすると、データはなくなります。永続性が必要な場合は、自分でデータをシリアル化します。

より複雑なアプローチ:Redisを試してください。

  • このようなタスクを実行するように最適化されています。
  • キャッシュの有効期限メカニズムが組み込まれています。
  • 簡単にスケーリング/シャードします。
  • 永続性があります。

欠点は、少し複雑になることです。


1
Memcachedを複数のマシンに分割して、使用可能なRAMの量を増やすことができます。memcacheボックスがダウンしても物を失うことのないように、データをファイルシステムにシリアル化する2番目のサーバーを使用することができます。Memcache APIは非常に使いやすく、あらゆる言語で機能するため、さまざまな場所でさまざまなスタックを使用できます。
マイケルショップシン14

1

言及したクエリのすべてのパッケージを保存する必要はありません。例-パッケージタイプカウンター:

次の2つの配列が必要です。

int[] packageCounters = new int[NumberOfTotalTypes];
int[,] counterDifferencePerMinute = new int[6, NumberOfTotalTypes];

最初の配列は、異なるタイプのパッケージの数を追跡します。2番目の配列は、毎分追加されるパッケージの数を追跡し、毎分間隔で削除する必要があるパッケージの数を把握します。2番目の配列がラウンドFIFOキューとして使用されていることをお伝えできれば幸いです。

そのため、各パッケージに対して、次の操作が実行されます。

packageCounters[packageType] += 1;
counterDifferencePerMinute[current, packageType] += 1;
if (oneMinutePassed) {
  current = (current + 1) % 6;
  for (int i = 0; i < NumberOfTotalTypes; i++) {
    packageCounters[i] -= counterDifferencePerMinute[current, i];
    counterDifferencePerMinute[current, i] = 0;
}

いつでも、パッケージカウンターをインデックスによって即座に取得できますが、すべてのパッケージを保存するわけではありません。


私が行うデータを保存しなければならない主な理由は、これらの300の接続がまったく同じパケットを受信する可能性があるという事実です。したがって、パケットを複数回処理/カウントしないように、見られたすべてのパケットを少なくとも5分間保持する必要があります。これは、辞書キーのulongの目的です。
ジョシュ

1

(これは古い質問であることは知っていますが、第2世代のガベージコレクションパスがアプリを数秒間一時停止していたので、同様の状況にある他の人のために録音するという同様の問題の解決策を探している間に遭遇しました)。

データにはクラスではなく構造体を使用します(ただし、コピーによるパスのセマンティクスでは値として扱われます)。これにより、gcが各マークパスを実行する必要がある1レベルの検索がなくなります。

配列(保存するデータのサイズがわかっている場合)またはリスト-配列を内部的に使用します。高速なランダムアクセスが本当に必要な場合は、配列インデックスの辞書を使用してください。これにより、gcが検索する必要がある別のレベル(またはSortedDictionaryを使用している場合は1ダース以上)が削除されます。

実行している内容によっては、構造体のリストの検索は、特定のアプリケーションのプロファイルである辞書検索(メモリのローカライズのため)よりも高速になる場合があります。

struct&listの組み合わせにより、メモリ使用量とガベージコレクタースイープのサイズの両方が大幅に削減されます。


私はSQLiteの使用、などの高速なディスク内のコレクション&辞書を生成し、最近の実験で、持っているgithub.com/modma/PersistenceCollectionsを
ModMa
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.