Upsert 性能随着 collection(文档数量)的增加而降低
Upsert performance decreases with a growing collection (number of documents)
用例:
我正在使用 REST Api,它提供视频游戏的战斗结果。这是一款团队对战在线游戏,每个团队由 3 名玩家组成,他们可以从 100 个不同的角色中选择一个。我想计算每个团队组合的胜/负和平局数。我每秒大约获得 1000 个战斗结果。我连接每个团队的角色 ID(升序),然后保存 wins/losses 并为每个组合绘制。
我当前的实现:
const combinationStatsSchema: Schema = new Schema({
combination: { type: String, required: true, index: true },
gameType: { type: String, required: true, index: true },
wins: { type: Number, default: 0 },
draws: { type: Number, default: 0 },
losses: { type: Number, default: 0 },
totalGames: { type: Number, default: 0, index: true },
battleDate: { type: Date, index: true, required: true }
});
对于每个返回的日志,我执行更新插入并将这些查询批量(5-30 行)发送到 MongoDB:
const filter: any = { combination: log.teamDeck, gameType, battleDate };
if (battleType === BattleType.PvP) {
filter.arenaId = log.arena.id;
}
const update: {} = { $inc: { draws, losses, wins, totalGames: 1 } };
combiStatsBulk.find(filter).upsert().updateOne(update);
我的问题:
只要我的 collection combinationStats
mongodb 中只有几千个条目,只需要 0-2% cpu。一旦 collection 有几百万个文档(由于可能的组合数量,这种情况发生得非常快)MongoDB 经常占用 50-100% cpu。显然我的方法根本不可扩展。
我的问题:
这些选项中的任何一个都可以解决我上面定义的问题:
- 我能否优化上述 MongoDB 解决方案的性能,使其不会占用那么多 CPU? (我已经为过滤的字段编制了索引,并且批量执行更新插入)。创建一个散列(基于我所有的过滤字段)是否有助于过滤数据然后提高性能?
- 是否有适合聚合此类数据的更好的数据库/技术?我可以想象更多的用例,其中我 want/need 为给定标识符递增计数器。
编辑: 在 khang 评论说这可能与 upsert 性能有关后,我用 $set
替换了我的 $inc
,实际上性能是一样的"poor"。因此,我尝试了建议的 find()
然后手动 update()
方法,但结果并没有变得更好。
在 (1.) 中,您断言您批量执行更新插入。但是根据这似乎是如何扩展的,您可能向每个批次发送的行太少。每次存储行加倍时,请考虑将批处理大小加倍。请为您的设置执行 post mongo 的 explain() 查询计划。
在 (2.) 中,您考虑切换到 mysql 或 postgres。是的,那绝对是一个有效的实验。同样,请务必 post EXPLAIN 输出与您的计时数据。
只有一百万种可能的团队组合,并且有一种分布,其中一些比其他的更受欢迎。你只需要维护一百万个计数器,这不是一个很大的数字。但是,执行 1e6 磁盘 I/O 可能需要一段时间,特别是如果它们是随机读取。考虑远离磁盘驻留数据结构,您可能会对其进行频繁的 COMMIT,并切换到内存驻留哈希或 b 树。听起来 ACID 类型的持久性保证对您的应用程序并不重要。
此外,一旦您组装了 "large" 个输入批次,肯定超过一千个,也许在一百万个数量级,请在处理之前注意对批次进行排序。那么你的柜台维护问题看起来就像合并排序,要么在内部存储器上,要么在外部存储器上。
扩展批次的一种原则性方法是在一些方便大小的排序内存缓冲区中累积观察值,并且仅当缓冲区中不同团队组合的数量高于某个阈值 K 时才从该管道阶段释放聚合(计数)观察值. Mongo 或管道中下一阶段的任何内容。如果 K 远大于 1e6 的 1%,那么即使对存储在磁盘上的计数器进行顺序扫描,也很有可能在读取的每个磁盘块上找到有用的更新工作。
根据您的过滤条件创建散列:
我能够将 CPU 从 80-90% 降低到 1-5% 并体验到更高的吞吐量。
显然是过滤器的问题。我没有根据这三个条件进行过滤: { combination: log.teamDeck, gameType, battleDate }
我在我的节点应用程序中创建了一个 128 位哈希。我使用此哈希进行更新并将组合、gameType 和 battleDate 设置为更新文档中的附加字段。
为了创建哈希,我使用了 metrohash 库,可在此处找到:https://github.com/jandrewrogers/MetroHash。不幸的是,我无法解释为什么性能要好得多,特别是因为我索引了我以前的所有条件。
用例:
我正在使用 REST Api,它提供视频游戏的战斗结果。这是一款团队对战在线游戏,每个团队由 3 名玩家组成,他们可以从 100 个不同的角色中选择一个。我想计算每个团队组合的胜/负和平局数。我每秒大约获得 1000 个战斗结果。我连接每个团队的角色 ID(升序),然后保存 wins/losses 并为每个组合绘制。
我当前的实现:
const combinationStatsSchema: Schema = new Schema({
combination: { type: String, required: true, index: true },
gameType: { type: String, required: true, index: true },
wins: { type: Number, default: 0 },
draws: { type: Number, default: 0 },
losses: { type: Number, default: 0 },
totalGames: { type: Number, default: 0, index: true },
battleDate: { type: Date, index: true, required: true }
});
对于每个返回的日志,我执行更新插入并将这些查询批量(5-30 行)发送到 MongoDB:
const filter: any = { combination: log.teamDeck, gameType, battleDate };
if (battleType === BattleType.PvP) {
filter.arenaId = log.arena.id;
}
const update: {} = { $inc: { draws, losses, wins, totalGames: 1 } };
combiStatsBulk.find(filter).upsert().updateOne(update);
我的问题:
只要我的 collection combinationStats
mongodb 中只有几千个条目,只需要 0-2% cpu。一旦 collection 有几百万个文档(由于可能的组合数量,这种情况发生得非常快)MongoDB 经常占用 50-100% cpu。显然我的方法根本不可扩展。
我的问题:
这些选项中的任何一个都可以解决我上面定义的问题:
- 我能否优化上述 MongoDB 解决方案的性能,使其不会占用那么多 CPU? (我已经为过滤的字段编制了索引,并且批量执行更新插入)。创建一个散列(基于我所有的过滤字段)是否有助于过滤数据然后提高性能?
- 是否有适合聚合此类数据的更好的数据库/技术?我可以想象更多的用例,其中我 want/need 为给定标识符递增计数器。
编辑: 在 khang 评论说这可能与 upsert 性能有关后,我用 $set
替换了我的 $inc
,实际上性能是一样的"poor"。因此,我尝试了建议的 find()
然后手动 update()
方法,但结果并没有变得更好。
在 (1.) 中,您断言您批量执行更新插入。但是根据这似乎是如何扩展的,您可能向每个批次发送的行太少。每次存储行加倍时,请考虑将批处理大小加倍。请为您的设置执行 post mongo 的 explain() 查询计划。
在 (2.) 中,您考虑切换到 mysql 或 postgres。是的,那绝对是一个有效的实验。同样,请务必 post EXPLAIN 输出与您的计时数据。
只有一百万种可能的团队组合,并且有一种分布,其中一些比其他的更受欢迎。你只需要维护一百万个计数器,这不是一个很大的数字。但是,执行 1e6 磁盘 I/O 可能需要一段时间,特别是如果它们是随机读取。考虑远离磁盘驻留数据结构,您可能会对其进行频繁的 COMMIT,并切换到内存驻留哈希或 b 树。听起来 ACID 类型的持久性保证对您的应用程序并不重要。
此外,一旦您组装了 "large" 个输入批次,肯定超过一千个,也许在一百万个数量级,请在处理之前注意对批次进行排序。那么你的柜台维护问题看起来就像合并排序,要么在内部存储器上,要么在外部存储器上。
扩展批次的一种原则性方法是在一些方便大小的排序内存缓冲区中累积观察值,并且仅当缓冲区中不同团队组合的数量高于某个阈值 K 时才从该管道阶段释放聚合(计数)观察值. Mongo 或管道中下一阶段的任何内容。如果 K 远大于 1e6 的 1%,那么即使对存储在磁盘上的计数器进行顺序扫描,也很有可能在读取的每个磁盘块上找到有用的更新工作。
根据您的过滤条件创建散列:
我能够将 CPU 从 80-90% 降低到 1-5% 并体验到更高的吞吐量。
显然是过滤器的问题。我没有根据这三个条件进行过滤: { combination: log.teamDeck, gameType, battleDate }
我在我的节点应用程序中创建了一个 128 位哈希。我使用此哈希进行更新并将组合、gameType 和 battleDate 设置为更新文档中的附加字段。
为了创建哈希,我使用了 metrohash 库,可在此处找到:https://github.com/jandrewrogers/MetroHash。不幸的是,我无法解释为什么性能要好得多,特别是因为我索引了我以前的所有条件。