1、选择片键
选择一个好的片键非常关键,如果选择了一个糟糕的片键,它可以立马或者在访问量变大时毁了你的应用程序,也有可能潜伏着,等待着,没准什么时候突然毁了你的应用程序。
另外一方面,如果你选择了一个好片键,只要应用程序还在正常运行,而且只要发现访问量提高就赶紧添加服务器,MongoDB就会确保一直正确地运行下去。
正如在前面所学的,片键决定了数据在集群中的分布情况,因此你会希望存在这样一个片键,它既能把读写分散开来,又能把正在使用的数据保持在一起,这些看似互相矛盾的目标在现实中却往往是可以实现的。
我们先挑片键的几个反面例子找茬,然后再拿几个较好的例子来琢磨一番,MongoDB的wiki上也有一页与选择片键相关且很不错的内容,可以看看。
1.1、小基数片键
一些人并不真正理解或者信任MongoDB自动分配数据的方式,所以他们总是沿着这么个思路来向:我有4个分片,所以应该用一个由4个可能的字段来做片键,这是一个非常糟糕的想法,为什么呢?
假设我们有一个存储用户信息的应用程序,每个文档有一个continent字段代表用户所在地区,其字段值可以是"Africa"、"Antarctica"、"Asia"、"Australia"、"Europe"、"North Ameraica"或者"south America",考虑到我们在每个大洲都有一个数据中心--或许不包括南极洲,并且想从人们所在当地的数据中心为其提供用户数据,我们决定按该字段进行分片。
集合开始于某个数据中心的一个分片的初始化块(负无穷,正无穷),所有的插入和读书都落在这一个块上,一旦它变的足够大,就会被划分成两个块(区间分别为(负无穷,“Europe”)和["Europe",正无穷)),这样一来,所有来自非洲(Africa)、南极洲(Antarctica)、亚洲(Asia)和澳洲(Australia)的文档都会被分到第一个块上,而所有来自欧洲(Europe)、北美(North America)或者南美(South America)的数据都会被分到第二块上,随着更多文档被添加进来,集合最终会变成7个块,如下所示:
每个大洋洲一个分片
然后呢?
MongoDB不能再进一步分割这些块了,块只能变得越来越大,虽然暂时不会出问题,但是当服务器磁盘空间开始主键消耗尽时问题就会付出睡眠了,除了买块更大的磁盘,你什么也做不了。
由于片键值数据量有限,因此这种片键成为小基数片键(low-cardinality shard key)。如果选择了一个基数很小的片键,到头来肯定会得到一堆即巨大又无法移动,还不能分割的块,它们会让你极为不块,这样的生活你懂的。
如果这么做是为了手动分配数据,那就不要再用MongoDB内置的分片机制了,否则它会和你一直抗争到底,当然不管则样你还是可以手动对集合进行分片,编写自己的路由器,并将读写路由到任何一台服务器上,只不过选择一个号的片键并让MongoDB来替你做这一切更加容易一些。
- 适用的键
这个规则适用于任何取值个数有限的键,一定要记得,如果在某集合中一个键有N个值,那就只能有N个数据块,因此也只能有N个分片。
如果打算采用小基数片键的原因是需要在那个字段上进行大量查询,请使用组合片键(一个片键包含两个字段),并确保第二个字段有非常多不同的值可以供MongoDB用来进行分割。
- 例外
如果一个集合有生命周期(比如,每周都创建一个新的集合而且你知道,在一周时间里数据量不会接近任何分片的最大容量),就可以选择这个生命周期为片键。
- 数据中心感知
这个例子不仅仅是关于选择小基数片键的,还与在MongoDB分片机制中添加数据中心感知(data-center awareness)支持的尝试有关,到目前为止,分片上不支持数据中心感知,如果对此感兴趣可以查看或者相关的问题投票。
靠使用者自己实现的问题在于其扩展性并不是很好,如果你的应用最近在日本很流行怎么办?你可能会想添加第二个分片来应付亚洲地区的访问量。
但是你打算如何迁移数据呢?一个数据块增长到好几GB大,你就没法迁移它了,而且也不能再分割块,因为整个块就只有一个片键值,由于片键值无法更新,因此也不可能通过更新所有文档来使用一个更独特的片键值,可以删除每个文档,更新片键值,然后再把它重新保存回去,但是对于大型数据库而言那并不是一个能迅速完成的操作。
你所能做的最好的事情就是在插入文档时开始用Asia,Japan替代简单的Asia,这样一来会有一批旧文档,其片键值应该Asia,Japan但却是Asia,因此应用程序逻辑就不得不同时支持两种情况,另外一旦开始拥有更细粒度的块,就不能保证MongoDB还会把它们放在你所期望的地方(除非关闭平衡器并手动处理所有事情)。
数据中心感知对于大型应用非常重要,而且它对MongoDB的开发者有很高的优先级,在这期间选择一个小技术片键绝不对是一个好的解决方案。
1.2、升序片键
从RAM中读取数据要比从磁盘中读取快,所以目标是尽可能多地访问内存中的数据,因此,如果有些数据总是被一起访问,我 们就希望片键能够把它们保持到遗弃,对大部分应用程序而言,新数据被访问的次数总比老数据多,所以人们往往会尝试使用诸如时间戳或者ObjectId一类的字段来做片键,但是这并不像它们所期望的那样可行。
比如说我们有一个类似微博的服务,其中每个文档都包含一条短信息、发送人以及发送时间,我们按发送时间字段来分片,取值为自公元元年起经过的秒数。
和往常一样,还是从一个数据库(负无穷、正无穷)开始,全部插入都会落在这个分片上至到它分裂成两个块,由于是从片键中点把块分开来的,所以在我们分隔块的那一刻,时间戳很可能已经远大于中间值了,这意味着再往后所有的插入都会落到第二个块上,不会再有插入操作命中第一块,一旦第二个块填满了,它就会分裂成为另外两个块,如[1294516901,1294930163)和[1294930163,正无穷)两个块,但是因为从现在起时间都在1294930163之后,所有新的插入都会被添加到区间为[1294930163,正无穷)的块上,这个模式会持续下去,所有的数据总是被添加到最后,一个数据块上,即所有数据都会被添加到一个分片上,这个片键创造了一个单一且不可分散的热点。
- 适用的键
这条规则适用于任何升序排列的键值,而并不必须是时间戳,其他例子包括ObjectId、日期、自增主键,只要键值趋向于无穷大,你就会面临这个问题。
- 例外
基本上,这种片键总是一个坏主意,因为它导致热点必须存在,如果访问量不大且用一个分片就能承受所有读写,那还行的通,当然如果遇到一个访问量尖峰或者应用开始变得更受欢迎,那它会终止停止工作并且难以修复。
除非你非常清楚自己在干什么,否则不要使用升序片键,肯定还有更好的片键存在,应该避免使用这一个。
1.3、随机片键
有时为了避免热点,人们会选择一个取值随机的字段来分片,采用这种片键一开始还不错,但是随着数据量越变越大,它会变得越来越慢。
假设我们在分片集合中存储照片的缩略图,每个文档都包含了照片的二进制数据、二进制数据的MD5散列图值,以及一段描述、拍照时间和拍照人,我们决定在MD5散列值上做分片。
随着集合的增长,我们最终会得到一组均匀分布各分片的数据块,目前为止一切顺利,现在,假设我们非常忙而分片2上的而一个块填满并分裂了,配置服务器注意到分片2比分片1多出了10个块并判定应该抹平分片间的差距,这样MongoDB就需要随机加载5个块得数据到内存中并将其发送给分片1,考虑到数据序列的随机性。一般情况下这些数据可能不会出现在内存中,所以此时的MongoDB会给RAM带来更大的压力,而且还会引发大量磁盘IP。
除此之外,片键上必须有索引,因此如果选择了从不依据它进行的随机键,基本上可以说是浪费了一个索引,另外考虑到每增加一个索引都会让写操作变得更慢,所以保持索引数量尽可能低也是非常重要的。
1.4、好片键
我们真正需要的是一种将访问模式也考虑进去的方案,如果应用会规律地访问25GB的数据,我们就希望所有的分割和歉意都发生在这25GB数据上,而不是随机访问数据以至于不断地有新数据呗从磁盘中复制到内存里。
因此我们希望能找到这样一个片键,它具备有良好的数据局部性特征,但有不会因为太局部而导致热点出现。
- 准升序键加搜索键
许多应用访问新数据比老数据更频繁,所以我们希望数据大致上按照时间排序,但是同时也要均匀分布,这样一来既能把我们正在读写的数据保持在内存汇总,又可以使负载均衡地分散在集群中。
举个例子,比如说有个分析程序,用户会定期通过它访问过去一个月的数据,而我 们希望能尽可能保持数据易于使用,因此可以在{month:1,user:1}上分片,其中month是一个粗粒度的升序字段,即每个月它都会有一个更新更大的值,user适合作为第二个字段,因为我们会经常查询某个特定用户的数据。
- 常见问题
search字段可不可以也是升序字段?
不可以,如果是,则该片键会降级成一个升序片键,进而使你同堂面临普通升序键带来的热点问题。
search字段应该是什么?
search字段最好是应用程序可以用于查询的东西,比如用户信息(比如上面的例子)、文件名字段或者GUID等,它应该具备非升序、分布随机且基数适当的特点。