天天看点

MongoDB 4.0自动分片+手工分片+片键选择逻辑

MongoDB对于数据的切分是根据shardKey来实现的,shardKey可以有一个也可以有多个,作用类似于复合主键。当我们在使用shard key切分时,默认是Mongos自动分片,自动设定切分规则。但是我们也可以手动指定分片规则。在后面的一篇博客《MongoDB4.0 Sharded Cluster+Replica Set集群搭建》详细介绍了MongoDB集群的搭建方法,下面详细介绍一下数据库分片方法和片键选择规则。

当我们将分片添加到mongos之后,就可以开始指定分片规则,这里以新建一个库为例。

下面的操作使用的MongoDB版本为4.0.6

首先我们新建一个数据库,名字为alextest

sh.enableSharding("alextest")
           

一个库里面可以有多个collection,可以分别指定这个collection是否进行分片,如果不分片,则这个collection会默认放在整个集群的第一个分片上。下面我们手动建立一个collection名为test并进行自动分片。

自动分片

自动分片也有多种选择,Hash分片和直接根据数值进行分片。

说道这里就要介绍一下MongoDB分片数据库的存储机制。首先collection里所有的document在分片后的collection中都会存储在一个叫chunk的东西里,chunk的大小默认为64M,可以手动修改,但是64M已经是一个比较合理的数值了。在一般的情况下,每个分片中chunk的数量应该大致相同,这样能够保持每个分片的存储数量比较均匀。举一个简单的例子来说,如果我有一个collection存储的是学校里学生的基本信息,每一个学生有一个studentId,班级ID classId。假设现在有10000名学生,我们直接以classid作为shard key。classID的范围假设为001-100

那么此时我在插入的过程中会发现这样一个现象:最开始所有的学生都被插入到了一个分片中,然后当该分片存储的数量超过一定大小之后,会发现MongoDB自动的把001-020放到一个分片里,把021-100放到另一个分片里。再继续往后插会发现MongoDB把001-010放到一个分片里,011-020放到另一个分片里,021-100放到第三个分片里。

也就是说,mongoDB在一个chunk插入达到6以上4Mt的时候,会自动的把这个chunk分成两个,然后会检查各个分片的chunk数是不是平衡,然后把chunk较多的分片里的几个chunk迁移到chunk较少的分片中去,这个迁移是由Sharded Cluster Balancer组件完成的,当然你也可以禁止自动迁移转为手动。在这个过程中会出现每个分片的metadata的document总数超过总插入数的情况,因为在迁移过程中,原分片保留这个chunk而另一个分片正在导入这个chunk。

sh.shardCollection(
  "alextest.test",
  { classID: 1 }
)
           

在上面这种shard key设置中,容易出现一个明显的问题,就是有的班级人数特别多,有的特别少,这样就会造成各分片数据量不均衡的问题。因为我们上面只指定了一个片键classID,所以mongoDB分割chunk只能根据classID的不同才能进行。所以很容易导致不平衡问题的出现。

比如极端情况下,1班有9901名学生,其他99个班级各只有一名学生,mongoDB自动把1班放到分片1中去,那么放一班的这个chunk就会远远超过64M而且不能再继续分割,因为如果分割了chunk并移动到别的分片,MongoDB就找不到1班的学生在哪里了,因为片键的设置实际上就是一个索引,一个字典。mongos进程通过mongo conf节点存储的这个索引信息,来确定该去哪一个分片找这个信息,从而避免全表全库全分片扫描,从而提高分片的效率。

由此可见,片键的设置对于提高查询速度,写入速度,存储均衡有着至关重要的作用,应该格外慎重的选择片键。

在单一片键的情况下,如果我们以classID作为片键,那么我们只查询某一个班下面的学生就会特别快速,因为MongoDB只需扫描一个分片就可以返回结果,sort起来会很方便。如果我们查询条件不是以classID为主,而是以姓名为主的话,那这个片键就会失去作用,Mongos会在所有的分片执行扫描,然后把每个分片的结果汇总至mongos,mongos再进行二次汇总,如果有sort这样的操作就会极大的损失效率。

