MongoDb性能慢,即使使用索引也是如此

3
我们正在尝试使用Mongo为我们的用户构建通知应用程序。我们创建了一个10GB RAM、150GB SAS HDD 15K RPM、4 Core 2.9GHZ xeon intel XEN VM上的mongodb。
数据库架构:
{
  "_id" : ObjectId("5178c458e4b0e2f3cee77d47"),
  "userId" : NumberLong(1574631),
  "type" : 2,
  "text" : "a user connected to B",
  "status" : 0,
  "createdDate" : ISODate("2013-04-25T05:51:19.995Z"),
  "modifiedDate" : ISODate("2013-04-25T05:51:19.995Z"),
  "metadata" : "{\"INVITEE_NAME\":\"2344\",\"INVITEE\":1232143,\"INVITE_SENDER\":1574476,\"INVITE_SENDER_NAME\":\"123213\"}",
  "opType" : 1,
  "actorId" : NumberLong(1574630),
  "actorName" : "2344"
}

DB stats :-
db.stats()
{
    "db" : "UserNotificationDev2",
    "collections" : 3,
    "objects" : 78597973,
    "avgObjSize" : 489.00035699393925,
    "dataSize" : 38434436856,
    "storageSize" : 41501835008,
    "numExtents" : 42,
    "indexes" : 2,
    "indexSize" : 4272393328,
    "fileSize" : 49301946368,
    "nsSizeMB" : 16,
    "dataFileVersion" : {
        "major" : 4,
        "minor" : 5
    },
    "ok" : 1
}

索引:用户ID和_ID

我们试图为一个用户选择最新的21个通知。

db.userNotification.find({ "userId" : 53 }).limit(21).sort({ "_id" : -1 });

但是这个查询花费的时间太长了。Fri Apr 26 05:39:55.563 [conn156] 查询 UserNotificationDev2.userNotification: { query: { userId: 53 }, orderby: { _id: -1 } } cursorid:225321382318166794 ntoreturn:21 ntoskip:0 nscanned:266025 keyUpdates:0 numYields: 2 locks(micros) r:4224498 nreturned:21 reslen:10295 耗时2581毫秒。

即使是计数也需要很长一段时间。

Fri Apr 26 05:47:46.005 [conn159] command UserNotificationDev2.$cmd command: { count: "userNotification", query: { userId: 53 } } ntoreturn:1 keyUpdates:0 numYields: 11 locks(micros) r:9753890 reslen:48 5022ms

我们的查询有什么问题吗?

请帮忙!!!

如果我们的模式不适合存储用户通知,请提供建议。我们尝试过将用户和通知组成嵌入式文档,但是文档大小限制了我们只能存储约50,000个通知。因此我们进行了更改。


2
你能在你的查找操作上运行 explain() 吗,特别是 db.userNotification.find({ "userId" : 53 }) - cirrus
2
你也能运行 getIndexes() 吗? - cirrus
4个回答

3
您正在通过userId查询,但没有在任何地方建立索引。我的建议是在{"userId":1,"_id":-1}上创建一个索引。这将创建一个以userId开始的索引树,然后是_id,这几乎正是您的查询所做的。这是加速查询的最简单/最灵活的方法。
另一种更节省内存的方法是将userId和时间戳作为字符串存储在_id中,例如_id:“USER_ID:DATETIME”
{_id : "12345:20120501123000"}
{_id : "15897:20120501124000"}
{_id : "15897:20120501125000"}

注意 _id 是一个字符串,而不是 MongoId。那么你上面的查询就变成了一个正则表达式:

db.userNotification.find({ "_id" : /^53:/ }).limit(21).sort({ "_id" : -1 });

预期结果是按降序返回userId 53的所有通知。内存高效的部分有两个方面:
  1. 你只需要一个索引字段。(索引与数据竞争内存,通常几个G大小)
  2. 如果您的查询经常涉及获取更新的数据,右平衡索引会在索引太大无法完全适合时将您最常使用的工作保留在内存中。
关于计数。计数确实需要时间,因为它扫描整个集合。
关于您的模式。我猜对于您的数据集,这是利用您的内存的最佳方法。当对象变得庞大并且您的查询跨多个对象扫描时,它们将需要完全加载到内存中(当我在2GB RAM机器上排序2000个2MB对象时,OOM killer杀死了我的mongod实例)。对于大型对象,您的RAM使用量将大幅波动(更不用说它们受到某种程度的限制)。使用当前的模式,Mongo将更容易地仅加载您正在查询的数据,从而减少交换并获得更一致的内存使用模式。

2
这不是完全正确的。count将使用索引。而在复合索引中存储一个大小与两个字段相同的单个字段并不能真正节省太多空间,同时你还失去了使用该索引进行排序的能力。 - Asya Kamsky
@AsyaKamsky - 感谢您提供的信息。据我所知,在<Mongodb 2.2中,普通计数(db.collection.count())不使用索引(速度不太快),除非在2.4中改进了此行为。带有子查询的计数就像查找一样。关于{ "userId":1,"_id":-1 }。 我应该澄清答案的一部分是为了以最明显的方式解决Op的查询问题。 - Adil
1
他们甚至在2.4之前就开始使用索引了,但并不高效。想象一下,在特定范围内遍历和读取索引B树中的条目与仅计算它们数量之间的性能差异。 - Asya Kamsky

0

我刚刚尝试复制了你的问题。在userNotifications中创建了140,000,000个插入操作。 如果userId没有索引,响应时间为3-4秒。在我创建了userId的索引之后,响应时间几乎瞬间下降。

db.userNotifications.getIndexes()

[ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.userNotifications", "name" : "id" }, { "v" : 1, "key" : { "userId" : 1 }, "ns" : "test.userNotifications", "name" : "userId_1" } ]

另外一件事是:当您的选择发生时,系统是否不断地写入Mongo用户通知集合?如果是这种情况,Mongo将锁定整个集合。如果是这种情况,我会在主从(请参见复制)之间分离读取和写入,并进行一些分片。顺便问一下,您的应用程序使用哪种语言?


2
你不能在主节点和从节点之间“分割”读取和写入,因为从节点必须完成与主节点相同数量的写入操作。 - Asya Kamsky

0

一种选择是尝试分片,这样您就可以在分片之间均匀分配通知,因此当您需要进行选择时,将扫描更小的数据子集。但是需要决定使用什么来进行分片。对我来说,看起来像是operationType或userName,但我不太了解您的数据。另外一个问题是为什么要按_id排序?


因为我需要用户最近插入的通知。 - user2323026
1
分片并不是最佳索引缺失时的解决方案。 - Asya Kamsky

0

最重要的是,目前似乎没有索引来支持查询用户最新通知。

您需要在userId、_id上创建一个复合索引。这将支持仅按userId查询的查询,但它们也被用于按_id排序/限制的userId查询。

当您添加{userId:1, _id:-1}索引时,请不要忘记删除仅针对userId的索引,因为它将变得多余。

至于count(),请确保您使用的是2.4.3(最新版本),因为count()如何使用索引有了显着的改进,从而实现了更好的性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接