MongoDB中的分层存储引擎:优化延迟及降低成本

作为一个面向用户的应用,速度及正常运行时间都是成功的关键因素。您可以使用大量方法调整应用和硬件配置,为您的客户提供最佳体验,而关键在于花费最低的成本。在这里,我们提供了一个使用MongoDB分层存储提高性能、降低成本的案例,一种根据不同延迟需求对数据存储分配不同优先级的方法。

在这个案例中,我们将对使用日期对数据进行分割:用户会比较频繁访问最近的数据,系统应该提供为最近的数据提供比 较远数据 更低的延迟。当然,这个想法也可以用于其它分割数据的方式,例如:位置、用户、来源、大小或者其它标准。这个方法利用了MongoDB中 标签感知分片(tag-aware sharding)的强大功能,MongoDB 2.2版本就已经开始提供该功能了。

示例应用:保险理赔

在许多应用中,随着数据保存时间的增加,对数据的低延迟读取就变得越来越不重要。例如,一个保险公司也许会优先考虑过去12个月中理赔数据的读取。用户应该能够快速查阅最近的理赔,但是一旦理赔超过一年,那么它们的读取频率将会大大降低,而时间延迟方面的需求也会变得不那么紧急。

通过创建不同性能和成本属性的分层存储,保险公司可以在优化成本的同时,为用户提供一个更好的体验。时间比较长的理赔可以存储在类似于商品硬件驱动器之类更划算的硬件存储层中。而最近的数据可以存储在类似于SSD之类可以提供更低延迟的高性能存储层。由于大部分的理赔都是一年之前的,在低成本层中存储数据可以提供极大的成本优势。保险公司可以在两个层之间优化他们的硬件分布,在最佳成本点的基础上提供绝佳的用户体验。

该应用场景的需求总结如下:

  • 最近12个月的理赔应该存储在更快的存储层
  • 一年之前的理赔应该移动到更慢的存储层
  • 随着时间的推移,不断有新理赔出现,之前的理赔需要从更快的存储层移动到更慢的存储层
    为了简化,在整篇文章中,我们将理赔数据分为”current”和”tier-2″两种数据。

搭建自己的进程:一个操作上令人头疼的问题

满足这些需求的一个方法是使用定期的批处理任务:选择数据并把数据加载到存档文件中,然后将其从更快的存储层移除。然而,这些任务本质上看来非常复杂:

  • 移动过程必须认真编写代码以便失败发生时可以得到很好的处理。面临一个导入失败的情况,您肯定不想删除原始数据。
  • 如果移动的数据非常庞大,您也许想要调节操作速度。
  • 如果只成功移动了部分数据,您将不得不重新尝试未完成的数据。
  • 除非您打算在移动阶段暂停您的应用(一般来说不会这样),您的应用需要自定义代码来查找移动之前、之中以及之后的数据。
  • 您的应用需要了解数据的物理位置,这将无谓地增加了代码关于分区逻辑的复杂性。

此外,向您的操作中引入其它自定义的组件将会需要额外的维护和监控。
这是许多团队不得不面临的、操作上令人头疼的问题,但是这里有一个更简单的方法:让MongoDB透明地处理将需要迁移的文档从”current”存储机器迁移到”tier-2″存储机器进行导入。结果证明,您可以使用一个名为标签感知分片(Tag-Aware Sharding)的功能非常容易地实现这一方法。

MongoDB的方法:标签感知分片

MongoDB提供了一个分片的功能实现在多台机器之间横向拓展系统。分片对您的应用而言是透明的:不管您有1个或者100个分片,您的应用端代码都是相同的。请查阅分片指南了解对分片的全面阐述。

