为一个集合(collection)选择合适的shard key非常重要。如果这个集合非常庞大,那么将来再来修改shard key将会很困难。如有任何疑问请到论坛或者IRC寻求帮助。
示例文档
- {
- server : "ny153.example.com" ,
- application : "apache" ,
- time : "2011-01-02T21:21:56.249Z" ,
- level : "ERROR" ,
- msg : "something is broken"
- }
基数(cardinality)
一个集合中的所有数据会被分裂为多个数据块(chunk),一个数据块包含了某个范围shard key的数据。请选择合适的shard key,否则你将会得到很大的不能分裂的数据块。
使用上面的日志例子,如果你选择了:
- {server:1}
作为shard key,那么所有关于某个server的数据会存在一个数据块中,你可以很容易想到一个server的数据会超过64MB(默认数据块大小)。如果shard key是:
- {server:1,time:1}
你可以将单个服务器的数据分裂到毫秒级。只要你的单个服务器不会有200MB/S,就不会有不能分裂的数据块。
保持数据块在一个合适大小是很重要的,这样数据就可以在集群中均衡分布并且移动一个数据块代价也不会太大。
水平写
使用分片的一个主要原因就是分发写操作。为了达到这个目的,写操作应当尽可能的分散到不同的数据块。
再次使用前面的例子,选择:
- { time : 1 }
作为shard key,会导致所有的写操作都集中到最新的数据块中。如果shard key选择:
- {server:1,application:1,time:1}
那么每个服务器:应用映射都会被写到不同的地方。如果这里有100种服务器:应用映射,和10台服务器,那么每台服务器将会分配约1/10的写操作。
需要注意的是,由于ObjectId中很重要的一部分是基于时间生成的,使用ObjectId作为shard key等同于直接使用时间值。
查询隔离
另外一个考虑就是任何一个查询需要分发到多少个shard。理想情况下,一个查询操作经mongos直接分发到拥有期望数据的mongod。如果你知道大部分的查询使用了那些条件,那么使用这些条件属性作为shard key可以提高很多效率。
即使查询条件中没有包含shard key,查询依然可以工作。由于mongos不知道哪个shard拥有期望的数据,mongos会将这个请求顺序分发到所有的shard中,这会增加响应时间和网络数据流量及服务器负载。
排序
如果查询中包含了排序请求,这个查询请求会同以前没有排序要求时一样分发到需要的shard中。每一个shard执行查询然后在本地做排序(如果有 索引的话会使用索引)。mongos会合并从shard返回的已经排序好的结果,然后返回合并后的数据给客户端。这样的话,mongos只需要做少量的工 作和很少的RAM。
可靠性
分片的一个重要方面就是如果整个shard不能访问了(即使使用了可靠的复制组),这将会对整个系统带来多大的影响。
例如你有一个类似于twitter的系统,评论记录类似于:
- {
- _id: ObjectId("4d084f78a4c8707815a601d7"),
- user_id : 42 ,
- time : "2011-01-02T21:21:56.249Z" ,
- comment : "I am happily using MongoDB",
- }
由于系统对写操作是非常敏感的,如果你希望将写操作分散到各个服务器中,你需要使用"_id"或者"user_id"作为shard key。"_id"可以给你较好的颗粒度和写扩散性。但是一旦某个shard宕机了,它将会影响到几乎所有的用户(有些数据丢失了)。如果你使 用"user_id"作为shard key,此时一小部分用户会受到影响(比如在5个shard组成的集群中,这个百分比是20%),即使这些用户再也看不到他们的任何数据了。
索引最佳化
正如前面章节关于索引的描述,通常经常对一部分索引做读/更新会带来较好的性能表现,这是因为这“活跃”的部分可以大多数时间都驻留在RAM。前面 介绍的shard key虽然可以将写操作分散到各个shard中,但是他们还是属于每个mongod的索引的。作为替代,将时间戳分解为某种形式并作为shard key的前缀可以带来一些好处,这样可以减小经常访问的索引大小。
例如你有一个图片存储系统,图片记录类似于:
- {
- _id: ObjectId("4d084f78a4c8707815a601d7"),
- user_id : 42 ,
- title: "sunset at the beach",
- upload_time : "2011-01-02T21:21:56.249Z" ,
- data: ...,
- }
你可以定制一个包含了上传时间的月份和唯一的标示符(如ObjectId,数据的md5值等)的_id来替代默认的_id,新的记录类似于:
- {
- _id: "2011-01_4d084f78a4c8707815a601d7",
- user_id : 42 ,
- title: "sunset at the beach",
- upload_time : "2011-01-02T21:21:56.249Z" ,
- data: ...,
- }
使用它作为shard key,同时也是访问一个文档使用的_id.它可以很好的将写操作分散到所有shard中。并且它减小了大部分查询需要访问的索引的大小。
进一步注释:
- 在每个月的开始,只有一台shard被访问知道均衡器开始分裂数据块。为了避免这种潜在的低性能和数据迁移,建议在时间前面增加了一个范围值(比如5或者更大的范围值如果你有5台服务器)。
- 更进一步的改善,你可以将用户名(user id)纳入到图片id中,这样可以保持同一个用户的文档都存储到同一个shard中,例如:"2011-01_42_4d084f78a4c8707815a601d7"
GirdFS
根据不同的需要,这里有多种不同的方法可以对GridFS进行分片。一种基于已经存在的索引的分片方法是:
- "files"集合不做分片。所有的文件记录都将存储在一个shard中,强烈建议保证该shard非常可靠(使用至少3节点的复制组)。
- "chunks" 集合应当使用索引"files_id:1"进行分片。由驱动创建的已经存在的"files_id,n"索引不能被使用在"files_id"上面进行分片 (这是一个限制,将来会被fix)。所以你需要单独为"files_id"创建一个索引。使用"files_id"进行分片开始保证某个文件存储到同一个 shard中,这样"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数据块会被送往同一个shard。如果你的写负载太大以至于单台服务器无法应付,你可能需要考虑使用其他的shard key或者使用其他的值作为file的_id.