第二点,如果有大量数据插入的场景,比如上学的时候进行打卡,因为学生到校的班级分布式非常随机的,所以以classID作为片键进行插入,mongos会把写操作均匀的路由到所有的分片上,让多个分片共同均摊写入操作,不容易导致单个分片数据库的过载。但是如果我们以时间作为片键(比如使用_id,因为_id是基于时间生成的,和直接使用打卡时间作用一样),那么某一个时间段写入操作肯定会集中在一个分片上,那么人多的时候就会出现反应慢的问题。而且多个分片在同一时刻只有一个在工作,非常的不科学。

第三点,如果有需要全表扫描的情况且无法避免的情况下,多个分片同时扫描肯定比单一分片扫描要快,但是如果结果集特别庞大,就会加重mongos二次汇总所需的时间。所以在这种情况下,要尽量减少sort这种操作,减轻二次汇总的开销。或者通过其他条件尽量减少各分片提交到mongos的结果集大小。

所以综合以上,没有一个绝对正确的分片规则,要根据你自己的实际业务需要选择合适的片键。但是一般来说片键的选择要选离散程度比较大的,或者作为最常用主键的属性。但是如果经常进行全表扫描或者高并发插入这种情景下,片键要选择随机性比较大的,把工作压力分摊到各个节点,反之,如果查询插入量较小,则应该把结果集中所有的数据尽量放置到一个分片中去,能减少多个分片同步和汇总的时间。

下面再回到数据分布不均匀的问题,mongoDB给出了很有效的解决方法,就是使用复合片键,当出现上面那个一个班的学生数量特别多的时候,可以给mongoDB增加一个片键,让它更细的去区分每一波学生,比如

sh.shardCollection(
  "alextest.test",
  { classID: 1,userID: 1 }
)
           

这样,当classID=1的分片满了之后,mongodb还会继续根据userID进行切分,如下

classID=1 userID=1-1000的放在一个chunk中,置于分片1

classID=1 userID=1000-2000的放在一个chunk中,置于分片2

classID=1 userID=2000-9901的放在一个chunk中,置于分片3

classID=2-100 userID=9902-无限 放在一个chunk中,置于分片2

上面只是一个粗略的例子,实际上mongoDB会根据chunk的实际大小自动的进行切分,shard间移动,最后让每个分片存储的数据量大致相同,几乎不需要人工操心。

具体的分布信息可以在mongos中,使用root用户登录,然后用下面三种方式的一种来查看具体的chunk片键范围和分布地点

sh.status({"verbose":1})
sh.status("verbose")
sh.status()
           

由于一个chunk就会有一条记录,太多chunk的话就省略显示,所以要使用“verbose” 

hash分片

上面是以某个属性的具体值作为分片依据,被分片的属性可以为数字,字符串,布尔值等等。但是MongoDB还提供了一种Hash算法,能让数据更加均匀。比如字符串"abcdefg"和字符串"abcedf"在数值上可能非常接近,MongoDB一开始会把他们放到一个chunk下,但是如果使用hash,那么数据的差异就很大了,mongo就会在一开始就分到两个分片上去,能缓解插入的压力。但是hash计算本身也要耗费一定的时间,且查询的时候数据过于离散导致需要在多个分片中汇总结果,会导致查询效率的下降,要根据业务具体情况来选择。

sh.shardCollection(
  "alextest.test",
  { classID: "hashed"}
)
           

手工分片

在某些情况下,我们的数据非常有规律且易于管理,我们想要手动进行分片,这样也是可以的,我们可以手工指定什么样的数据落在哪一个分片中,比如我让2019年入学的学生放到分片一中,2020的分片2,2021的分片3,虽然这样做不一定有实际意义,但是某些极为特殊的场合中还是有其意义。

这里我们就要建立一个叫ZoneKeyRange的东西,也就是区域,我们可以指定哪些属性落在哪些区域中,如下我建立了两个区域

sh.addShardToZone("shard1", "DC1")
sh.addShardToZone("shard2", "DC2")

sh.updateZoneKeyRange(
   "alextest.test",
   { sid: 1 },
   { sid: 100 },
   "DC1"
)

sh.updateZoneKeyRange(
   "alextest.test",
   { sid: 100 },
   { sid: 200 },
   "DC2"
)

sh.shardCollection("alextest.test",  { sid: 1 } )
           

DC1是一个区域,存储sid=[1-100), 范围前开后闭,放到分片一里,DC2是另一个区域,存储sid=[100-200),放在分片2里,

