web-dev-qa-db-ja.com

MongoDB Java APIの読み取り速度が遅い

地元のMongoDBからコレクションのすべてのドキュメントを読んでいますが、パフォーマンスはそれほど素晴らしいものではありません。

すべてのデータをダンプする必要があります。理由を気にせず、本当に必要であると信じてください。回避策はありません。

次のような4mioドキュメントがあります。

{
    "_id":"4d094f58c96767d7a0099d49",
    "exchange":"NASDAQ",
    "stock_symbol":"AACC",
    "date":"2008-03-07",
    "open":8.4,
    "high":8.75,
    "low":8.08,
    "close":8.55,
    "volume":275800,
    "adj close":8.55
}

そして、私たちは今、これを使って簡単なコードを読んでいます:

MongoClient mongoClient = MongoClients.create();
MongoDatabase database = mongoClient.getDatabase("localhost");
MongoCollection<Document> collection = database.getCollection("test");

MutableInt count = new MutableInt();
long start = System.currentTimeMillis();
collection.find().forEach((Block<Document>) document -> count.increment() /* actually something more complicated */ );
long start = System.currentTimeMillis();

コレクション全体を16秒(250k行/秒)で読んでいますが、小さなドキュメントではまったく印象的ではありません。 800mio行をロードすることに注意してください。集約、マップの縮小などはできません。

これはMongoDBが取得するのと同じくらい高速ですか、ドキュメントをより高速にロードする他の方法(他の手法、Linuxの移動、RAMの追加、設定など)がありますか?

23
ic3

ユースケースを指定しなかったため、クエリの調整方法を説明するのは非常に困難です。 (つまり、カウントのために一度に800mil行をロードしたい人は誰ですか?).

スキーマを考えると、データはほとんど読み取り専用であり、タスクはデータ集約に関連していると思います。

現在の作業は、データを読み取るだけで(ドライバーがバッチで読み取る可能性が高い)、停止してから計算を実行し(そう、intラッパーを使用して処理時間をさらに増やします)、繰り返します。それは良いアプローチではありません。正しい方法でアクセスしないと、DBは魔法のように高速になりません。

計算が複雑すぎない場合は、すべてをRAMにロードする代わりに aggregation framework を使用することをお勧めします。

集計を改善するために考慮する必要があるもの:

  1. データセットを小さなセットに分割します。 (例:dateによるパーティション、exchange...によるパーティション)。そのパーティションをサポートするインデックスを追加し、パーティションで集計を実行し、結果を結合します(典型的な分割-統治アプローチ)
  2. プロジェクトに必要なフィールドのみ
  3. 不要なドキュメントを除外する(可能な場合)
  4. メモリで集計を実行できない場合(ピピラインあたり100MBの制限に達した場合)、ディスク使用を許可します。
  5. 組み込みのパイプラインを使用して、計算を高速化します(例:$countあなたの例の場合)

計算が複雑すぎて集約フレームワークで表現できない場合は、 mapReduce を使用します。 mongodプロセスで動作し、データをネットワーク経由でメモリに転送する必要はありません。

更新済み

したがって、OLAP処理を実行したいように見えますが、ETLステップで停止します。

OLTPデータを毎回OLAPにロードする必要があります。データウェアハウスに新しい変更をロードするだけです。データのロード/ダンプに時間がかかるのは正常で許容範囲です。

初めてロードする場合は、次の点を考慮する必要があります。

  1. Divide-N-Conquerは、データを小さなデータセットに分割します(日付/交換/在庫ラベルなどの述語を使用して...)
  2. 並列計算を行い、結果を結合します(データセットを適切に分割する必要があります)
  3. forEachで処理する代わりにバッチで計算を実行します。データパーティションを読み込んでから、1つずつ計算する代わりに計算します。

collection.find().forEach((Block<Document>) document -> count.increment());

メモリ内で25万件を超えるレコードを繰り返し処理しているため、この行は多くの時間を費やしている可能性があります。

それがケースであるかどうかをすばやく確認するには、これを試すことができます-

long start1 = System.currentTimeMillis();
List<Document> documents = collection.find();
System.out.println(System.currentTimeMillis() - start1);

