0

我正在尝试对需要非常高的写入吞吐量和合理的读取吞吐量的数据库进行建模。我有一组分布式系统将“事件”数据添加到数据库中。

目前,事件记录的 id 是 Guid。我一直在读到 guid 不倾向于创建很好的索引,因为它们的随机分布意味着最近的数据将分散在磁盘中,这可能会导致分页问题。

所以这是我要验证的第一个假设:我假设我不想选择创建正确平衡树的 _id,例如自动编号。这将是有益的,因为最近的 2 个事件在磁盘上基本上彼此相邻。这是一个正确的假设吗?

假设 (1) 是正确的,那么我正在尝试找出生成此类 id 的最佳方法。我知道 Mongo 本身就支持 ObjectId,这对于可以将数据绑定到 Mongo 的应用程序很方便,但我的应用程序不是这样。由于有多个系统产生数据,模拟“自动编号”字段有点问题,因为 mongo 不支持服务器端的自动编号,所以生产者必须分配 id,如果他们不这样做很难不知道其他系统在做什么。

为了解决这个问题,我正在考虑使 _id 字段成为 { localId, producerId } 上的复合键,其中本地 id 是生产者可以生成的自动编号,因为 producerId 将使其唯一。ProducerId 是我可以在生产者之间协商的东西,以便他们可以提出唯一的 ID。

所以这是我的下一个问题:如果我的目标是从所有生产者那里获取最新数据,那么 { localId, producerId } 应该是首选的键排序,因为 localId 将是右派,而 producerId 将是一个小集群,我宁愿两个最近的事件彼此保持本地。如果我颠倒该顺序,那么我对树最终外观的推理将类似于以下内容:

               root
        /        |           \
       p0        p1          p2
       /         |            \
     e0..n      e0..n        e0..n

其中 p# 是生产者 ID,e# 是一个事件。这似乎会将我的索引分割成 p# 数据集群,并且新事件不一定彼此相邻。我对首选排序的假设应该(请验证)看起来像这样:

               root
      /          |          \
     e0          e1         e2
     /            |           \
  p0..n         p0..n        p0..n

这似乎使最近的事件彼此靠近。(我知道 Mongo 使用 B 树作为索引,但我只是想在这里简化视觉效果)。

我可以看到对 { localId, producerId } 的唯一警告是,用户的常见查询是按生产者列出最近的事件,{ producerId, localId } 实际上会处理得更好。为了让这个查询与 { localId, producerId } 一起工作,我想我还需要将 producerId 作为一个字段添加到文档中,并为其编制索引。

为了明确我的问题到底是什么,我想知道我是否正确地考虑了这个问题,或者是否有明显更好的方法来解决这个问题。

谢谢

4

1 回答 1

1

回答您的问题:如果您只是按 b 查询然后按 a 排序,像这样的复合:{a,b} 将以分散查询结尾。但它将使用索引进行排序。

如果使用 Document 而不是 ObjectId,_id 将被索引但不使用,但它不是复合索引!

例子:

鉴于 Collection 'a' 中的此 Documents 且没有附加索引:

{ "_id" : { "e" : 1, "p" : 1 } }
{ "_id" : { "e" : 1, "p" : 2 } }
{ "_id" : { "e" : 2, "p" : 1 } }
{ "_id" : { "e" : 1, "p" : 3 } }
{ "_id" : { "e" : 2, "p" : 3 } }
{ "_id" : { "e" : 2, "p" : 2 } }
{ "_id" : { "e" : 3, "p" : 1 } }
{ "_id" : { "e" : 3, "p" : 2 } }
{ "_id" : { "e" : 3, "p" : 3 } }

像这样的查询:

db.a.find({'_id.p' : 2}).sort({'_id.e' : 1}).explain()

不会使用索引:

{
    "cursor" : "BasicCursor",
    "nscanned" : 9,
    "nscannedObjects" : 9,
    "n" : 3,
    "scanAndOrder" : true,
    "millis" : 0,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {   
    }
}

仅仅因为文档被索引。

如果您创建这样的索引:

db.a.ensureIndex({'_id.e' : 1, '_id.p' : 1})

然后再次查询:

db.a.find({'_id.p' : 2}).sort({'_id.e' : 1}).explain()

{
    "cursor" : "BtreeCursor _id.e_1__id.p_1",
    "nscanned" : 9,
    "nscannedObjects" : 3,
    "n" : 3,
    "millis" : 0,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "_id.e" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ],
        "_id.p" : [
            [
                2,
                2
            ]
        ]
    }
}

由于排序,它将查询索引(nscanned:9),然后获取对象:3,这比按 _id 排序(nscanned 和 nscannedObjects 为 9)要好。

文档 .explain()

因此,对于高写入吞吐量(每秒超过 15k 次写入),您可能会分片。如果设置了选项,两个索引都将保证唯一性。但是只有复合分片键可以帮助您进行直接查询,而不是分散收集。

使用 ({'_id.e' : 1, '_id.p' : 1}) 作为分片键将直接路由所有“_id.e”查询,而不是“_id.p”(没有“e”)查询,所以这些查询将发送到每个主机并以索引查找结束,但也可能很快(取决于网络等)。如果您想通过“p”对这些查询进行聚类,则必须将“_id.p”作为复合键的第一部分,如下所示:

{'_id.p' : 1, '_id.e' : 1}

所以所有“p”查询都是直接查询。但是,是的,这会将最近发生的事件分散到整个集群中。因此,使用基于时间的键的单独索引可能会加快这些分散查询。

我会生成一些示例数据,并在开发系统上使用两个分片的设置中使用它,并使用 .explain() 来选择分片键 + 索引。

于 2012-12-12T15:01:11.503 回答