适用于仅插入工作负载的分布式本地写入
MongoDB标签感知分片允许管理员定义分片分片键的范围并将其标记到一个或多个分片,从而控制分片分片集群中的数据分布。
本教程使用区域以及多数据中心分片集群部署和应用程序端逻辑来支持分布式本地写入,以及副本集选举或数据中心故障时的高写入可用性。
通过对空集合或不存在的集合进行分片之前定义区域和区域范围,分片集合操作会为定义的区域范围创建数据块以及任何其他数据块,以覆盖分片键值的整个范围,并执行基于区域范围的初始数据块分配。数据块的初始创建和分布可以更快地设置区域分片。在初始分布之后,负载均衡器将管理未来的数据段分布。
有关示例,请参阅为空集合或不存在的集合预先定义区域和区域范围。
重要
Scenario
考虑一个插入密集型应用程序,与写入相比,读取频率较低且优先级较低。 应用程序将文档写入到一个分片collection,并需要数据库近乎恒定的正常运行时间来支持其 SLA 或 SLO。
下图是应用程序写入数据库的文档格式的部分视图:
{ "_id" : ObjectId("56f08c447fe58b2e96f595fa"), "message_id" : 329620, "datacenter" : "alfa", "userid" : 123, ... } { "_id" : ObjectId("56f08c447fe58b2e96f595fb"), "message_id" : 578494, "datacenter" : "bravo", "userid" : 456, ... } { "_id" : ObjectId("56f08c447fe58b2e96f595fc"), "message_id" : 689979, "datacenter" : "bravo", "userid" : 789, ... }
片键
该collection使用{ datacenter : 1, userid : 1 }
复合索引作为片键。
每个文档中的datacenter
字段允许在每个不同的数据中心值上创建标签范围。 如果没有datacenter
字段,就无法将文档与特定数据中心关联起来。
userid
字段为分片键提供相对于 datacenter
的高关联基数和低频分量。
有关选择分片键的更多一般说明,请参阅选择分片键。
架构
该部署由两个数据中心alfa
和bravo
组成。 有两个分片: shard0000
和shard0001
。 每个分片都是一个具有三个成员的副本集。 shard0000
在alfa
上有两个节点,在bravo
上有一个优先级为 0的节点。 shard0001
在bravo
上有两个成员,在alfa
上有一个优先级为 0 的成员。
标记
此应用程序需要为每个数据中心配备一个标签。 根据包含大多数副本集成员的数据中心,为每个分片分配一个标签。 有两个标签范围,每个数据中心一个。
alfa
数据中心将此数据中心上拥有多数成员的分片标记为
alfa
。创建标签范围:
{ "datacenter" : "alfa", "userid" : MinKey }
的下限,{ "datacenter" : "alfa", "userid" : MaxKey }
的上限,以及标签
alfa
bravo
数据中心将此数据中心上拥有多数成员的分片标记为
bravo
。创建标签范围:
{ "datacenter" : "bravo", "userid" : MinKey }
的下限,{ "datacenter" : "bravo", "userid" : MaxKey }
的上限,以及标签
bravo
根据配置的标签和标签范围, mongos
将带有datacenter : alfa
的文档路由到alfa
数据中心,并将带有datacenter : bravo
的文档路由到bravo
数据中心。
写入操作
如果插入或更新的文档与配置的标签范围匹配,则只能将其写入具有相关标签的分片。
MongoDB 可以将与配置的标签范围不匹配的文档写入集群中的任何分片。
注意
上述行为要求集群处于稳定状态,没有违反配置标签范围的数据段。 有关更多信息,请参阅以下有关负载均衡器的部分。
平衡器
负载均衡器将带标签的数据块迁移到相应的分片。在迁移之前,分片可能包含违反已配置标签范围和标签的数据段。 均衡完成后,分片应仅包含范围不违反分配的标签和标签范围的数据块。
添加或删除标签或标签范围可能会导致数据块迁移。根据数据集的大小和标签范围影响的数据段数量,这些迁移可能会影响集群性能。 考虑在特定的计划窗口运行负载均衡器。 有关如何设置安排窗口的教程,请参阅安排窗口。
应用程序行为
默认情况下,应用程序会写入最近的数据中心。 如果本地数据中心当机,或者在设定的时间段内未确认对该数据中心的写入,则应用程序会在尝试将文档写入数据库之前,通过更改datacenter
字段的值来切换到其他可用的数据中心。
应用程序支持写入超时。 应用程序使用写关注(write concern)为每个写入操作设置超时。
如果应用程序遇到写入或超时错误,它会修改每个文档中的datacenter
字段并执行写入。 这会将文档路由到其他数据中心。 如果两个数据中心均已关闭,则写入无法成功。 请参阅解决写入失败问题。
该应用程序会定期检查与标记为“关闭”的任何数据中心的连接。 如果连接恢复,则应用程序可以继续执行正常的写入操作。
考虑到切换逻辑以及用于处理数据中心之间客户端流量的任何负载均衡器或类似机制,应用程序无法预测给定文档将写入两个数据中心中的哪一个。 为了确保在读取操作中不错过任何文档,应用程序必须执行广播查询,在任何查询中都不包含datacenter
字段。
应用程序使用读取偏好(read preference)nearest
执行读取,以减少延迟。
尽管报告了超时错误,但写入操作仍有可能成功。 应用程序通过尝试将文档重新写入其他数据中心来响应错误,这可能会导致文档在两个数据中心之间重复。 应用程序将解决重复项作为读取逻辑的一部分。
切换逻辑
如果一个或多个写入失败,或者写入未在设定的时间段内得到确认,则应用程序具有切换数据中心的逻辑。 应用程序根据目标数据中心的标签修改datacenter
字段,将文档定向到该数据中心。
例如,尝试写入alfa
数据中心的应用程序可能会遵循以下一般过程:
尝试写入文档,并指定
datacenter : alfa
。写入超时或出错时,将
alfa
记录为暂时关闭。尝试写入同一文档,同时修改
datacenter : bravo
。写入超时或出错时,将
bravo
记录为暂时关闭。如果
alfa
和bravo
均已关闭,则记录并报告错误。
请参阅解决写入失败问题。
步骤
配置分片标签
mongos
您必须连接到与目标 分片集群 关联的 才能继续。您无法通过直接连接到分片副本集成员来创建标签。
标记每个分片。
使用alfa
标签标记alfa
数据中心中的每个分片。
sh.addShardTag("shard0000", "alfa")
使用bravo
标签标记bravo
数据中心中的每个分片。
sh.addShardTag("shard0001", "bravo")
您可以通过运行sh.status()
查看分配给任何给定分片的标签。
为每个标签定义范围。
定义alfa
数据库的范围,并使用sh.addTagRange()
方法将其与alfa
标签关联。 此方法需要:
目标集合的完整命名空间。
范围的下限(包含在内)。
范围的独占上限。
标签的名称。
sh.addTagRange( "<database>.<collection>", { "datacenter" : "alfa", "userid" : MinKey }, { "datacenter" : "alfa", "userid" : MaxKey }, "alfa" )
定义bravo
数据库的范围,并使用sh.addTagRange()
方法将其与bravo
标签关联。 此方法需要:
目标集合的完整命名空间。
范围的下限(包含在内)。
范围的独占上限。
标签的名称。
sh.addTagRange( "<database>.<collection>", { "datacenter" : "bravo", "userid" : MinKey }, { "datacenter" : "bravo", "userid" : MaxKey }, "bravo" )
MinKey
和MaxKey
值是保留用于比较的特殊值。 MinKey
在比较时始终小于所有其他可能的值,而MaxKey
在比较时始终大于所有其他可能的值。 配置的范围捕获每个datacenter
的每个用户。
查看更改。
均衡完成后,标记为alfa
的分片应仅包含具有datacenter : alfa
的文档,而标记为bravo
的分片应仅包含具有datacenter : bravo
的文档。
您可以通过运行sh.status()
查看数据段分布。
解决写入失败问题
当应用程序的默认数据中心关闭或无法访问时,应用程序会将datacenter
字段更改为其他数据中心。
例如,默认情况下,应用程序会尝试将以下文档写入alfa
数据中心:
{ "_id" : ObjectId("56f08c447fe58b2e96f595fa"), "message_id" : 329620, "datacenter" : "alfa", "userid" : 123, ... }
如果应用程序在尝试写入时收到错误,或者写入确认用时过长,则会将该数据中心记录为不可用,并将字段datacenter
更改为指向bravo
数据中心。
{ "_id" : ObjectId("56f08c457fe58b2e96f595fb"), "message_id" : 329620, "datacenter" : "bravo", "userid" : 123, ... }
应用程序会定期检查alfa
数据中心的连接性。 如果可以再次访问数据中心,则应用程序可以恢复正常写入。
解决读取时的重复文档问题
应用程序的切换逻辑允许潜在的文档重复。 执行读取时,应用程序会解析应用程序层上的所有重复文档。
以下查询搜索userid
为123
的文档。 请注意,虽然userid
是分片键的一部分,但该查询不包含datacenter
字段,因此不会执行定向读取操作。
db.collection.find( { "userid" : 123 } )
结果显示, message_id
为329620
的文档已被插入 MongoDB 两次,这可能是由于延迟写入确认造成的。
{ "_id" : ObjectId("56f08c447fe58b2e96f595fa"), "message_id" : 329620 "datacenter" : "alfa", "userid" : 123, data : {...} } { "_id" : ObjectId("56f08c457fe58b2e96f595fb"), "message_id" : 329620 "datacenter" : "bravo", "userid" : 123, ... }
应用程序可以忽略重复项,获取两个文档中的一个,也可以尝试修剪重复项,直到只剩下一个文档。
删除重复项的一种方法是使用ObjectId.getTimestamp()
方法从_id
字段中提取时间戳。 然后,应用程序可以保留插入的第一个文档或最后一个插入的文档。 假设_id
字段使用 MongoDB ObjectId()
。
例如,在包含ObjectId("56f08c447fe58b2e96f595fa")
的文档上使用getTimestamp()
} 会返回:
ISODate("2016-03-22T00:05:24Z")
在包含ObjectId("56f08c457fe58b2e96f595fb")
的文档上使用getTimestamp()
} 会返回:
ISODate("2016-03-22T00:05:25Z")