CLRプロセスに3億個のオブジェクトを保存します

つまずき-GC



JavaやC#などのすべてのマネージ言語には、1つの重大な欠点があります-無条件の自動メモリ管理。 これがマネージ言語の利点であると思われます。 貴重な1時間あたり10 KBが流れる場所を理解せずに、お気に入りのサーバーを1日に1回再起動することをせずに、ダンドリングポインターでどのようにガタガタしたのか覚えていますか? もちろん、JavaとC#(およびそれらに似た他の製品)は、一見すると99%のケースで状況を解決します。



それだけです。問題は1つだけです。大量のオブジェクトをどのように扱うかです。同じ.Netには魔法がないからです。 CLRは、膨大な数のオブジェクトとその相互リンクをスキャンする必要があります。 この問題は、世代を導入することで部分的に解決されます。 ほとんどのオブジェクトは長生きしないという事実に基づいて、それらをより早く解放するため、毎回すべてのヒープオブジェクトを調べる必要はありません。



しかし、問題はオブジェクトが長生きしなければならない場合にはまだ存在します。 たとえば、キャッシュ。 何百万ものオブジェクトが含まれている必要があります。 特に、典型的な最新のサーバーでのRAMの増加を考慮してください。 64GBのメモリを搭載したマシンのキャッシュに、何億ものビジネスオブジェクト(たとえば、1ダースのフィールドを持つPerson)を潜在的に格納できることがわかりました。



ただし、実際にはこれは失敗します。 最初の1,000万個のオブジェクトを追加し、それらが第1世代から第2世代に「陳腐化」するとすぐに、次のフルGCスキャンはプロセスを8〜12秒間「ハング」させ、この一時停止は避けられません。 すでにバックグラウンドサーバーGCモードになっており、これは「世界を停止する」時間に過ぎません。 これは、サーバーaplikuが単に10秒間「死ぬ」という事実につながります。 さらに、「臨床的死」の瞬間を予測することはほとんど不可能です。

どうする? 多くのオブジェクトを長期間保存しないでください?



なぜ



しかし、特定のタスクで多くのオブジェクトを長期間保存する必要があります。 たとえば、2億通りのネットワークとその関係を保存しています。 フラットファイルから読み込んだ後、アプリケーションでオッズを計算する必要があります。 時間がかかります。 したがって、データがディスクからメモリにダウンロードされるとすぐにこれを行います。 その後、事前に計算され、「仕事と防衛のために」準備ができているオブジェクトグラフが必要です。 つまり、1秒間に数百のリクエストに応答しながら、数週間にわたって約48GBのデータを常駐して保存する必要があります。



別のタスクがあります。 2〜3週間で数億を蓄積するソーシャルデータのキャッシュ。1秒あたり数万の読み取り要求を処理する必要があります。



どうやって



そこで、メモリマネージャを作成し、「Pile」(束)と名付けました。 「クリップル」管理メモリモデルを回避する方法はありません。 管理されていないメモリは何も保存しません。 アクセスするには、速度を「殺し」、設計全体を複雑にするチェックに時間がかかります。 .NetもJavaも、ヒープではないメモリのチャンクでは「通常」モードで動作できません。



私たちは何をしましたか? メモリマネージャーは、完全に100%マネージコードです。 バイト配列を動的に割り当てます。これをセグメントと呼びます。 セグメントの内部には、ポインター-通常のintがあります。 そして、PilePointerを取得します。

/// <summary> /// Represents a pointer to the pile object (object stored in a pile). /// The reference may be local or distributed in which case the NodeID is>=0. /// Distributed pointers are very useful for organizing piles of objects distributed among many servers, for example /// for "Big Memory" implementations or large neural networks where nodes may inter-connect between servers. /// The CLR reference to the IPile is not a part of this struct for performance and practicality reasons, as /// it is highly unlikely that there are going to be more than one instance of a pile in a process, however /// should more than 1 pile be allocated than this pointer would need to be wrapped in some other structure along /// with source IPile reference /// </summary> public struct PilePointer : IEquatable<PilePointer> { /// <summary> /// Distributed Node ID. The local pile sets this to -1 rendering this pointer as !DistributedValid /// </summary> public readonly int NodeID; /// <summary> /// Segment # within pile /// </summary> public readonly int Segment; /// <summary> /// Address within the segment /// </summary> public readonly int Address; ………………………………………………………………… }
      
      





