Mongo分片之选择片键

导航:

Mongo分片:

  1.Mongo分片介绍

  2.Mongo分片之配置分片

  3.Mongo分片之选择片键

  4.Mongo分片之分片管理

  使用分片时,最重要也是最困难的任务就是选择数据的分发方式。需要理解MongoDB的数据分发机制才能够做出明智的选择。本章旨在帮助大家更好地选择片键,内容包括: 

    • 如何在多个可用的片键中做出选择;
    • 不同使用场景中的片键选择;
    • 哪些键不能作为片键;
    • 自定义数据分发方式的可选策略;
    • 如何手动对数据分片。

  由于前几章已经讲述了分片的基本知识,所以本章假设大家对分片已有基本的了解。

 

1.检查使用情况

  对集合进行分片时,要选择一或两个字段用于拆分数据。这个键(或这些键)就叫做片键。一旦拥有多个分片,再修改片键几乎是不可能的事情,因此选择合适的片键(或者至少快速注意到可能存在的问题)是非常重要的。

  为了选择合适的片键,需了解自己的工作量以及片键是如何对应用程序的请求进行分发的。这个问题不太好描述,可以尝试一些小例子,或者是在备用数据集上做一些实验。本节含有大量图表和解释说明,但最好的方式还是在自己的数据集上试一试。

  对集合进行分片前,先回答以下问题。

    • 计划做多少个分片?拥有3个分片的集群比拥有1000个分片的集群更具有灵活性。随着集群变得越来越大,不应做那些需要査询所有分片的查询,因此几乎所有査询都须包含片键。
    • 分片是为了减少读写延迟吗?(延迟指某个操作花费的时间,如写操作花费20 毫秒,但我们需要将其缩减至10毫秒)。降低写延迟的方式通常是将请求发送到地理位置更近的服务器或者是更强大的机器上。
    • 分片是为了增加读写吞吐量吗?(吞吐量指集群在同一时间能够处理的请求数量:集群能够在20毫秒内处理1000次写请求,但我们需要其能够在20毫秒内处理 5000次写请求)。增加吞吐量通常需要提高并行性,并确保请求被均衡地分发到各集群成员上。
    • 分片是为了增加系统资源吗?(比如,每GB数据提供MongoDB更多的可用 RAM)。如果是这样,可能会希望尽量保持工作集较小。

  根据这些问题来对不同片键进行评估,并判断所选片键是否适用于自己的情况。这样做能够提供所需的目标査询吗?能够按所需方式提高系统吞吐量或者减少读写延迟吗?如需保持工作集的小巧,这样做可以达到要求吗?

 

2.数据分发

  拆分数据最常用的数据分发方式有三种:升序片键(ascending key)、随机分发的片键和基于位置(location-based)的片键。也有一些其他类型的键可供使用,但大部分都属于这三种类别。以下几节会分别介绍这三种方式。

 

2.1 升序片键

  升序片键通常有点类似于"date"字段或者是ObjectId,是一种会随着时间稳定增长的字段。自增长的主键是升序键的另一个例子,但它很少出现在MongoDB中,除非要从其他数据库中导入数据。

  假设我们依据升序键做分片,如使用ObjectId的集合中的"_id"键。如果基于"id"分片,那么集合就会依据不同的"_id"范围被拆分为多个块,如图15-1所示。这些块会分发在我们这个拥有分片的集群中(比如说3个分片),如图15-2所示。

  假设要创建一个新文档,它会位于哪个块呢?答案是范围为0bjectId("5112fae0b4a4b396ff9d0ee5")到 $maxKey的块。这个块叫做最大块(max chunk),因为该块包含有$maxKey。

  如果再插入一个文档,它也会出现在最大块中。事实上,接下来的每个新文档都会被插入到最大块中!毎一个插入文档的"_id"字段值都会比之前文档的"_id"字段值更接近正无穷(因为ObjectId—直在增长),所以这些文档都会插入到最大块中。

  这样会带来一些有趣的属性,通常都是些不良属性。首先,所有的写请求都会被路由到一个分片(本例中shard0002)中。该块是唯一一个不断增长和拆分的块,因为它是唯一一个能够接收到插入请求的块。随着新数据的不断插入,该最大块会不断拆分出新的小块,如图15-3所示。

  这种模式经常会导致MongoDB的数据均衡处理变得更为困难,因为所有的新块都是由同一分片创建的。因此,MongoDB必须不断将一些块移至其他分片,而不能像在一个比较均衡分发的系统中那样,只需纠正那些比较小的不均衡就好了。

 

