MongoDB分片到副本集改造实践

生产环境分片改造为复制集的解决方案

背景

生产线上使用 MongoDB Sharidng 的场景非常多,但由于业务初期评估不到位或者业务发展不符合预期,为了管理起来更方便,可能需要将 Sharding 改造为 复制集。
 
我就针对生产级业务环境需求提供最小影响服务将分片改造为复制集(含减分片场景)的解决方案。

首先,我提供两种可选方案:
> 1)如果有同步工具支持,可以选择从分片全量+增量的方式同步到复制集,然后选个时间点切换;
> 2)从集群中减分片(removeShard),最后只保留一个shard(复制集),业务接入从mongos改为复制集;

当然,如果业务数据量特别少,而且可接受一定程度上的业务停服,那也可以选择逻辑导出导入的方式。尽管这种方法最为简便,但因影响服务时间过长,所以很少会在生产环境中使用。
 
本文,我主要讲第二种方案,其核心技术点为removeShard,但经验告诉我们,这个操作往往不会那么顺利完成,大家可能会遇到primary shard提示,也可能会遇到jumbo chunk无法迁移的问题。下面我拿一个线上正式服务的案例来详细说明。

线上案例

 
简单描述业务背景,起初业务评估需求特别高,因此我们采用了分片架构,设计了3个shard,通过_id进行hash分片,但后来业务远远没能达到预期目标,再后来业务越来越萎缩,到现在分片集群反而成为了业务负担。为了减少其成本,业务决定将分片替换为复制集,同时将物理机部署改为容器化。因此,我们提供了如下迁移步骤:
> 1)目前有三个shard,remove两个shard
> 2)业务从分片访问方式改为复制集访问方式
> 3)复制集做一次迁移,迁移到容器上

 
进入正题,目前我们系统有三个shard,第一步要提前确认primary shard,何为primary shard,官方说明
> Each database in a sharded cluster has a primary shard that holds all the
un-sharded collections for that database. Each database has its own primary
shard. The primary shard has no relation to the primary in a replica set.

简单理解就是没有进行分片的集合所在库的shard。那如何确认,其实也简单,笨一点办法就是连接每个分片show collection查看即可。也可以执行sh.status查看。
那为什么要提前确认primary shard,因为如果是primary shard就无法remove,会有如下提示:

mongos> db.runCommand( { removeShard: "anav_team_3" } )
{
        "msg" : "draining started successfully",
        "state" : "started",
        "shard" : "anav_team_3",
        "note" : "you need to drop or movePrimary these databases",
        "dbsToMove" : [
             "friend",
             "users"
   ],
        "ok" : 1
}

这时候如果该shard为你要删除的对象,那么需要先删除或者移动这些对象,删除不用解释,正式环境也不允许你操作,下面看下movePrimary,官方文档

use admin
db.runCommand( { movePrimary: "friend", to: "anav_team_1" })
db.runCommand( { movePrimary: "users", to: "anav_team_1" })

之后,我们就可以进行removeShard了,其操作说明官方文档也非常详细:
 
首先保证均衡器是开启的,因为在draining数据的过程中均衡器负责将该shard上面的数据迁移至其余的shard。

mongos> sh.getBalancerState()
true
-- 如果这里结果为false,那就通过sh.setBalancerState(true)启动。
mongos> db.runCommand( { removeShard: "anav_team_2" } )
{
        "msg" : "draining started successfully",
        "state" : "started",
        "shard" : "anav_team_2",
        "note" : "you need to drop or movePrimary these databases",
        "dbsToMove" : [ ],
        "ok" : 1
}

执行完removeShard,我们再通过sh.status查看的时候可以看到指定shard正在draining数据。

11

draining数据过程非常缓慢,可以继续通过执行removeShard命令来查看当前状态:

mongos> db.runCommand( { removeShard: "anav_team_2" } )
{
    "msg" : "draining ongoing",
    "state" : "ongoing",
    "remaining" : {
        "chunks" : NumberLong(1),
        "dbs" : NumberLong(0)
    },
    "note" : "you need to drop or movePrimary these databases",
    "dbsToMove" : [ ],
    "ok" : 1
}

另外也可以通过sh.status命令看到被删除shard上的chunk数量不断减少,其余shard的chunk数量增多。

12

mongos以及shard的日志里面也可以看到相关迁移记录。
 
如果业务选择了合理的片键,removeShard会顺利完成,但在我们业务中仅仅拿_id进行了hash分片,在removeShard过程中我们遇到了jumbo chunk,导致无法迁移。

应对 jumbo chunk

13

jumbo chunk如何产生呢?每个分片都会有最大chunk的大小,保存在config.settings里面:

mongos> use config
switched to db config
mongos> db.settings.findOne({"_id":"chunksize"})
{ "_id" : "chunksize", "value" : 64 }

如果片键设计不合理很容易会导致有些chunk超出上面大小,这样均衡器就无法移动这个块儿。执行sh.status(true)可以看到jumbo chunk,也可以通过查看config.chunks来获取jumbo chunk的信息:

