web-dev-qa-db-ja.com

1秒あたりの挿入数が多い、クエリ用に数千万のオブジェクトを保存する効率的な方法

これは基本的に、p2pチャットネットワーク上のパケット数やパケットタイプなどをカウントするロギング/カウントアプリケーションです。これは、5分間で約400〜600万パケットに相当します。また、この情報の「スナップショット」しか撮らないので、5分より古いパケットのみを5分ごとに削除しています。したがって、このコレクションに含まれるアイテムの最大数は1000万から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は、現時点ではRAMが2GBに制限されています。

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

15
Josh

辞書を1つ用意し、古すぎるエントリをその辞書で検索するのではなく、 10個の辞書があります。 30秒ごとに新しい「現在の」辞書を作成し、何も検索せずに最も古い辞書を破棄します。

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

12
Brendan

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

第2に、LINQは簡潔なコードに最適ですが、実際にはLINQは「通常の」C#の構文シュガーであり、最適なコードが生成される保証はありません。演習として、LINQを使用せずにホットスポットを書き直してみると、パフォーマンスは向上しませんが、何をしているのかが明確になり、プロファイリング作業が簡単になります。

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

3
Steve

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

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

欠点は、それがメモリベースであり、永続性がないことです。インスタンスが停止すると、データは失われます。永続化が必要な場合は、自分でデータをシリアル化してください。

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

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

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

3
9000

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

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

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

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

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

1
Malcolm

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

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;
}

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

1
Codism