2.2 随机分发的片键

  另一种方式是随机分发的片键。随机分发的键可以是用户名、邮件地址、UDID (Unique Device IDentifier,唯一设备标识符)、MD5散列值,或者是数据集中其他一些没有规律的健。

  假如片键是0和1之间的随机数,各分片上随机分发的块如图15-4所示。

  随着更多的数据被插入,数据的随机性意味着,新插入的数据会比较均衡地分发在不同的块中。可以试着插入10 000个文档,来验证一下会发生什么:

> var servers = {}
> var findShard = function (id) {
...     var explain = db.random.find({_id:id}).explain();
...     for (var i in explain.shards) {
...         var server = explain.shards[i][0];
...         if (server.n == 1) {
...             if (server.server in servers) {
...                 servers[server.server]++;
...             } else {
...                 servers[server.server] = 1;
...             }
...         }
...     }
... }
> for (var i = 0; i < 10000; i++) {
...     var id = ObjectId();
...     db.random.insert({"_id" : id, "x" : Math.random()});
...     findShard(id);
... }
> servers
{
    "spock:30001" : 2942,
    "spock:30002" : 4332,
    "spock:30000" : 2726
}

  由于写入数据是随机分发的,各分片增长的速度应大致相同,这就减少了需要进行迁移的次数。

  使用随机分发片键的唯一弊端在于,MongoDB在随机访问超出RAM大小的数据时效率不高。然而,如果拥有足够多的RAM或者是并不介意系统性能的话,使用随机片键在集群上分配负载是非常好的。

 

2.3 基于位置的片键

  基于位置的片键可以是用户的ip、经纬度,或者是地址。位置片键不必与实际的物理位置字段相关:这里的“位置”比较抽象,数据会依据这个“位置”进行分组。无论如何,所有与该键值比较接近的文档都会被保存在同一范围的块中。这样可以比较方便地将数据与相应的用户,以及相关联的数据保存在一起。

  例如,假设我们有一个集合的文档是按照ip地址进行分片的。文档会依据ip地址被分成不同的块,并随机分布在集群中,如图15-5所示。

  如果希望特定范围的块出现在特定的分片中,可以为分片添加tag,然后为块指定相应的tag。在本例中,假如我们希望特定范围的IP段出现在特定的分片中,比如让“56.*.*.*”(美国邮政署的IP段)出现在shardOOOO,让“17.*.*.*”(苹果公司的IP段)出现在shardOOOO或shard0002上。我们并不关心其他的IP出现在什么位置。可通过为分片指定tag,请求均衡器实现该指令:

>  sh.addShardTag("shard0000", "USPS")
>  sh.addShardTag("shard0000", "Apple")
>  sh.addShardTag("shard0002", "Apple")

  然后,创建下列规则:

> sh.addTagRange("test.ips", {"ip" : "056.000.000.000"}, 
... {"ip" : "057.000.000.000"}, "USPS")

  这样就会将所有IP地址大于等56.0.0.0和小于57.0.0.0的文档分发到标签为 “USPS”的分片上。接下来,再为苹果公司的IP段添加一条规则:

> sh.addTagRange("test.ips", {"ip" : "017.000.000.000"}, 
... {"ip" : "018.000.000.000"}, "Apple")

  均衡器在移动块时,会试图将这些范围的块移动到这些分片上。注意,该过程不会立即生效。没有被打过标签的块仍会正常移动。均衡器会继续尝试将块均衡地分发在不同的分片上。

 