分片的一个主要组件是平衡器进程。随着集合的增长,运行在分片之间的后台平衡器小心地移动文档。通常情况下平衡器在分片之间归档文档。但是,通过使用标签感知分片,我们可以构建影响文档存储位置的策略。这个功能可以用于许多用例。一个示例就是将用户数据存储在靠近用户的数据中心。在我们的应用中,我们可以使用这个功能来将”current”数据存储在我们的快服务器上,”tier-2″数据存储在更便宜、更慢的服务器上。

下面是它的实现过程:

  • 为分片分配标签。其中,标签是一个类似于“London-DC”之类的字母数字别称。
  • 将唯一的片键范围“钉在”标签中。
  • 在正常的平衡操作中,数据段只会迁移到那些带有标签,且标签的键值范围包含了数据段键值范围的分片。
  • 当一个数据段的键值范围与不止一个标签范围发生重叠时,会根据具体情况进行一些微妙的调整。针对特殊案例,请认真阅读文档。

这就意味着我们可以将”tier-2″标签分配给运行在慢服务器的分片,将”current”标签分配给运行在快服务器的分片,平衡器将会自动解决数据在不同层之间的迁移。美妙的地方在于:我们可以在一个数据库中维护所有数据,因此,数据在存储层之间移动时,我们的应用端代码不需要做任何修改。

确定片键

当你查询一个分片集合时,查询路由器将会尽其所能只检测存储着相应数据的分片,但是只有在您提供了片键作为查询的一个部分时,才能实现这个功能。(请查阅分片集群查询路由以了解更多信息。)

因此,我们需要确保通过片键查找文档。我们也知道时间是我们两层存储层中决定文档位置的基础。因此,片键必须包含一个显式的时间戳。在我们的示例中,我们将使用Enron的电子邮箱数据库,并且将顶层“日期”作为片键。下面是一个示例文档:

{
 "_id": ObjectId("4f16fc97d1e2d32371003f87"),
 "body": "i say el tiemponnnTo: Timothy Blanchard/HOU/EES@EES, Bryan Hull/HOU/ECT@ECT, Luis nMena/NA/Enron@Enron, Lisa Gillette/HOU/ECT@ECT, Susan M Scott/HOU/ECT@ECT, nShanna Husser/HOU/EES@EES, Eric Bass/HOU/ECT@ECT,mmmarcantel@equiva.comncc: nSubject: nndoes everyone want to meet at tortucas on kirby south of 59 or el tiempo(no ntequilla shots) or cabos downtown tonight. let's meet around 6-6:30.nn",
 "date": ISODate("2001-03-01T09:55:00Z"),
 "filename": "1107.",
 "headers": {
 "Content-Transfer-Encoding": "7bit",
 "Content-Type": "text/plain; charset=us-ascii",
 "From": "eric.bass@enron.com",
 "Message-ID": "<25125852.1075854773620.JavaMail.evans@thyme>",
 "Mime-Version": "1.0",
 "Subject": "Re:",
 "To": [
 "matthew.lenhart@enron.com"
 ],
 "X-FileName": "ebass.nsf",
 "X-Folder": "Eric_Bass_Jun2001Notes FoldersSent",
 "X-From": "Eric Bass",
 "X-Origin": "Bass-E",
 "X-To": "Matthew Lenhart",
 "X-bcc": "",
 "X-cc": ""
 },
 "mailbox": "bass-e",
 "subFolder": "sent"
}

由于时间是存储在日期中最显著的部分,任意给定日期的信息在数字上将会小于随后日期的信息。

实现

下面是搭建这个系统的一些步骤:

  • 创建一个空的MongoDB分片集群
  • 创建一个目标数据库用于存储分片的集合
  • 根据存储层将标签分配给不同的分片
  • 将标签范围分配给分片
  • 将数据加载到MongoDB集群

搭建MongoDB集群

第一件需要做的事情是:搭建分片集群。您可以在这里查阅更多关于搭建分片集群的信息。