mongos> use config
switched to db config
mongos> db.chunks.find({jumbo:true})
{ "_id" : "team.team-_id_-6148914691236517200", "lastmod" : Timestamp(19, 5), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("-6148914691236517200") }, "max" : { "_id" : NumberLong("-5380242722759272487") }, "shard" : "anav_team_2", "jumbo" : true }
{ "_id" : "team.team-_id_1537228672809129300", "lastmod" : Timestamp(14, 2), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("1537228672809129300") }, "max" : { "_id" : NumberLong("2304588625798705234") }, "shard" : "anav_team_2", "jumbo" : true }
{ "_id" : "team.team-_id_7686143364045646500", "lastmod" : Timestamp(15, 2), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("7686143364045646500") }, "max" : { "_id" : NumberLong("8451003142456834418") }, "shard" : "anav_team_2", "jumbo" : true }
{ "_id" : "team.team-_id_2304588625798705234", "lastmod" : Timestamp(14, 3), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("2304588625798705234") }, "max" : { "_id" : NumberLong("3074114379322568708") }, "shard" : "anav_team_2", "jumbo" : true }
{ "_id" : "team.team-_id_8451003142456834418", "lastmod" : Timestamp(15, 3), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("8451003142456834418") }, "max" : { "_id" : NumberLong("9219967741494243703") }, "shard" : "anav_team_2", "jumbo" : true }
{ "_id" : "team.team-_id_-8456336119759655766", "lastmod" : Timestamp(25, 1), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("-8456336119759655766") }, "max" : { "_id" : NumberLong("-7690129288411891978") }, "shard" : "anav_team_2", "jumbo" : true }
{ "_id" : "team.team-_id_-5380242722759272487", "lastmod" : Timestamp(19, 6), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : NumberLong("-5380242722759272487") }, "max" : { "_id" : NumberLong("-4612372550362685618") }, "shard" : "anav_team_2", "jumbo" : true }

从上可以看到,anav_team_2里面存在7个jumbo chunk。
 
遇到jumbo chunk不必慌张,解决方法必然是有的。首先,我们能够想到的方法是能否直接给手动移动?官方也的确提供了moveChunk功能,参考文档

db.adminCommand( { moveChunk :  ,
                 find :  | bounds :  ,
                 to : ,
                 _secondaryThrottle : ,
                 writeConcern: ,
                 _waitForDelete :  } )

提供两种方式来定位移动的对象,find后接文档查询query条件,bound则提供要移动块儿的边界,更为精准。
 
MongoDB不允许移动大于chunksize的chunk,所以我们可以临时将chunk大小调大,方法为:

sh.setBalancerState(false)
use config
db.settings.save({"_id":"chunksize","value":10000})
db.settings.findOne({"_id":"chunksize"})

备注:chunksize单位为M。
 
使用moveChunk命令移动块儿到指定的shard:

mongos> use admin
switched to db admin
mongos> db.adminCommand({"moveChunk":"team.team","bounds":[{"_id":NumberLong("-6148914691236517200")},{"_id":NumberLong("-5380242722759272487")}],"to":"anav_team_1","_secondaryThrottle":true})
{
        "cause" : {
                "chunkTooBig" : true,
                "estimatedChunkSize" : 175991270,
                "ok" : 0,
                "errmsg" : "chunk too big to move"
        },
        "ok" : 0,
        "errmsg" : "move failed"
}

我这里是moveChunk失败了,原因是MongoDB 3.4版本手动moveChunk命令做了个限制。但失败归失败,如果其他版本中使用该功能时,务必注意加上_secondaryThrottle,加上会强制要求迁移过程间歇进行,每迁移完一些数据,需等待集群中大多数分片成功完成数据复制后再进入下一次迁移。尽管放慢迁移的过程,但同时减缓了对系统性能的影响。这在生产环境中还是尤为重要。当然,该选项仅仅适用于复制集shard。
 
移动块儿不可行我们还有一招可以尝试,那就是splitChunk,官方文档
思路就是拆分jumbo chunk为更小的块儿,然后通过均衡器来自动迁移。拿一个jumbo chunk来举例说明:

{ "_id" : "team.team-_id_MinKey", "lastmod" : Timestamp(52, 1), "lastmodEpoch" : ObjectId("59c22c0eabb58cb716f1abd7"), "ns" : "team.team", "min" : { "_id" : { "$minKey" : 1 } }, "max" : { "_id" : NumberLong("-8456336119759655766") }, "shard" : "anav_team_3", "jumbo" : true }

我们取一个min._id和max._id的大概中间值来进行split。

// 注意:该操作之前必须先关闭均衡器
use admin
sh.splitAt("team.team",{"_id":NumberLong("-9000000000000000000")})

 
这时候我们再去查config.chunks,已经看不到该chunk信息。以此类推,其他chunk都split下,sh.status可以看到要删除shard上的chunk数量翻倍
14

最后打开均衡器,这时候我们庆幸的发现,均衡器又开始迁移chunk了。
当然迁移过程中可能还会出现jumbo chunk,解法就是重复上面splitChunk操作。

待迁移完shard上所有chunk,执行removeShard会返回成功信息。

mongos> db.runCommand( { removeShard: "anav_team_2" } ) {         "msg" : "removeshard completed successfully",         "state" : "completed",         "shard" : "anav_team_2",         "ok" : 1 }

通过该方式我成功remove了两个shard,只留下primary shard,然后通知业务服务从mongos访问改为复制集方式,后面物理机改容器这种不在本文范围内,所以不再往下去讨论。

经验教训

 
最后,如果是分片场景,请务必重视: 设计合理片键****!****设计合理的片键****!****设计合理片键****!念之再三,铭之肺腑。

作者简介

许春植 花名碧壳,阿里5年DBA,主要负责MySQL和MongoDB数据库,熟悉地图领域行业数据库解决方案。

MongoDB分片到副本集改造实践》有1个想法

发表评论