MongoDB——再论片键选择 (转)

转自:知乎

https://zhuanlan.zhihu.com/p/339785784

 

片键选择对于Sharded Cluster有非常重大的意义,但在实际接触的案例中往往很多人选择了错误的片键,导致集群性能低下。其实在官方文档中已经对Shard Key有非常详尽的介绍,所有的注意事项和选择原则都十分清楚。所以在阅读本文之前请仔细阅读官方文档中的Shard Keys

基本原则

基于文档中的内容,我们再来仔细理解一下片键选择的这些原则到底都代表了什么意义。

取值基数

即是片键字段的备选值。这里有2个必须了解的知识点,理解它们才能理解为什么取值基数这么重要:

  • chunk定义的是一个连续的片键值范围,文档中的片键字段取值在这个范围内时,文档就属于这个chunk;
  • 在不同shard间均衡时是以chunk为单位,而不是文档;

但是chunk其实只是一个虚拟的概念,它仅存在于元数据中,存放文档的shard并不知道chunk的存在。在一个集群中,文档的分布大致是这样的:

也许有人要问,而为什么要有chunk的概念?为什么不是以文档为单位进行均衡?那我们看一下如果以文档为单位进行均衡会带来什么后果:

  • 首先我们必须知道每个文档分布在哪个shard上(元数据)
  • 如果这样的分布情况放在config上,那么元数据数量将和文档数量一样多,代表着config的数据量将会跟shard达到同样的水平

这在实现上是不现实的,因为我们既然选择分片,大部分时候是因为数据容量或处理能力已经超出了单台机器所能承受的范围。所以如果元数据也达到了这样的数量,即代表元数据很有可能也必须分片,那么又会有元数据的元数据,以及元数据的元数据的元数据……现实当中元数据只是一个复制集而已,可见它的数据量要远小于分片中的数据量(最小可达1/250000),而造成这个结果的原因正是因为有chunk的存在。

举个容易理解的例子:

如果文档是学生,那么chunk就是班级,运动会的时候老师会以班级为单位指挥大家行动而不是指挥每一个人。同样,均衡的时候是以chunk为单位指挥每个chunk的文档到哪里去,而不是每个文档。

上面说了很多关于chunk存在的必要性的题外话,那么为什么取值基数对于chunk如此重要?取值基数直接决定了理论上最多能有多少chunk。而最多有多少chunk又会影响到什么?

再来看一个例子:

假设要存储一个学校的所有的师生情况,选择年龄为片键。人的年龄具有非常固定的范围,假设为[0,99]。可见当学校人数较多的时候,chunk可能被拆得非常细(请暂时忽略什么时候会拆chunk的问题,后文再详细描述),比如(0, 10], (11, 15], (16, 30] …。但是无论怎么拆,因为年龄是整数,最细的情况下也就是一个数字一个chunk,所以我们最多只可以拆出100个chunk。随着数据增长这100个chunk将会越来越大,并且每个chunk的读写压力往往是不均匀的,所以哪个shard存储了压力较大的chunk比较多,相应地这个shard的压力也会比较大。但是此时系统并不会有任何动作,因为对于系统而言,每个片上的chunk数量是均衡的。

综上所述,取值基数直接决定了一共有多少个chunk,从而间接影响到分片的数据量/压力分布。选择时应该尽可能选择基数较大(即可选值较多)的字段作为片键。

片键取值分布

前一条原则讲的是片键的可能取值范围,这一条原则讲的则是片键在可能取值范围内的分布情况。分布情况会对集群造成什么影响?前面已经提到MongoDB的均衡是以chunk为单位进行的,只要chunk数量均衡了,对系统而言就是均衡的。所以我们不难发现,即使chunk数量均衡,文档数量可能并不均衡。如图所示,shard 1上虽然有2个chunk但实际文档数量可能还不如shard 2上的一个chunk多。此时无论是文档数量还是空间大小都是不均衡的,而我们却无能为力。

仍然以上面的例子继续说明:

对于学校而言,人数最多的是7~20岁的学生,可见7,8,9……19,20这几个chunk会非常繁忙,数据量会非常大,导致拥有这些chunk的shard也会非常繁忙并且面临存储压力。如果不巧这些chunk中的大部分正好都分布在同一个片上,那么这个片可能就忙不过来了。

这就体现出了片键取值分布情况的重要性:片键取值分布直接影响到每个片的压力情况,应该尽可能选择取值分布均匀的字段做片键。

 

 

 

 

 

 

 

以下转自:

https://www.cnblogs.com/spnt/archive/2012/07/27/2611540.html 

 

 

 

当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,数据总量有多少

 总的来说,在进行分片前,你需要清楚的了解你的数据。

posted @ 2021-05-05 16:49  会飞的斧头  阅读(182)  评论(0编辑  收藏  举报