3.片键策略

  本节我们将学习针对不同类型应用程序的几种片键选项。

 

3.1 散列片键

  如果追求的是数据加载速度的极致,那么散列片键(Hashed Shard Key)是最佳选择。散列片键可使其他任何键随机分发,因此,如果打算在大量查询中使用升序键,但同时又希望写入数据随机分发的话,散列片键会是个非常好的选择。

  弊端是无法使用散列片键做指定目标的范围查询。如无需做范围查询,那么散列片键就非常合适。

  创建一个散列片键,首先要创建散列索引:

> db.users.ensureIndex({"username" : "hashed"})

  然后对集合分片:

> sh.shardCollection("app.users", {"username" : "hashed"})
{ "collectionsharded" : "app.users", "ok" : 1 }

  如果在一个不存在的集合上创建散列片键,shardCollection的行为会比较有趣:它假设我们希望对数据块进行均衡分发,所以会立即创建一些空的块,并将这些块分发在集群中。例如,在创建散列片键之前,集合如下:

> sh.status()
--- Sharding Status --- 
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
        {  "_id" : "shard0000",  "host" : "localhost:30000" }
        {  "_id" : "shard0001",  "host" : "localhost:30001" }
        {  "_id" : "shard0002",  "host" : "localhost:30002" }
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
        {  "_id" : "test",  "partitioned" : true,  "primary" : "shard0001" }

  shardCollection命令返回后,每个分片上立即出现了两个块,并均衡地分发在整个集群中:

