MongoDB Java API读取性能缓慢

时间:2018-08-30 16:09:22

标签: java mongodb performance mongodb-java mongo-java

我们正在从本地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秒(每秒25万行/秒)的速度读取整个集合,对于小文档而言,这确实一点都不令人印象深刻。请记住,我们要加载800mio行。无法进行汇总,缩小地图或类似操作。

这是否与MongoDB一样快,还是有其他方法可以更快地加载文档(其他技术,移动Linux,更多RAM,设置...)?

5 个答案:

答案 0 :(得分:12)

您没有指定用例,因此很难告诉您如何调整查询。 (即:谁想一次加载8亿行只是为了计数?)。

鉴于您的模式,我认为您的数据几乎是只读的,并且您的任务与数据聚合有关。

您当前的工作只是读取数据(很有可能您的驱动程序将批量读取数据),然后停止,然后执行一些计算(是的,使用int包装器来增加处理时间),然后重复。那不是一个好方法。如果您未以正确的方式访问数据库,DB的运行速度将非常快。

如果计算不太复杂,建议您使用aggregation framework而不是全部加载到RAM中。

您应该考虑的一些事情可以改善您的汇总:

  1. 将数据集划分为较小的集合。 (例如:按date进行分区,按exchange进行分区...)。添加索引以支持该分区并在分区上进行聚合,然后合并结果(典型的“分n征服”方法)
  2. 仅投影需要的字段
  3. 过滤掉不必要的文档(如果可能)
  4. 如果无法在内存上执行聚合(如果每个pipiline达到100MB的限制),则允许使用磁盘。
  5. 使用内置管道来加快计算速度(例如:$count例如)

如果计算太复杂而无法使用聚合框架表示,则使用mapReduce。它在mongod进程上运行,不需要将数据通过网络传输到您的内存中。

已更新

看起来您想进行OLAP处理,并且陷入了ETL步骤。

您不必而且必须避免每次都将整个OLTP数据加载到OLAP。只需要将新的更改加载到您的数据仓库。然后,第一次数据加载/转储花费更多时间是正常且可以接受的。

对于首次加载,您应考虑以下几点:

  1. Divide-N-Conquer再次将您的数据分解为较小的数据集(具有谓词,如日期/交易所/股票标签...)
  2. 进行并行计算,然后合并结果(您必须正确划分数据集)
  3. 批量处理而不是在forEach中进行处理:加载数据分区然后进行计算,而不是一个接一个地计算。

答案 1 :(得分:3)

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

由于您要遍历内存中的超过250k记录,因此这一行可能要花费很多时间。

要快速检查情况是否如此,可以尝试以下方法-

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

这将帮助您了解从数据库获取文档实际花费的时间以及迭代花费的时间。

答案 2 :(得分:2)

我认为在您的情况下我应该做的是一个简单的解决方案,同时一种有效的方法是通过使用parallelCollectionScan使总体吞吐量最大化

  

允许应用程序在读取所有内容时使用多个并行游标   来自集合的文档,从而提高了吞吐量。的   parallelCollectionScan命令返回包含以下内容的文档:   游标信息数组。

     

每个游标都提供对部分返回值的返回的访问   集合中的文档。迭代每个游标会返回   集合中的文档。游标不包含   数据库命令。数据库命令的结果标识了   游标,但不包含或不构成游标。

一个带有 parallelCollectionScan的简单示例应该是这样的

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

答案 3 :(得分:1)

首先,正如@ xtreme-biker所说,性能很大程度上取决于您的硬件。具体来说,我的第一条建议是检查您是在虚拟机上还是在本机主机上运行。对于在带有SDD驱动器的i7上的CentOS VM,我每秒可以读取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()的速度相当快(不到10毫秒),大部分时间都花在while循环内,尤其是在私有方法QueryBatchCursor.getMore()

getMore()调用DefaultServerConnection.command(),基本上是通过两个操作来消耗时间: 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来获取下一批。

在我看来,驱动程序实现的“问题”是注入了String to JSON解码器,但没有注入JsonReader(decode()方法所依赖的)。这样一来,即使到com.mongodb.internal.connection.InternalStreamConnection,您已经在套接字通讯附近。

因此,我认为您无法对MongoCollection.find()进行任何改进,除非您深入到InternalStreamConnection.sendAndReceiveAsync()

您不能减少往返次数,也不能更改将响应转换为BsonDocument的方式。并非绕过驱动程序并编写自己的客户端,我怀疑这是个好主意。

P.D。。如果您想尝试上面的某些代码,则需要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;
}

答案 4 :(得分:0)

以我的数量,您正在处理大约50 MiB / s(250k行/秒* 0.2 KiB /行)。这已经涉及到磁盘驱动器和网络瓶颈领域。 MongoDB使用哪种存储?客户端和MongoDB服务器之间有什么样的带宽?您是否尝试过以最小的延迟(<1.0 ms)在高速(> = 10 Gib / s)网络上共置服务器和客户端?请记住,如果您使用的是诸如AWS或GCP之类的云计算提供商,则它们将具有物理瓶颈之上的虚拟化瓶颈。

您询问了可能有用的设置。您可以尝试更改connectioncollection上的压缩设置(选项为“ none”,snappyzlib)。即使在snappy上没有任何改善,看到设置造成(或不产生)的差异也可能有助于找出系统中哪个部分承受最大压力。

与C ++或Python相比,Java在数字运算方面不具有良好的性能,因此您可以考虑使用其中一种语言重写此特定操作,然后将其与Java代码集成。我建议您进行一次测试运行,即仅循环使用Python中的数据并将其与Java中的数据进行比较。