MongoDB分片中片键的选择
当MongoDB整个架构已经部署好以后,真正考验架构者能力的时候就到了:该如何选择片键。
如果选择了一个不恰当的片键,他可能会在访问量变大的时候,使你的整个应用系统崩溃,同样好的片键可以构成一个良性的生态系统,根据需要增删服务器,MongoDB会确保系统一直正确的运行下去。
咱们先看看几种不恰当的片键
1,小基数片键
假设我们有一个存储用户信息的应用程序,每个文档有一个continent的字段,存储用户所在地区,其值有:africa,antarctica,asia,australia,europe,north america,south america。考虑到我们在每个州都有一个数据中心,并且想从人们所在地的数据中心为其提供数据,我们决定按该字段分片。
集合开始于某个数据中心的一个分片的初始块(-∞,∞),所有的插入和读取都落在这一块上,一旦他变得足够大,就会被分成两个块(-∞,europe)和[europe,∞),这样一来,所有来自africa,antarctica,asia和australia的文档都会被分到第一块,其他的都会被分到第二块,随着更多的文档被添加到数据库,集合最终会变成7个块,如下
(-∞,antarctica)
[anrarctica,asia)
[asia,australia)
[australia,europe)
[europe,north america)
[north america,south america)
[south america,∞)
然后呢?
MongoDB不能再进一步分割这些块了,快会越来越大,虽然暂时不会出问题,但是当服务器硬盘空间被用完的时候,你就没办法了,只能购买新的硬盘,悲剧的是硬盘是有极限的。
由于片键数量有限,因此这种片键称为小基数片键,如果选择了一个基数很小的片键,到头来肯定会得到一堆巨大无法移动也不能分割的块,这时候做维护做扩展是什么感觉,你懂得。
如果是因为需要在某个字段上做大量查询而采用小基数片键,那就需要使用组合片键了,一个片键包含两个字段,并确保第二个字段有非常多不同的值供MongoDB用来进行分割,比如大部分的查询都和时间关联,可以用时间字段做第二个字段,又可以减轻负载。
2,升序片键
从RAM中读取数据要比磁盘取快,所以目标是尽可能多的访问内存中的数据。一次,如果有些数据总是被一起访问,我们就希望能够把他们保持在一起。对大部分应用来说,新数据被访问的次数总比老的多,所以往往会使用如时间戳或者objectId一类的字段来分片,但是这并不想我们期望的那样可行。
比如我们有一个类似微博的服务,其中每个文档都包含一条消息,发送人和发送时间,我们按时间来分片,让我们看看MongoDB会如何运行
首先还是一个大块(-∞,∞),文档会全部插入到这个分片上,然后开始分裂,比如(-∞,1294516901),[1294516901,-∞)----(使用的是时间戳),由于是从片键中点把块分开,所以在分割开快的那一刻开始,说有数据都会插入到第二个块上,不会在插入带第一个块上,一旦块二被插满,他会在分割成两块,但同样的只会在最后一块插入数据,这种情况会一直持续下去,这就造成了一个单一且不可分散的热点。在网站高峰期可见这个点上的压力。
3,随机片键
有时为了避免热点,会采用一个取值随机的字段来做分片,采用这种片键一开始还不错,但是随着数据量越来越大,他会越来越慢。
比如我们在分片集合中存储照片缩略图,每个文档包含了照片的二进制数据,二进制数据的md5散列值,以及描述等字段,我们决定在md5散列值上做分片。
随着集合的增长,我们最终会得到一组均匀分布于各分片的数据块。目前一起正常。现在假设我们非常忙而分片2上的一个块填满并分裂了,配置服务器注意到分片2比分片1多出了10个块并判定应该抹平分片间的差距,这样MongoDB就需要随机加载5个块的数据到内存中并发给片1,考虑到数据序列的随机性,一般情况下这些数据可能不会出现在内存中,所以此时的MongoDB会给RAM带来更大的压力,而且还会引发大量的磁盘IO。
另外,片键上必须有索引,因此如果选择了从不依据索引查询的随机键,基本上可以说浪费了一个索引,另一方面索引的增加会降低写操作的速度,所以降低索引量也是非常必要的。
那么怎么样的片键才是好的片键呢?
从上面的分析可得出一个好的片键应该具备良好的数据局部性,但又不会因为太局部而导致热点出现。
1,准升序键加搜索键
许多应用程序都是访问新数据更频繁,所以我们希望数据大致按时间排序,但是同时也要均匀分布,这样一来既能把我们正在读写的数据保持在内存中,又可以均衡的分散在集群中。
我们可以通过像{coarselyAscending:1,search:1}这样的组合片键来实现,其中coarselyAscending的每个值最好能对应几十到几百个数据块,而search则应当是应用程序通常都会一句其进行查询的字段。
举个例子:有个分析程序,用户定期通过他访问过去一个月的数据,而我们希望能尽量保持数据易于使用,因此可以使用{month:1,user:1}来做分片,现在来说说运行过程
首先一个大数据块((-∞,-∞),(∞,∞)),当他被填满,MongoDB将自动分割成两块,比如:
((-∞,-∞),("2012-07","susan"))
[("2012-07","susan"),(∞,∞))
假设现在还是7月,则所有写操作会被均匀的分布到两个块上。所有用户名小于susan的数据被写入块1中,所有大于susan的数据被写入块2,然后整个生态系统就良性运行了,等到8月,MongoDB又开始创建2012-08的块,分布还是均衡的(这里不是时时均衡,肯定有个抹平的过程),等到9月,7月的数据无人访问就开始退出内存,不再占用资源。
--------------------------------------------------------------------------------------------------------------------------------------
当然场景不同,应用就不同,上面也只是基本的东西,具体选择片键还应该根据自己的程序来做,选键的时候多考虑以下问题。
1,写操作是怎么样的,有多大?
2,系统没小时会写多少数据,每天呢,高峰期呢
3,那些字段是随机的,那些是增长的
4,读操作是怎么样的,用户在访问那些数据
5,数据索引做了吗?应不应该索引呢?
6,数据总量有多少
总的来说,在进行分片前,你需要清楚的了解你的数据。
以上内容参考了“深入学习MongoDB” 一书,如果需了解更多MongoDB优化的知识,这本数绝对值得一读,这本书还列出了50例MongoDB优化,维护等等需要注意的地方