> sh.status()
--- Sharding Status --- 
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
    {  "_id" : "shard0000",  "host" : "localhost:30000" }
    {  "_id" : "shard0001",  "host" : "localhost:30001" }
    {  "_id" : "shard0002",  "host" : "localhost:30002" }
  databases:
    {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
    {  "_id" : "test",  "partitioned" : true,  "primary" : "shard0001" }
        test.foo
            shard key: { "username" : "hashed" }
            chunks:
                shard0000       2
                shard0001       2
                shard0002       2
            { "username" : { "$MinKey" : true } } 
                -->> { "username" : NumberLong("-6148914691236517204") } 
                on : shard0000 { "t" : 3000, "i" : 2 } 
            { "username" : NumberLong("-6148914691236517204") } 
                -->> { "username" : NumberLong("-3074457345618258602") } 
                on : shard0000 { "t" : 3000, "i" : 3 } 
            { "username" : NumberLong("-3074457345618258602") } 
                -->> { "username" : NumberLong(0) } 
                on : shard0001 { "t" : 3000, "i" : 4 } 
            { "username" : NumberLong(0) } 
                -->> { "username" : NumberLong("3074457345618258602") } 
                on : shard0001 { "t" : 3000, "i" : 5 } 
            { "username" : NumberLong("3074457345618258602") } 
                -->> { "username" : NumberLong("6148914691236517204") } 
                on : shard0002 { "t" : 3000, "i" : 6 } 
            { "username" : NumberLong("6148914691236517204") } 
                -->> { "username" : { "$MaxKey" : true } } 
                on : shard0002 { "t" : 3000, "i" : 7 }

  注意:现在集合中还没有文档,但当插入新文档时,写请求一开始就会被均衡地分发到不同的分片上。通常需要等待块的增长与拆分,直到块移动时再将写请求分发到其他分片上。使用这种自动机制,数据块从一开始就会均衡地分发在所有分片上。

  使用散列片键存在着一定的局限性。首先,不能使用unique选项。其次,与其他片键一样,不能使用数组字段。最后注意,浮点型的值会先被取整,然后才会进行散列,所以1和1.999999会得到相同的散列值。

 

3.2 GridFS的散列片键

  在对GridFS集合做分片之前,确保已理解了GridFS的数据存储机制。

  在接下来的介绍中,“块”(chunks)这一术语会存在多重含义,因为GridFS会将文件拆分为块,而分片也会将集合拆分为块。因此,在本章后续内容中,分别以“GridFS块”和“分片块”表示这两种块。

  GridFS集合通常来说非常适合做分片,因为它们包含大量的文件数据。但是,在fs.chunk上自动创建的索引并不是特别适合作为分片键:{"_id":1}是一个升序键,{"fi1es_id" : 1, "n" : 1}使用了fs.files的_id字段,因此它也是一个升序键。

  但是,如果在"files id"字段上创建散列索引,则每个文件都会被随机分发到集群中。但是一个文件只能被包含在一个单一的块中。这是非常好的,因为,写请求被均衡地分发到所有分片上,而读取文件数据时只需査询一个单一的分片即可。

  为实现这种策略,必须在{"files_id" : "hashed"}上创建新的索引(在本书编写之时,mongos还不支持使用复合索引的子集作为片键)。然后依据这个字段对集合分片:

> db.fs.chunks.ensureIndex({"files_id" : "hashed"})
> sh.shardCollection("test.fs.chunks", {"files_id" : "hashed"})
{ "collectionsharded" : "test.fs.chunks", "ok" : 1 }

  另外提醒一下,由于fs.files集合比fs.chunks集合小得多,fs.files集合可能需要做分片,也可能不需要。可以对该集合做分片,但通常没什么必要。

 

3.3 流水策略

  如果有一些服务器比其他服务器更强大,我们可能会希望让这些强大的服务器处理更多的负载。比如说,假如有一个使用SSD的分片能够处理10倍于其他机器(使用转式磁盘)的负载。幸运的是,我们有10个其他分片。可强制将所有新数据插入到SSD,然后让均衡器将旧的块移动到其他分片上。这样能够提供比转式磁盘更低的延迟。

  为实现这种策略,需将最大范围的块分布在SSD上。首先,为SSD指定一个标签:

>  sh.addShardTag("shard-name", "ssd")

  将升序键的当前值一直到正无穷范围的块指定分布在SSD分片上,以便后续的写入请求均被分发到SSD分片上:

> sh.addTagRange("dbName.collName", {"_id" : ObjectId()}, 
... {"_id" : MaxKey}, "ssd")

  现在,所有的插入请求均会被路由到这个块上,这个块始终位于标签为ssd的分片上。

  但是,除非修改标签范围,否则从升序键的当前值一直到正无穷的这个范围则被固定在了这个分片上。可创建一个定时任务每天更新一次标签范围,如下:

> use config
> var tag = db.tags.findOne({"ns" : "dbName.collName", 
... "max" : {"shardKey" : MaxKey}})
> tag.min.shardKey = ObjectId()
> db.tags.save(tag)

  这样,前一天的块就可以被移动到其他分片上了。

  此策略的另一弊端是需做一些修改才能进行扩展。如果写请求超出了SSD的处理能力,想要将负载均衡地分布到当前服务器和另一台服务器并不简单。

  如果没有高性能服务器来处理插入流水,或者是没有使用标签,那么不要将升序键用作片键。否则,所有写请求都会被路由到同一分片上。

 

3.4 多热点

  单个mongod服务器在处理升序写请求时是最有效的。这种技术与分片相冲突,写请求分布在集群中时,分片是最高效的。这种技术会创建多个热点(最好在每个分片都创建几个热点),写请求于是会均衡地分布在集群内,而在单个分片上则是以升序分布的。

  为实现这种方式,需使用复合片键(compound shard key)。复合片键中的第一个值 只是比较粗略的随机值,势也比较低。可将片键第一部分中的每个值想象为一个块,如图15-6所示。随着插入数据的增多,这种现象也会随之出现,虽然可能不会被分离得这么整洁(注意图中的$minKey行)。但是,如果插入足够多的数据,最终会发现基本上每个随机值都位于一个块中。如果继续插入数据,最终同一个随机值则会对应有多个块,这时候就轮到片键中的第二部分出马了。

       

  片键的第二部分是个升序键。也就是说,在一个块内,值总是增加的,如图15-7中的文档样例所示。因此,如果每个分片拥有一个块,会是非常完美的配置:写请求在每个分片内都是升序的,如图15-8所示。当然,在多个分片中拥有多个块,每个块拥有多个热点,这种方式并不易于扩展:添加一个新的分片不会获得任何写请求,因为这个分片上没有热点块。因此,我们会希望在毎个分片上拥有几个热点块(以提供增长空间)。然后,热点块不能过多。少数的热点块能够保持升序写请求的效率。但是,在一个分片上拥有1000个“热点”的话,其实写请求就相当于是完全随机的了。

  可将这种配置想象成每个块都是一个升序文档的栈。每个分片上拥有多个栈,每个栈都是不断增长的,直到块被拆分。一旦块被拆分,只有一个新块会成为热点块:其他块实际上会处于一种“死掉”的状态,且不会再继续增长。如果这些栈均衡地分发在分片中,那么写请求也会被均衡地分发到不同的分片上。

 

4.片键规则和指导方针

  在选择片键前,应注意一些实际限制。

  由于与创建索引键的概念类似,因此决定使用哪个键作分片以及创建片键的方法都与之非常相似。事实上,我们使用的片键可能常常就是使用最频繁的索引(或者是索引的变种)。

 

4.1 片键限制

  片键不可以是数组。在拥有数组值的键上执行sh.shardCollection(),则命令不会生效。向片键插入数组值也是不被允许的。

  文档一旦插入,其片键值就无法修改了。要修改文档的片键值,必须先删除文档,修改片键的值,然后重新插入。因此,应选择不会被改变的字段,或者是很少发生改变的字段。

  大多特殊类型的索引都不能被用作片键。特别是不能在地理空间索引上进行分片。如前所述,使用散列索引作为片键是可以的。

 

4.2 片键的势

  不管片键是跳跃增长还是稳定增长,选择一个值会发生变化的键是非常重要的。与索引一样,分片在势比较高的字段上性能更佳。例如,"logLevel"键只拥有"DEBUG"、"WARN"和"ERROR"这几个值。如用其作为片键,则MongoDB最多只能将数据分为三个块(因为片键只拥有三个不同的值)。如果键拥有的值比较少,而且确实希望将这个键用作片键,则可使用该键与另一个拥有多样值的键创建一个复合片键,比如"logLevel"和"timestamp"。注意,复合片键的势比较高。

 

5.控制数据分发

  有时候,自动数据分发无法满足需求。前面已经学习过了有关选择片键以及让MongoDB自动处理事务的内容,接下来将在本节学习到更多相关内容。

  随着集群变得越来越大或者越来越繁忙,这些解决方案可能会变得不是那么有效。但是,对于比较小的集群,也许我们会希望拥有更多的控制权。

 

5.1 对多个数据库和集合使用一个集群

  MongoDB将集合均衡地分发到集群中的分片上,如果保存的数据比较均匀,则该方法非常有效。然而,如果有一个日志集合,该集合的数据不如其他集合的数据有“价值”,我们可能不希望其占用昂贵的服务器。或者,如果拥有一个强大的分片,我们可能只希望将其用在实时集合上,而不允许其他集合使用它。这些情况下,可建立独立的集群,也可将数据的保存位置明确指定给MongoDB。

  为实现这种模式,在shell中运行sh.addShardTag()辅助函数:

> sh.addShardTag("shard0000", "high")
> // shard0001 - no tag
> // shard0002 - no tag
> // shard0003 - no tag
> sh.addShardTag("shard0004", "low")
> sh.addShardTag("shard0005", "low")

  然后可以将不同的集合指定到不同的分片。例如,对于实时集合:

> sh.addTagRange("super.important", {"shardKey" : MinKey}, 
... {"shardKey" : MaxKey}, "high")

  上面这条命令的意思是,“将该集合内片键的值在负无穷到正无穷之间的数据,保存到标签为high的分片上”。也就是说,该重要集合的所有数据都不会被保存在其他服务器上。注意,这并不会影响其他集合的分发方式:其他集合仍会被均衡地分发在该分片和其他分片上。

  同样地,也可以将日志集合指定到比较便宜的服务器上:

> sh.addTagRange("some.logs", {"shardKey" : MinKey}, 
... {"shardKey" : MaxKey}, "low")

  现在,日志集合会被均衡地分发在shard0004和shard0005上。

  为集合指定一个标签范围的指令并不会立即生效。它只是一个对于均衡器的指令,运行指令可将集合移动到这些目标分片上。因此,如果整个日志集合都位于shard0002或者是均衡地分发在所有分片上,那么需要消耗一定的时间,日志集合的所有块才会被迁移到shard0004和shard0005上。

  再举一个例子。也许有这样一个集合,我们希望其出现在除标签为high的分片以外的任何分片上。可为所有的非高性能分片添加一个新的标签,创建一个新分组。分片可创建的标签多少没有限制:

> sh.addShardTag("shard0001", "whatever")
> sh.addShardTag("shard0002", "whatever")
> sh.addShardTag("shard0003", "whatever")
> sh.addShardTag("shard0004", "whatever")
> sh.addShardTag("shard0005", "whatever")

  现在,可指定该集合(名为normal.coll)分发在这五个分片上:

> sh.addTagRange("normal.coll", {"shardKey" : MinKey}, 
... {"shardKey" : MaxKey}, "whatever")

  不能动态地指定集合,如“当新集合创建时,将其随机分发到一个分片上”。但是,可使用定时任务来做这些事情。

  如果操作失误或是改变了主意,可使用sh.removeShardTab()删除分片的标签:

>  sh.removeShardTag("shard0005", "whatever")

  如果删除了某个标签范围的所有标签(例如,刪除了标签为"high"的分片标签), 均衡器不会再将数据分发到任何地方,因为没有可用的位置。所有数据仍可读可写,但不会被迁移到其他位置,除非修改标签或者标签范围。

  不存在用于删除标签范围的辅助函数,但可手动删除。手动处理标签范围,可通过mongos访问config.tags命名空间。类似地,分片的标签信息保存在分片文档"tags"字段下的config.shards命名空间。如分片文档中没有"tags"字段,则该分片就不存在标签。

 

5.2 手动分片

  有时候,对干复杂的需求或是特殊的情况,我们可能希望对集群的数据分发拥有绝对控制权。如果不希望数据被自动分发,可关闭均衡器,使用moveChunk命令手动对数据进行迁移。

  要关闭均衡器,可连接到一个mongos(任何mongos都可以),然后使用以下命令更新config.settings命名空间:

> db.settings.update({"_id" : "balancer"}, {"enabled" : false}, true)

  注意,这是一个upsert操作:如果均衡器设置不存在,则会自动创建一个。

  如正在进行迁移,则该设置要等到当前迁移完成之后才会生效。然而,一旦当前迁移完成了,均衡器就不会再做数据移动了。

  只要均衡器被关闭,就可以手动做数据迁移了(如有必要的话)。首先,査看config.chunks找出每个块的分发位置:

>  db.chunks.find()

  现在,使用moveChunk命令将块迁移到其他分片。指定需被迁移块的下边界值和目标分片的名称:

> sh.moveChunk("test.manual.stuff", 
... {user_id: NumberLong("-1844674407370955160")}, "test-rs1") 
{ "millis" : 4079, "ok" : 1 }

  然而,除非遇到特殊情况,否则都应使用MongoDB的自动分片,而非手动进行分片。如果最后得到一个拥有一个热点的分片(这并非是我们所期望的),那么大部分数据可能都将出现在这个分片上。

  尤其不要在均衡器开启的情况下手动做一些不寻常的分发。如果均衡器检测到一些不均衡的块,则会对调整过的数据进行重新分发,以便让集合再次处于均衡状态。如果希望得到非均衡的数据块分发,应使用上一小节介绍过的分片标签技术。

posted @ 2021-10-09 17:49  小家电维修  阅读(475)  评论(0编辑  收藏  举报