当然也可以把DC1和DC2都放在分片一里,随你自己设定。然后mongoDB会自动补充你没有设置好的范围,比如-无穷到1,200到正无穷这两个DC然后随机放到某一个分片下。由于自动分配的就比较随机了,所以在一开始就尽量把你的数据范围覆盖完整。

自动分片+手工预切割+手工迁移chunk

在自动分片的部分中,上面写道MongoDB在插入的过程中,发现一个chunk超过了64M之后会自动的按照shard key进行切割,然后分割成两个chunk,然后在多个分片中迁移这个chunk。插入操作作为这个过程的触发其实有一些弊端,因为切割和迁移也需要很多计算资源和网络资源,如果插入量特别大,那么切割和迁移本身也会浪费一些资源。一个办法是我们先根据shard key提前将chunk切割好,此时Sharded Cluster Balancer会将我们切割好的chunk自动分配给分片一或者分片二,如果我们觉得它分配的不合理,还可以停掉Sharded Cluster Balancer然后来手工规定chunk的位置。但这一切的前提是你需要对你的数据分布特别熟悉,否则很容易搞出数据不均衡来。

比如我这有很多交易记录信息,我根据交易的日期来进行预切割,那么可以这样执行

sh.splitAt( 
    "alexTest.test", 
    { transTime: "20190101") } 
)
sh.splitAt( 
    "alexTest.test", 
    { transTime: "20190201") } 
)
sh.splitAt( 
    "alexTest.test", 
    { transTime: "20190301") } 
)
           

这样自动就会按照交易日期20190101,20190201,20190301进行预切割,并放到各个分片中,但是分片的分配是比较随机的,有可能三个月都放到一个分片中(因为切割的数量比较少),或者两个chunk在一个分片里,一个在第二个分片里,或者三个chunk分别属于不同的分片。如果在实际插入的过程中,发现一月份的数据太多了超过了一个chunk,那么mongo会自动的把一月再分成两个chunk,并放到不同的分片中,于是就变成了:20190101,20190115,20190201,20190301这样四个chunk,所以手工预分割好处还是挺大的,兼顾了灵活和自动。

正常情况下,Sharded Cluster Balancer会根据各个分片所包含的chunk数自动在多个分片中迁移,但是你如果实在觉得不满意,可以在预分割之后停掉Sharded Cluster Balancer并自己指定每个chunk应该放在什么位置。可以参考https://docs.mongodb.com/manual/tutorial/manage-sharded-cluster-balancer/index.html

来手工迁移,但是不特别推荐这种方式。

由于mongoDB的命令行使用JavaScript(ES6)的语法,所以我写了一个预分割的脚本,可以方便的按照月份进行预切割

function  getLocaleDateTime(date){
    var getyear = date.getFullYear();//返回一个表示年份的 4 位数字。
    var getmonth = date.getMonth()+1;//返回表示月份的数字,返回值是 0(一月) 到 11(十二月) 之间的一个整数。
    var getday = date.getDate();//返回月份的某一天,返回值是 1 ~ 31 之间的一个整数。
    var gethour = date.getHours();//返回值是 0 (午夜) 到 23 (晚上 11 点)之间的一个整数。
    var getminute = date.getMinutes();//返回值是 0 ~ 59 之间的一个整数。
    var getsecond = date.getSeconds();//返回值是 0 ~ 59 之间的一个整数。
    //不过返回值不总是两位的,如果返回的值小于 10,则仅返回一位数字。故进行加“0”操作。
    var getMonth = changeTime(getmonth);
    var getDay =  changeTime(getday);
    var getMinute =  changeTime(getminute);
    var getSecond =  changeTime(getsecond);
    var gethours;
    if(gethour>12){
        gethours = "下午" + (gethour-12).toString();//changeTime(gethour-12)
    } else {
        gethours = "上午" + gethour.toString();//changeTime(gethour)
    }
    
	return getyear + "" + getMonth + "" + getDay;
    
}
function  changeTime(time) {
   if(time<10){
        time = "0" + time;
   }
      return  time ;
}
for(var i=0;i<24;i++){sh.splitAt( "alexTest.test", { transTime: getLocaleDateTime(new Date(2018,i,1)) } )}
           

最后再放出一个我本地一个库里,使用sh.status()按照时间自动切割chunk的效果,总共有300多个chunk,每个分片100多个,数据比较均匀