在这个示例中,我们创建一个名为”enron”的数据库和名为”messages”的集合,用于存储Enron电子邮件集合的部分数据。我们已经搭建了一个包含3个分片的集群。第一个,shard0000,为数据的低延迟读取进行了优化。其它两个:shard0001和shard0002,使用性价比更高的硬件存储那些早于指定截止日期的数据。

下面是我们的分片集群,现在是没有存储任何数据的空机器:

sh.status()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("53616554992f1bbce576f9fd")
}
shards:
{ "_id" : "shard0000", "host" : "Server1:27017" }
{ "_id" : "shard0001", "host" : "Server2:27017" }
{ "_id" : "shard0002", "host" : "Server3:27017" }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }

增加标签

我们可以“标记”每个分片,使它们与那些应该属于我们”current”层或者那些应该属于”tier-2″的文档联系起来。在没有标签和基于范围的标签时,平衡器将会在不考虑任何其它字段中数据的情况下,尝试保证每个分片中的数据段都相等。在将数据添加到集合之前,先让我们将shard0000“标记”为”current”,将其它两个“标记”为”tier-2″:

sh.addShardTag('shard0000', 'current')
sh.addShardTag('shard0001', 'tier-2')
sh.addShardTag('shard0002', 'tier-2')

现在,我们可以通过调用sh.status()来验证我们的标签:

sh.status()
--- Sharding Status ---
...
"clusterId" : ObjectId("53616554992f1bbce576f9fd")
}
shards:
{ "_id" : "shard0000", "host" : "Server1:27017", "tags" : [ "current" ] }
{ "_id" : "shard0001", "host" : "Server2:27017", "tags" : [ "tier-2" ] }
{ "_id" : "shard0002", "host" : "Server3:27017", "tags" : [ "tier-2" ] }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }

接下来,我们需要创建存储Enron电子邮件的数据库和集合。我们将创建一个有’messages’集合的新数据库’enron’,并且在集合上启用分片:

use enron
switched to db enron

db.createCollection("messages")
{ "ok" : 1 }

sh.enableSharding("enron")
{ "ok" : 1 }

因为我们打算对集合进行分片,将需要生成一个片键。我们将会使用’date’字段作为片键,因为这是用来定义文档如何在分片间分布的字段:

db.messages.ensureIndex({date:1})

sh.shardCollection('enron.messages',{date:1})
{ "collectionsharded" : "enron.messages", "ok" : 1 }

** 定义存储层之间的截止日期 **

“current”数据和”tier-2″数据之间的截止日期是我们需要定期更新的时间点,并且将最近的文档保存在”current”分片中。我们从2001年7月1日作为截止日期开始,将其保存为ISO日期:ISODate(“2001-07-01″)。一旦向”messages”集合增加文档,任何晚于2001年7月1日的文档将会存储到”current”分片,早于2001年7月1日的文档将会存储到”tire-2″分片。

//Add the tags
sh.addTagRange('enron.messages',{date:MinKey},{date:ISODate("2001-07-01")},'tier-2')
sh.addTagRange('enron.messages',{date:ISODate("2001-07-01")},{date:MaxKey},'current')

两个范围在一个相同的时间点有重合非常重要。一个标签范围的下界将会属于这个标签范围,而上界则被排除了。也就是说:日期正好为ISODate(“2001-07-01″)的文档将会存储于”current”分片而不是”tier-2″分片。

接下来,您将看到每个分片的新标签范围:

sh.status()
--- Sharding Status ---
...
"clusterId" : ObjectId("53616554992f1bbce576f9fd")
}
shards:
{ "_id" : "shard0000", "host" : "Server1:27017", "tags" : [ "current" ] }
{ "_id" : "shard0001", "host" : "Server2:27017", "tags" : [ "tier-2" ] }
{ "_id" : "shard0002", "host" : "Server3:27017", "tags" : [ "tier-2" ] }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
///delete above { "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
{ "_id" : "enron", "partitioned" : true, "primary" : "shard0000" }
enron.messages
shard key: { "date" : 1 }
chunks:
shard0001 1
shard0000 1
{ "date" : { "$minKey" : 1 } } -->> { "date" : ISODate("2001-07-01T00:00:00Z") } on : shard0001 Timestamp(2, 0)
{ "date" : ISODate("2001-07-01T00:00:00Z") } -->> { "date" : { "$maxKey" : 1 } } on : shard0000 Timestamp(2, 1)
tag: tier-2 { "date" : { "$minKey" : 1 } } -->> { "date" : ISODate("2001-07-01T00:00:00Z") }
tag: current { "date" : ISODate("2001-07-01T00:00:00Z") } -->> { "date" : { "$maxKey" : 1 } }

作为最后的检查,在配置数据库中查看标签范围的定义。

var configdb = db.getSiblingDB("config")
configdb.tags.find().pretty()
{
"_id" : {
"ns" : "enron.messages",
"min" : {
"date" : { "$minKey" : 1 }
}
},
"ns" : "enron.messages",
"min" : {
"date" : { "$minKey" : 1 }
},
"max" : {
"date" : ISODate("2001-07-01T00:00:00Z")
},
"tag" : "tier-2"
}
{
"_id" : {
"ns" : "enron.messages",
"min" : {
"date" : ISODate("2001-07-01T00:00:00Z")
}
},
"ns" : "enron.messages",
"min" : {
"date" : ISODate("2001-07-01T00:00:00Z")
},
"max" : {
"date" : { "$maxKey" : 1 }
},
"tag" : "current"
}

现在所有分片和范围都已经定义完成,我们已经准备好将消息数据加载到数据库中。集合将会按照标签范围的指示,分布到正确的机器上。

mongorestore -d enron -c messages enron/messages.bson

现在,让我们检查一下分片状态来了解文档的存储情况。

sh.status()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("53616554992f1bbce576f9fd")
}
shards:
{ "_id" : "shard0000", "host" : "Server1:27017", "tags" : [ "current" ] }
{ "_id" : "shard0001", "host" : "Server2:27017", "tags" : [ "tier-2" ] }
{ "_id" : "shard0002", "host" : "Server3:27017", "tags" : [ "tier-2" ] }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
{ "_id" : "enron", "partitioned" : true, "primary" : "shard0000" }
enron.messages
shard key: { "date" : 1 }
chunks:
shard0002 6
shard0001 7
shard0000 3
{ "date" : { "$minKey" : 1 } } -->> { "date" : ISODate("2001-02-01T07:48:00Z") } on : shard0002 Timestamp(5, 0)
{ "date" : ISODate("2001-02-01T07:48:00Z") } -->> { "date" : ISODate("2001-02-09T11:24:00Z") } on : shard0002 Timestamp(6, 0)
{ "date" : ISODate("2001-02-09T11:24:00Z") } -->> { "date" : ISODate("2001-02-23T08:09:00Z") } on : shard0002 Timestamp(7, 0)
{ "date" : ISODate("2001-02-23T08:09:00Z") } -->> { "date" : ISODate("2001-03-06T09:11:00Z") } on : shard0002 Timestamp(8, 0)
{ "date" : ISODate("2001-03-06T09:11:00Z") } -->> { "date" : ISODate("2001-03-19T19:21:00Z") } on : shard0002 Timestamp(9, 0)
{ "date" : ISODate("2001-03-19T19:21:00Z") } -->> { "date" : ISODate("2001-03-28T15:04:00Z") } on : shard0002 Timestamp(10, 0)
{ "date" : ISODate("2001-03-28T15:04:00Z") } -->> { "date" : ISODate("2001-04-10T16:06:49Z") } on : shard0001 Timestamp(10, 1)
{ "date" : ISODate("2001-04-10T16:06:49Z") } -->> { "date" : ISODate("2001-04-23T17:00:35Z") } on : shard0001 Timestamp(3, 10)
{ "date" : ISODate("2001-04-23T17:00:35Z") } -->> { "date" : ISODate("2001-05-06T23:20:00Z") } on : shard0001 Timestamp(3, 11)
{ "date" : ISODate("2001-05-06T23:20:00Z") } -->> { "date" : ISODate("2001-05-11T15:19:00Z") } on : shard0001 Timestamp(4, 1)
{ "date" : ISODate("2001-05-11T15:19:00Z") } -->> { "date" : ISODate("2001-05-23T10:06:00Z") } on : shard0001 Timestamp(4, 2)
{ "date" : ISODate("2001-05-23T10:06:00Z") } -->> { "date" : ISODate("2001-06-05T16:24:00Z") } on : shard0001 Timestamp(3, 14)
{ "date" : ISODate("2001-06-05T16:24:00Z") } -->> { "date" : ISODate("2001-07-01T00:00:00Z") } on : shard0001 Timestamp(3, 15)
{ "date" : ISODate("2001-07-01T00:00:00Z") } -->> { "date" : ISODate("2001-07-27T09:08:00Z") } on : shard0000 Timestamp(3, 2)
{ "date" : ISODate("2001-07-27T09:08:00Z") } -->> { "date" : ISODate("2001-09-01T03:05:08Z") } on : shard0000 Timestamp(3, 3)
{ "date" : ISODate("2001-09-01T03:05:08Z") } -->> { "date" : { "$maxKey" : 1 } } on : shard0000 Timestamp(4, 0)
tag: tier-2 { "date" : { "$minKey" : 1 } } -->> { "date" : ISODate("2001-07-01T00:00:00Z") }
tag: current { "date" : ISODate("2001-07-01T00:00:00Z") } -->> { "date" : { "$maxKey" : 1 } }