以下のNodeIDに注意してください。 次のようにPilePointerを取得します。



 var obj = new MyBusinessType(); var pilePointer = Pile.Put(obj); ………………………………………… // -   ,    var originalObj = Pile.Get(pilePointer);
      
      





Put()を使用してPileにロードした元のオブジェクトのコピー、またはポインターが正しくない場合はPileAccessViolationを取得します。



 Pile.Delete(pilePointer)
      
      





それぞれメモリを解放できます。このメモリを読み取ろうとすると、再びPileAccessViolationが呼び出されます。



質問:CLRオブジェクトを実際のポインターで保存できないため、GCを混乱させるため、これはどのように行われ、バイト[]に何を保存しますか。 管理された参照を削除して形式に何かを保存するには、反対のことが必要です。 したがって、データを保存することができ、GCはこれらがオブジェクトであることを認識せず、それらを訪問しません。 これは、シリアル化によって実行できます。 もちろん、これは組み込みの.Netシリアライザー(BinaryFormatterなど)ではなく、NFXのネイティブシリアライザーに関するものです。



PilePointer.NodeIDを使用すると、分散パイルコホート内のノードを識別するため、分散ヒープ間でデータを「分散」できます。



そして今、主な質問。 シリアル化が「内部」で使用される場合、なぜこれがすべて必要であり、遅いのですか?



スピード



実際には、次のように機能します。NFXSlimシリアル化を使用するバイト[]に没入した<300バイトのオブジェクトは、メモリ内のネイティブCLRオブジェクトよりも平均10〜25%少ないスペースを使用します。 大きなオブジェクトの場合、この差はゼロになる傾向があります。 これはなぜですか? 実際、NFX.Serialization.Slim.SlimSerializerは文字列にUTF8を使用し、可変長整数エンコーディングは12バイト以上のCLRヘッダーを必要としません。 その結果、シリアル化の速度が障害になります。 SlimSerializerは驚異的な速度を「保持」します。 周波数が3GHzのIntel I7 Sandy Bridgeの1つのコアで、1秒あたり44万PilePointer'ovをオブジェクトに変換します。 このテストの各オブジェクトには20個のフィールドがあり、208バイトのメモリを消費します。 1秒あたり405千の単一コアを持つオブジェクトをPileに挿入します。 この速度は、シリアライズ可能な各オブジェクトの式ツリーをパイルセグメントに動的にコンパイルすることによって実現されます。 SlimSerializerは平均してBinaryFormatterの5倍の速度で実行されますが、多くの単純なタイプではこの比率は10に達しますが、スペースの観点から、SlimSerializerはBinaryFormatterの1/4から1/10でデータをパックします。 まあ、最も重要なこと。 SlimSerializerは、操作するオブジェクトのフィールドの特別なマークアップを必要としません。 つまり デリゲート以外は何でも保存できます。



マルチスレッドデータ挿入テストは、CoreI7 3GHzで毎秒100万件を超えるトランザクションを安定して保持します。

さて、今最も重要なこと。 プロセスで300,000,000個のオブジェクトを割り当てたため、フルGCにかかる時間は30ミリ秒未満







まとめ



NFX.ApplicationModel.Pileテクノロジーは、数億個のオブジェクトを長時間(数週間)メモリに常駐させることにより、GCガベージコレクターによる予測不可能な遅延を回避し、「アウトプロセス」ソリューション(MemCacheなど)よりも高速なアクセス速度を提供します。 Redis et.al)。



Pileは、大きなバイト[]のロックを解除し、アプリケーションにメモリを割り当てる専用のメモリマネージャに基づいています。 Pileに浸漬されたオブジェクトは、PilePointer構造体ctrによって識別されます。 オブジェクトを相互に参照する効果的なオブジェクトグラフの作成に寄与する12バイトを占有します。



コードを取得します。



NFX GitHub



All Articles