{  "_id" : "alextest",  "primary" : "shard1",  "partitioned" : true,  "version" : {  "uuid" : UUID("42b8f136-9ece-4f43-9ed9-baf5d249f879"),  "lastMod" : 1 } }
                alextest.test
                        shard key: { "transTime" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                shard1	130
                                shard2	132
                                shard3	131
                        { "transTime" : { "$minKey" : 1 } } -->> { "transTime" : "20180327000000" } on : shard2 Timestamp(154, 1) 
                        { "transTime" : "20180327000000" } -->> { "transTime" : "20180329090713" } on : shard1 Timestamp(156, 1) 
                        { "transTime" : "20180329090713" } -->> { "transTime" : "20180330120905" } on : shard1 Timestamp(138, 0) 
                        { "transTime" : "20180330120905" } -->> { "transTime" : "20180331133109" } on : shard1 Timestamp(145, 0) 
                        { "transTime" : "20180331133109" } -->> { "transTime" : "20180401173100" } on : shard2 Timestamp(149, 1) 
                        { "transTime" : "20180401173100" } -->> { "transTime" : "20180402181657" } on : shard1 Timestamp(152, 0) 
                        { "transTime" : "20180402181657" } -->> { "transTime" : "20180403180321" } on : shard1 Timestamp(136, 1) 
                        { "transTime" : "20180403180321" } -->> { "transTime" : "20180404173426" } on : shard1 Timestamp(73, 0) 
                        { "transTime" : "20180404173426" } -->> { "transTime" : "20180406125334" } on : shard1 Timestamp(74, 0) 
                        { "transTime" : "20180406125334" } -->> { "transTime" : "20180407214515" } on : shard1 Timestamp(79, 0) 
                        { "transTime" : "20180407214515" } -->> { "transTime" : "20180409091015" } on : shard1 Timestamp(78, 0) 
                        { "transTime" : "20180409091015" } -->> { "transTime" : "20180410095331" } on : shard1 Timestamp(81, 0) 
                        { "transTime" : "20180410095331" } -->> { "transTime" : "20180411103532" } on : shard1 Timestamp(82, 0) 
                        { "transTime" : "20180411103532" } -->> { "transTime" : "20180412115041" } on : shard1 Timestamp(87, 0) 
                        { "transTime" : "20180412115041" } -->> { "transTime" : "20180413135641" } on : shard1 Timestamp(88, 0) 
                        { "transTime" : "20180413135641" } -->> { "transTime" : "20180414162449" } on : shard1 Timestamp(90, 0) 
                        { "transTime" : "20180414162449" } -->> { "transTime" : "20180416144115" } on : shard1 Timestamp(92, 0) 
                        { "transTime" : "20180416144115" } -->> { "transTime" : "20180417110231" } on : shard1 Timestamp(93, 0) 
                        { "transTime" : "20180417110231" } -->> { "transTime" : "20180418115801" } on : shard1 Timestamp(96, 0) 
                        { "transTime" : "20180418115801" } -->> { "transTime" : "20180419123106" } on : shard1 Timestamp(97, 0) 
                        { "transTime" : "20180419123106" } -->> { "transTime" : "20180420120536" } on : shard1 Timestamp(100, 0) 
                        { "transTime" : "20180420120536" } -->> { "transTime" : "20180421115113" } on : shard1 Timestamp(101, 0) 
                        { "transTime" : "20180421115113" } -->> { "transTime" : "20180422220620" } on : shard1 Timestamp(106, 0) 
                        { "transTime" : "20180422220620" } -->> { "transTime" : "20180424065618" } on : shard1 Timestamp(105, 0) 
                        { "transTime" : "20180424065618" } -->> { "transTime" : "20180425095836" } on : shard1 Timestamp(110, 0) 
                        { "transTime" : "20180425095836" } -->> { "transTime" : "20180426092908" } on : shard1 Timestamp(111, 0) 
                        { "transTime" : "20180426092908" } -->> { "transTime" : "20180427110910" } on : shard1 Timestamp(114, 0) 
                        { "transTime" : "20180427110910" } -->> { "transTime" : "20180428104304" } on : shard1 Timestamp(115, 0) 
                        { "transTime" : "20180428104304" } -->> { "transTime" : "20180429104331" } on : shard1 Timestamp(118, 0) 
                        { "transTime" : "20180429104331" } -->> { "transTime" : "20180430144942" } on : shard1 Timestamp(119, 0) 
                        { "transTime" : "20180430144942" } -->> { "transTime" : "20180501204213" } on : shard1 Timestamp(120, 0) 
                        { "transTime" : "20180501204213" } -->> { "transTime" : "20180502204104" } on : shard1 Timestamp(121, 0) 
                        { "transTime" : "20180502204104" } -->> { "transTime" : "20180503212440" } on : shard1 Timestamp(126, 0) 
                        { "transTime" : "20180503212440" } -->> { "transTime" : "20180504212438" } on : shard1 Timestamp(123, 0) 
                        { "transTime" : "20180504212438" } -->> { "transTime" : "20180506105352" } on : shard1 Timestamp(124, 0) 
                        { "transTime" : "20180506105352" } -->> { "transTime" : "20180507140851" } on : shard1 Timestamp(125, 0) 
                        { "transTime" : "20180507140851" } -->> { "transTime" : "20180508135731" } on : shard2 Timestamp(128, 0) 
                        { "transTime" : "20180508135731" } -->> { "transTime" : "20180509161120" } on : shard1 Timestamp(129, 0) 
                        { "transTime" : "20180509161120" } -->> { "transTime" : "20180510160025" } on : shard3 Timestamp(157, 1) 
                        { "transTime" : "20180510160025" } -->> { "transTime" : "20180511162409" } on : shard3 Timestamp(84, 3) 
                        { "transTime" : "20180511162409" } -->> { "transTime" : "20180512201649" } on : shard3 Timestamp(84, 8) 
                        { "transTime" : "20180512201649" } -->> { "transTime" : "20180514111958" } on : shard3 Timestamp(84, 9) 
                        { "transTime" : "20180514111958" } -->> { "transTime" : "20180515112244" } on : shard3 Timestamp(87, 2) 
                        { "transTime" : "20180515112244" } -->> { "transTime" : "20180516093438" } on : shard3 Timestamp(87, 3) 
                        { "transTime" : "20180516093438" } -->> { "transTime" : "20180517173810" } on : shard3 Timestamp(87, 4) 
                        { "transTime" : "20180517173810" } -->> { "transTime" : "20180518163123" } on : shard3 Timestamp(89, 5) 
                        { "transTime" : "20180518163123" } -->> { "transTime" : "20180519163826" } on : shard3 Timestamp(89, 6) 
                        { "transTime" : "20180519163826" } -->> { "transTime" : "20180520190104" } on : shard3 Timestamp(91, 2) 
                        { "transTime" : "20180520190104" } -->> { "transTime" : "20180521173644" } on : shard3 Timestamp(91, 3) 
                        { "transTime" : "20180521173644" } -->> { "transTime" : "20180522175426" } on : shard3 Timestamp(91, 8) 
                        { "transTime" : "20180522175426" } -->> { "transTime" : "20180523192203" } on : shard3 Timestamp(91, 9) 
                        { "transTime" : "20180523192203" } -->> { "transTime" : "20180524205714" } on : shard3 Timestamp(93, 5) 
                        { "transTime" : "20180524205714" } -->> { "transTime" : "20180525185239" } on : shard3 Timestamp(93, 6) 
                        { "transTime" : "20180525185239" } -->> { "transTime" : "20180527070643" } on : shard3 Timestamp(95, 2) 
                        { "transTime" : "20180527070643" } -->> { "transTime" : "20180528164238" } on : shard3 Timestamp(95, 3) 
                        { "transTime" : "20180528164238" } -->> { "transTime" : "20180529161316" } on : shard3 Timestamp(95, 8) 
                        { "transTime" : "20180529161316" } -->> { "transTime" : "20180530180126" } on : shard3 Timestamp(95, 9) 
                        { "transTime" : "20180530180126" } -->> { "transTime" : "20180531170439" } on : shard3 Timestamp(97, 5) 
                        { "transTime" : "20180531170439" } -->> { "transTime" : "20180601213926" } on : shard3 Timestamp(97, 6) 
                        { "transTime" : "20180601213926" } -->> { "transTime" : "20180603091142" } on : shard3 Timestamp(99, 2) 
                        { "transTime" : "20180603091142" } -->> { "transTime" : "20180604184014" } on : shard3 Timestamp(99, 3)