没错!mongos进程自动移动文档以满足标签范围。在这个示例中,它将”current”分片中ISODate早于 ISODate(“2001-07-01T00:00:00Z”)的所有文档移动到了”tier-2″分片上。

对标签范围定期更新非常重要,需要保证截止点在过去(1年,在本示例中)和现在的正确时间间隔上。为了实现这一点,两个范围都需要进行更新。为了执行这个修改,需要临时禁止平衡器,以保证不存在范围重合的时间点。暂时停止平衡器是一个安全的操作:它并不会影响应用或者用户体验。

如果您想将截止日期往后推一个月,到2001年8月1日,您只需要完成下列几个步骤:

  • 停止平衡器:sh.setBalancerState(false)
  • 在8月1日创建一个数据段分割点:sh.splitAt(‘enron.messages’, {“date” : ISODate(“2001-08-01″)})
  • 将截止日期移动到 ISODate(“2001-08-01T00:00:00Z”):
var configdb=db.getSiblingDB("config");
configdb.tags.update({tag:"tier-2"},{$set:{'max.date':ISODate("2001-08-01")}})
configdb.tags.update({tag:"current"},{$set:{'min.date':ISODate("2001-08-01")}})
  • 重启平衡器:sh.setBalancerState(true)
  • 验证分片状态