long start2 = System.currentTimeMillis();
documents.forEach((Block<Document>) document -> count.increment());
System.out.println(System.currentTimeMillis() - start2);

これは、データベースからドキュメントを取得するのに実際にかかる時間と、反復にかかる時間を理解するのに役立ちます。

3

あなたのケースで私がすべきと思うことは簡単なソリューションであり、同時に効率的な方法はparallelCollectionScanを使用して全体的なスループットを最大化することです

コレクションからすべてのドキュメントを読み取るときに、アプリケーションが複数の並列カーソルを使用できるようにして、スループットを向上させます。 parallelCollectionScanコマンドは、カーソル情報の配列を含むドキュメントを返します。

各カーソルは、コレクションからドキュメントの部分セットを返すためのアクセスを提供します。各カーソルを反復すると、コレクション内のすべてのドキュメントが返されます。カーソルには、データベースコマンドの結果は含まれません。データベースコマンドの結果はカーソルを識別しますが、カーソルを含んでいないか、構成していません。

parallelCollectionScan を使用した簡単な例は、次のように考える必要があります。

 MongoClient mongoClient = MongoClients.create();
 MongoDatabase database = mongoClient.getDatabase("localhost");
 Document commandResult = database.runCommand(new Document("parallelCollectionScan", "collectionName").append("numCursors", 3));
2

まず、@ xtreme-bikerがコメントしたように、パフォーマンスはハードウェアに大きく依存します。具体的には、最初のアドバイスは、仮想マシンで実行しているかネイティブホストで実行しているかを確認することです。私の場合、CentOS VM SDDドライブを搭載したi7では1秒あたり123,000文書を読むことができますが、同じドライブのWindowsホストで実行されているまったく同じコードは最大387,000文書を読み取ります秒。

次に、あなたが本当に全コレクションを読む必要があると仮定しましょう。これは、フルスキャンを実行する必要があるということです。そして、MongoDBサーバーの構成を変更することはできず、コードを最適化するだけであると仮定しましょう。

その後、すべてが何になります

_collection.find().forEach((Block<Document>) document -> count.increment());
_

実際に。

MongoCollection.find()をすばやく展開すると、実際にこれが行われていることがわかります。

_ReadPreference readPref = ReadPreference.primary();
ReadConcern concern = ReadConcern.DEFAULT;
MongoNamespace ns = new MongoNamespace(databaseName,collectionName);
Decoder<Document> codec = new DocumentCodec();
FindOperation<Document> fop = new FindOperation<Document>(ns,codec);
ReadWriteBinding readBinding = new ClusterBinding(getCluster(), readPref, concern);
QueryBatchCursor<Document> cursor = (QueryBatchCursor<Document>) fop.execute(readBinding);
AtomicInteger count = new AtomicInteger(0);
try (MongoBatchCursorAdapter<Document> cursorAdapter = new MongoBatchCursorAdapter<Document>(cursor)) {
    while (cursorAdapter.hasNext()) {
        Document doc = cursorAdapter.next();
        count.incrementAndGet();
    }
}
_

ここで、FindOperation.execute()はかなり高速(10ms未満)であり、ほとんどの時間はwhileループ内、特にプライベートメソッドQueryBatchCursor.getMore()内で費やされます。

getMore()DefaultServerConnection.command()を呼び出し、その時間は基本的に2つの操作で消費されます:1)文字列データのフェッチサーバーから2)文字列データをBsonDocumentに変換します。

Mongoは、大きな結果セットを取得するためにネットワークラウンドトリップを何回行うかに関して、非常に賢いことがわかります。 firstBatchコマンドで最初に100個の結果をフェッチし、次にコレクションサイズに応じたバッチサイズであるnextBatchを上限として、より大きなバッチをフェッチします。

そのため、最初のバッチを取得するには、このような状況が発生します。

