选择分片字段
将存储在MongoDB数据库中的Collection
进行分片需要合理选择分片Key
,它直接决定了集群中数据分布是否均衡、集群性能是否良好。
下面是一个记录日志的Document。
{
server : "ny153.example.com" ,
application : "apache" ,
time : "2021-01-02T21:21:56.249Z" ,
level : "ERROR" ,
msg : "something is broken"
}
分片Key
的选择需要考虑如下几点。
基数
Mongodb中一个被分片的Collection
的所有数据都存放在众多的Chunk
中。一个Chunk
存放分片字段的一个区间范围的数据。选择一个好的分片字段非常重要,否则就会遭遇到不能被拆分的大Chunk
。
以上述的日志为例,如果选择{server:1}
来作为一个分片Key
的话,那么server
上的所有数据都是在同一个Chunk
中,很容易想到日志数据会超过200MB(默认Chunk大小)。
如果分片Key
是{server:1,time:1}
,那么能够将一个Server
上的日志信息进行分片,直至毫秒级别,绝对不会存在不可被拆分的Chunk
。
将Chunk
的规模维持在一个合理的大小是非常重要的,只有这样数据才能均匀分布,并且移动Chunk
的代价也不会过大。
写操作可扩展
使用分片的一个主要原因之一是分散写操作。为了实现这个目标,尽可能的将写操作分散到多个Chunk
就尤为重要了。
如果选择{time:1}
来作为分片key
将导致所有的写操作都会落在最新的一个Chunk
上,这样就形成了一个热点区域。
如果选择{server:1,application:1,time:1}
来作为分片Key
的话,那么每一个server
上的应用的日志信息将会写在不同的地方,如果有100个应用和10个server
,那么每一个server
将会分担1/10的写操作。
查询隔离
另外一个需要考虑的是任何一个查询操作将会由多少个分片来来提供服务。
最理想的情况是,一个查询操作直接从Mongos
进程路由到一个Mongodb
上,并且这个Mongodb
拥有该次查询的全部数据。
因此,如果通用的查询操作的都以server
作为查询条件的话,那么以server
作为一个起始的分片Key
会使整个集群更加高效。
任何一个查询都能执行,不管使用什么来作为分片Key
。但是,如果Mongos
进程不知道是哪一个Mongodb
的分片拥有要查询的数据的话,Mongos
将会让所有的Mongod
分片去执行查询操作,再将结果信息汇总起来返回。
显而易见,这会增加服务器的响应时间,会增加网络成本,也会无谓的增加了负担。
排序
在需要调用sort()
来查询排序后的结果的时候,以分片Key
的最左边的字段为依据,Mongos
可以按照预先排序的结果来查询最少的分片,并且将结果信息返回给调用者。这样会花最少的时间和资源代价。
相反,如果在利用sort()
排序的时候,排序所依据的字段不是最左侧(起始)的分片Key
,那么Mongos
将不得不并行的将查询请求传递给每一个分片,然后将各个分片返回的结果合并之后再返回请求方。这个会给Mongos
增加额外的负担。
可靠性
选择分片Key
的一个非常重要因素是万一某一个分片彻底不可访问了,受到影响的Chunk
有多大。
例如,有一个类似于Twitter
的系统,其Comment
记录类似如下形式。
{
_id: ObjectId("4d084f78a4c8707815a601d7"),
user_id : 42 ,
time : "2021-01-02T21:21:56.249Z" ,
comment : "I am happily using MongoDB",
}
由于这个系统对写操作非常敏感,所以需要将写操作扁平化的分布到所有的Server
上去,这个时候就需要用id
或者user_id
来作为分片Key
了。
使用id
作为分片Key
有最大粒度的扁平化,但是在一个分片宕机的情况下,会影响几乎所有的用户(一些数据丢失了)。
如果使用user_id
作为分片Key
,只有极少比率的用户会收到影响(在存在5个分片的时候,20%的用户受影响),但是这些用户会再也不会看到他们的数据了。
索引优化
如果只有一部分的索引被读或者更新的话,通常会有更好的性能,因为“活跃”的部分在大多数时间内能驻留在内存中。
上面选择分片Key
的方法都是为了最终数据能够均匀的分布,与此同时,每一个Mongod
的索引信息也被均匀分布了。
相反,使用时间戳作为分片key
的起始字段会有利于将常用索引限定在较小的一部分。
例如,有一个图片存储系统,图片记录类似于如下形式。
{
_id: ObjectId("4d084f78a4c8707815a601d7"),
user_id : 42 ,
title: "sunset at the beach",
upload_time : "2021-01-02T21:21:56.249Z" ,
data: ...,
}
可以构造一个客户id
,让它包括图片上传时间对应的信息和一个唯一标志符。记录看起来就像下面这个样子。
{
_id: "2021-01-02_4d084f78a4c8707815a601d7",
user_id : 42 ,
title: "sunset at the beach",
upload_time : "2021-01-02T21:21:56.249Z" ,
data: ...,
}
客户id
作为分片key
,并且这个id
也被用于去访问Document
。这样既能将数据均衡的分布在各个节点上,也减少了大多数查询所使用的索引比例。
更进一步来讲:在每一个月份的开始,在最初的一段时间内只有一个server
来存取数据。
随着数据量的增长,负载均衡器(balancer)就开始进行分裂和迁移数据块了。为了避免潜在的低效率和迁移数据,预先创建分片范围区间是明智之举。
可以继续改进,把user_id
包含到图片的id
中来。这样的话会让一个用户的所有Document
都在一个分片上。比如用2021-01-02_42_4d084f78a4c8707815a601d7
作为图片的id。
GridFS
根据需求的不同,GridFS
有几种不同的分片方法。基于预先存在的索引是惯用的分片办法。
files集合
不会分片,所有的文件记录都会位于一个分片上,推荐使该分片保持高度灵活(至少使用由3个节点构成的replica set)。chunks集合
应该被分片,并且用索引files_id:1
。已经存在的由MongoDB
的驱动来创建的files_id,n
索引不能用作分片Key
(这个是一个分片约束,后续会被修复),所以不得不创建一个独立的files_id
索引。使用files_id
作为分片Key
的原因是一个特定的文件的所有Chunks
都是在相同的分片上,非常安全并且允许运行filemd5
命令(要求特定的驱动)。
> db.fs.chunks.ensureIndex({files_id: 1});
> db.runCommand({ shardcollection : "test.fs.chunks", key : { files_id : 1 }})
{ "collectionsharded" : "test.fs.chunks", "ok" : 1 }
由于默认的files_id
是一个ObjectId
,files_id
将会升序增长。
因此,GridFS
的全部Chunks
都会被从一个单点分片上存取。如果写的负载比较高,就需要使用其他的分片Key
了,或者使用其它的值来作为分片Key
。
选择分片Key
需要考虑的因素具有一定的对立性,不可能样样具备,在实际使用过程中还是需要根据需求的不同来进行权衡,适当放弃一些。
感谢支持
更多内容,请移步《超级个体》。