sh.status()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 3,
"currentVersion" : 4,
"clusterId" : ObjectId("5314f1487abd6cb2803696d6")
}
shards:
{ "_id" : "shard0000", "host" : "Server1:27017", "tags" : [ "current" ] }
{ "_id" : "shard0001", "host" : "Server1:27017", "tags": [ "tier-2" ] }
{ "_id" : "shard0002", "host" : "Server1:27017", "tags": [ "tier-2" ] }
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
{ "_id" : "enron", "partitioned" : true, "primary" : "shard0000" }
enron.messages
shard key: { "date" : 1 }
chunks:
shard0001 7
shard0002 6
shard0000 2
{ "date" : { "$minKey" : 1 } } -->> { "date" : ISODate("2001-02-16T10:55:00Z") } on : shard0001 Timestamp(2, 0)
{ "date" : ISODate("2001-02-16T10:55:00Z") } -->> { "date" : ISODate("2001-03-06T10:04:00Z") } on : shard0002 Timestamp(3, 0)
{ "date" : ISODate("2001-03-06T10:04:00Z") } -->> { "date" : ISODate("2001-03-20T22:12:00Z") } on : shard0001 Timestamp(4, 0)
{ "date" : ISODate("2001-03-20T22:12:00Z") } -->> { "date" : ISODate("2001-04-03T13:37:14Z") } on : shard0002 Timestamp(5, 0)
{ "date" : ISODate("2001-04-03T13:37:14Z") } -->> { "date" : ISODate("2001-04-17T13:50:16Z") } on : shard0001 Timestamp(6, 0)
{ "date" : ISODate("2001-04-17T13:50:16Z") } -->> { "date" : ISODate("2001-04-27T16:29:00Z") } on : shard0002 Timestamp(7, 0)
{ "date" : ISODate("2001-04-27T16:29:00Z") } -->> { "date" : ISODate("2001-05-09T12:27:00Z") } on : shard0001 Timestamp(8, 0)
{ "date" : ISODate("2001-05-09T12:27:00Z") } -->> { "date" : ISODate("2001-05-18T07:46:00Z") } on : shard0002 Timestamp(9, 0)
{ "date" : ISODate("2001-05-18T07:46:00Z") } -->> { "date" : ISODate("2001-05-30T19:21:00Z") } on : shard0001 Timestamp(10, 0)
{ "date" : ISODate("2001-05-30T19:21:00Z") } -->> { "date" : ISODate("2001-06-08T12:49:56Z") } on : shard0002 Timestamp(11, 0)
{ "date" : ISODate("2001-06-08T12:49:56Z") } -->> { "date" : ISODate("2001-07-01T00:00:00Z") } on : shard0001 Timestamp(12, 0)
{ "date" : ISODate("2001-07-01T00:00:00Z") } -->> { "date" : ISODate("2001-07-12T20:25:00Z") } on : shard0002 Timestamp(13, 0)
{ "date" : ISODate("2001-07-12T20:25:00Z") } -->> { "date" : ISODate("2001-08-01T00:00:00Z") } on : shard0001 Timestamp(14, 0)
{ "date" : ISODate("2001-08-01T00:00:00Z") } -->> { "date" : ISODate("2001-08-20T19:34:00Z") } on : shard0000 Timestamp(14, 1)
{ "date" : ISODate("2001-08-20T19:34:00Z") } -->> { "date" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 12)
tag: tier-2 { "date" : { "$minKey" : 1 } } -->> { "date" : ISODate("2001-08-01T00:00:00Z") }
tag: current { "date" : ISODate("2001-08-01T00:00:00Z") } -->> { "date" : { "$maxKey" : 1 } }

通过将数据段分割更新到8月1日,我们已经将7月1日之后8月1日之前的所有文档从”current”分片迁移到”tier-2″分片。好消息是:我们可以不用修改应用端代码在数据零宕机的情况下执行这个操作。我们也可以看到:通过一个外部进程来自动定期运行这个过程是非常简单的。

从头疼的运维问题到简单操作

最终的结果是:一个集合存储于三个分片、两个不同的存储系统中,这个解决方案能够在不增加系统架构复杂性的同时帮助您降低存储成本。不用再在不同的机器上搭建不同数据库这样的复杂设置,我们只需要查询一个数据库。不用再进行数据的迁移,更新一些简单的规则就可以控制系统中数据的位置。

想要了解更多关于MongoDB的信息?了解MongoDB 3.2中的最新功能:
MongoDB 3.2 中的新功能

翻译:周颖敏

审核:张耀星

原文链接:https://www.mongodb.com/blog/post/tiered-storage-models-in-mongodb-optimizing-latency-and-cost

发表评论