_ReadPreference readPref = ReadPreference.primary();
ReadConcern concern = ReadConcern.DEFAULT;
MongoNamespace ns = new MongoNamespace(databaseName,collectionName);
FieldNameValidator noOpValidator = new NoOpFieldNameValidator();
DocumentCodec payloadDecoder = new DocumentCodec();
Constructor<CodecProvider> providerConstructor = (Constructor<CodecProvider>) Class.forName("com.mongodb.operation.CommandResultCodecProvider").getDeclaredConstructor(Decoder.class, List.class);
providerConstructor.setAccessible(true);
CodecProvider firstBatchProvider = providerConstructor.newInstance(payloadDecoder, Collections.singletonList("firstBatch"));
CodecProvider nextBatchProvider = providerConstructor.newInstance(payloadDecoder, Collections.singletonList("nextBatch"));
Codec<BsonDocument> firstBatchCodec = fromProviders(Collections.singletonList(firstBatchProvider)).get(BsonDocument.class);
Codec<BsonDocument> nextBatchCodec = fromProviders(Collections.singletonList(nextBatchProvider)).get(BsonDocument.class);
ReadWriteBinding readBinding = new ClusterBinding(getCluster(), readPref, concern);
BsonDocument find = new BsonDocument("find", new BsonString(collectionName));
Connection conn = readBinding.getReadConnectionSource().getConnection();

BsonDocument results = conn.command(databaseName,find,noOpValidator,readPref,firstBatchCodec,readBinding.getReadConnectionSource().getSessionContext(), true, null, null);
BsonDocument cursor = results.getDocument("cursor");
long cursorId = cursor.getInt64("id").longValue();

BsonArray firstBatch = cursor.getArray("firstBatch");
_

次に、cursorIdを使用して、次の各バッチをフェッチします。

私の意見では、ドライバーの実装に伴う「問題」は、JSONデコーダーへのストリングが注入されますが、decode()メソッドが依存するJsonReaderは注入されないことです。これは、すでにソケット通信の近くにいる_com.mongodb.internal.connection.InternalStreamConnection_までです。

したがって、MongoCollection.find()のように深くしない限り、InternalStreamConnection.sendAndReceiveAsync()を改善するためにできることはほとんどないと思います。

往復回数を減らすことはできません。また、応答がBsonDocumentに変換される方法を変更することはできません。ドライバーをバイパスせずに、独自のクライアントを作成することなく、疑わしいことはないでしょう。

PD上記のコードを試してみたい場合、getCluster()メソッドが必要です。これは、 mongo-Java-driver

_private Cluster getCluster() {
    Field cluster, delegate;
    Cluster mongoCluster = null;
    try {
        delegate = mongoClient.getClass().getDeclaredField("delegate");
        delegate.setAccessible(true);
        Object clientDelegate = delegate.get(mongoClient);
        cluster = clientDelegate.getClass().getDeclaredField("cluster");
        cluster.setAccessible(true);
        mongoCluster = (Cluster) cluster.get(clientDelegate);
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
        System.err.println(e.getClass().getName()+" "+e.getMessage());
    }
    return mongoCluster;
}
_
2
Serg M Ten

私の数では、約50 MiB/s(250k行/秒* 0.2 KiB /行)を処理しています。それは、ディスクドライブとネットワークのボトルネックの両方の領域に入り込んでいます。 MongoDBはどのようなストレージを使用していますか?クライアントとMongoDBサーバーの間にどのような帯域幅がありますか?最小(<1.0ミリ秒)の遅延で高速(> = 10 Gib/s)ネットワークにサーバーとクライアントを共存させようとしましたか? AWSやGCPなどのクラウドコンピューティングプロバイダーを使用している場合、物理的なものに加えて仮想化のボトルネックが発生することに注意してください。

役に立つかもしれない設定について尋ねました。 connection および collection で圧縮設定を変更してみてください(オプションは "none"、snappy、およびzlib) 。どちらもsnappyを改善しなかったとしても、設定によって生じる(または生じない)違いを見ると、システムのどの部分が最もストレスを受けているかを把握するのに役立つ場合があります。

Javaは、C++やPythonと比較して、数値演算のパフォーマンスが優れていないため、これらの言語のいずれかでこの特定の操作を書き直し、それをJavaコードと統合することをお勧めします。 Pythonでデータをループし、Javaで同じものと比較するだけのテスト実行。

0
Old Pro