使用MongoShake实现MongoDB副本集间的单向同步

使用MongoShake实现MongoDB副本集间的单向同步

Scroll Down

0、导

本篇文章前4小节介绍了使用阿里云自研的MongoShake开源工具,来实现MongoDB数据库间的单向数据同步。第5小节大篇幅着重介绍了 k8s + MongoDB单节点副本集 + MongoShake 的使用与故障解决。

1、注意事项

截止2022-04-02,MongoShake最新版本2.6.6,仅支持MongoDB 5 以下版本进行数据同步,Mongo 4生命周期在2022年4月底到期,届时MongoShake的2.6.7版本也会在4月底发布,该版本将会支持MongoDB 5版本的数据同步

MongoShake是阿里云以Golang语言编写的通用平台型服务工具,它通过读取MongoDB的Oplog操作日志来复制MongoDB的数据以实现特定需求。

支持的数据源

源数据库目标数据库
ECS上的自建MongoDB数据库ECS上的自建MongoDB数据库
本地自建的MongoDB数据库本地自建的MongoDB数据库
阿里云MongoDB实例阿里云MongoDB实例
第三方云MongoDB数据库第三方云MongoDB数据库

在全量数据同步完成之前,请勿对源库进行DDL操作,否则可能导致数据不一致。 不支持同步admin和local数据库。

数据库用户的权限要求

同步的数据源所需权限
源MongoDB实例readAnyDatabase权限、local库的read权限和mongoshake库的readWrite权限。(mongoshake库会在增量同步开始时由MongoShake程序自动在源实例中创建。)
目标MongoDB实例readWriteAnyDatabase权限或目标库的readWrite权限。

2、前提准备

上文有提到MongoShake是通过读取MongoDB的Oplog操作日志来对数据进行复制的,如果你的MongoDB没有开启这个玩意儿,MongoShake会直接给你报错run replication failed: no oplog ns in mongo.

那单节点的MongDB能使用MongoShake单向同步数据吗?答案是当然的,单节点的MongoDB也是可以开启Oplog操作日志的,详情可参考MongoDB 单机开启Oplog

单节点的MongDB也可以通过配置文件开启Oplog,mongod启动之后使用mongo-cli子命令init replication cluster即可。

# 配置文件追加以下部分(yaml格式)
replication:
 oplogSizeMB: 50  # 定义oplog大小
 replSetName: rs  # 定义副本集的名称


# mongo命令行下初始化副本集群,将该节点加入集群(默认会成为primary节点)
rs.initiate({ _id: "副本集名称", members: [{_id:0,host:" 服务器的IP : Mongo的端口号 "}]})

注意:如果使用的是MongoDB的镜像,需要注意一下,docker版的mongo移除了默认的/etc/mongo.conf配置文件(容器的/etc/mongod.conf.orig文件是个配置模板,可以借鉴),如果需要自定义配置文件,可以将其挂载到容器中的某个路径下,启动容器时通过指定该配置文件启动即可( 启动命令如下 ),mongodb容器还修改了db数据存储路径为/data/db

image.png

mongod -f /data/conf/mongod.conf

image.png

3、MongoShake安装配置

下载阿里云开源MongoShake程序

wget "http://docs-aliyun.cn-hangzhou.oss.aliyun-inc.com/assets/attach/196977/jp_ja/1608863913991/mongo-shake-v2.4.16.tar.gz" -O mongoshake.tar.gz

说明: 本文提供的链接是MongoShake 2.4.16版本,如需下载最新版本的MongoShake,请参见releases页面

解压 MongoShake

tar zxvf mongoshake.tar.gz && mv mongo-shake-v2.4.16 /root/mongoshake && cd /root/mongoshake 

修改MongoShake的配置文件collector.conf

vi collector.conf

默认情况下,仅需修改的参数如下表格所示,具体参数可见

参数说明示例值
mongo_urls源端MongoDB实例的ConnectionStringURI格式连接地址mongo_urls = mongodb://root:123456789@192.168.1.31:30017/?authSource=admin
tunnel.address目标端MongoDB实例的ConnectionStringURI格式连接地址tunnel.address = mongodb://root:123456789@dds-iiiiiiiiiabf91341198-pub.mongodb.rds.aliyuncs.com:3717,dds-iiiiiiiiiiii91342393-pub.mongodb.rds.aliyuncs.com:3717/admin?replicaSet=mgset-51666616
sync_mode数据同步的方式,all:执行全量数据同步和增量数据同步。full:仅执行全量数据同步。incr:仅执行增量数据同步sync_mode = all

随后执行下述命令启动同步任务,并打印日志信息

./collector.linux -conf=collector.conf -verbose

image.png

可以额外选择监控MongoShake状态

增量数据同步开始后,可以再开启一个命令行窗口,通过如下命令来监控MongoShake

cd /root/mongoshake && ./mongoshake-stat --port=9100

4、实际测试

因为添加测试数据的时候未及时截图,事后写文章就贴了最后的成果图给大家参考(谅解)

配置完成之后,我分别测试了两个场景,数据同步效果几乎实时,其余场景的适用结果自然就不言而喻了(基本没问题)

本地源mongodb数据库1(暂称mongodb1代号)数据单向同步到目标端阿里云Mongodb副本集(暂称mongo0代号),全量模式下,执行同步任务之后,首先会将所有数据同步到目标端,随后会几乎实时将增量数据同步到目标端。哪怕中间执行任务被中断了,再次执行任务的时候仍然会将未同步的数据同步到目标端mongodb0

本地开启两个MongoShake同步任务(需要修改collector.conf的部分参数,下问会具体说),分别将本地的两个mongodb数据库(mongo1、mongo2)同步到目标端mongodb(mongo0),测试发现,当有多个源mongodb端时,多个同步任务都会进行,如果源端的db名称一致,数据将会同步到目标端的同一个db中,每个源mongodb的数据不会被覆盖(应该是因为_id不一致);如果多个源mongodb的数据在不同的db中,那么mongoshake将会把不同的db数据同步到目标端的不同db中

这里需要注意的是,如果同一台主机开启了多个MongoShake同步任务进程,需要多修改几个参数如下所示:

# 运行多个MongoShake同步任务进程时,要保证多个进程的以下配置参数不重复
# id、full_sync.http_port、incr_sync.http_port、system_profile_port


# id用于输出pid文件等信息。
id = mongoshake2

# 全量和增量的restful监控端口,可以用curl查看内部监控metric统计情况。
full_sync.http_port = 9103
incr_sync.http_port = 9102

# profiling端口,用于查看内部go堆栈。
system_profile_port = 9201

同步数据效果就不一一展示了,如下
测试结果

5、k8s + 单节点mongodb + mongoshake

同时满足 k8s \ 单节点mongodb \ mongoshake 的情况下,有这么几个问题需要解决:

① 首先,要想使用mongoshake程序来同步mongodb数据,那么就必须有Oplog,默认情况下只有 Replica Set副本集模式才有Oplog,单节点mongodb能开启Oplog吗?

答案是能,在单实例上配置副本集,就有Oplog。具体操作可参考

② 当开启副本集之后,需要对集群初始化,命令格式是 rs.initiate({ _id: "副本集名称", members: [{_id:0,host:"服务器的IP:Mongo的端口号"}]}),那么这里的IP和端口应该怎么写呢?

mongodb初始化时填的IP地址是Pod的IP地址,如果Pod因为一些原因重启了,因为数据是持久化的(记录有mongodb的副本集信息),此时前一个Pod的IP地址已经失效了,mongodb的单副本集肯定会出现相应问题(因为primary的ip都已改变)。思考了一下,不如将Pod.IP通过环境变量传入容器内部进行使用。

     containers:
       - image: mongo:4.2.15
         name: mongodb
         env:
           - name: MONGO_INITDB_ROOT_USERNAME # 生产环境中请使用 secret
             value: root
           - name: MONGO_INITDB_ROOT_PASSWORD
             value: "123456789"
           # 这四行配置就是将Pod的IP值传入容器环境变量POD_IP
           - name: POD_IP
             valueFrom:
               fieldRef:
                 fieldPath: status.podIP

与此同时,虽然Pod.IP通过环境变量传入容器内部了,但mongo的子命令行肯定无法识别容器的环境变量(毕竟上下文都不一样了),那我们怎么办呢?能不能在连接mongd服务端时直接在当前shell中执行mong命令呢?答案是可以的,类似mysql的--execute参数,mongo也有自己的对应参数--eval,用以执行相应的mongo语句。

# mongo --help | grep "\-\-eval"
--eval arg                          evaluate javascript

image.png

这样每次重启之后都能及时获取到当前Pod的IP并绑定到mongodb的rs primary主节点上,测试结果显示,确实可以使用环境变量将当前的Pod.IP传入容器内部,同时使用mongo --eval参数指定要执行的子命令,如上图所示。

③ 解决了问题②之后又发现,因为mongodb的数据持久化了,rs.initiate()命令只有在第一次初始化才能执行成功

image.png

之后每次重启后,新的mongodb pod的状态都会由primary变为other。经搜索,这个问题是可以通过mongo命令解决的,,即执行几行命令重置集群member0的IP值(如下,因为单节点副本集,member0一定就是该单节点)

config = rs.conf();
config.members[0].host = '$POD_IP:27017';
rs.reconfig(config,{'force':true});

但是当我们使用多行mongo --eval去分别执行这三条命令的时候发现,多行mongo --eval之间并不是一个上下文环境,就类似我们在shell下开启的多个子shell一样。

image.png

紧接着查看mongo相关文档,发现mongo客户端命令可以将文件作为输入源,以此来执行多个子命令。但经过测试与验证,发现mongo客户端命令 基于文件来执行相应命令时,并不能加载容器的环境变量(这里即容器的IP地址 $POD_IP),这是因为实际上运行环境已经由当前shell变成了mongo的命令行模式。如下

mongo-eval

由以上测试可得知,mongo从文件执行多行命令的过程是,先使用mongo命令建立和mongod服务端的连接,随后在mongo命令行中执行文件中的多行命令。既然使用mongo客户端命令从文件执行多行命令并不能加载mongodb容器的环境变量,就传递参数这个目的而言,使用mongo命令从文件执行多行命令的这条路算是走不通了

之前搜索资料的时候(下图),我一直以为--eval只能执行一个命令,经过自己多次测试发现,多行命令使用;隔开即可

image.png

这样的话,如果我们在shell环境使用mongo --eval并把这三条命令合为一行,这样不就能间接将容器环境的$POD_IP参数传递给mongo命令行了嘛,受这篇文章的启发,我做了以下测试,果然成功了。

mongo-eval-1

虽然但是,总不能每次Pod重启了,都得手动exec pod执行一遍命令吧?

为了解决这个问题,那就需要用到kubernetes的command启动参数了,command执行一个脚本,脚本内容分别是启动mongod服务端程序加载配置文件、初始化mongodb副本集集群、更改mongodb副本集集群member0节点IP(如果数据持久化了,那么初始化命令只有在Pod第一次启动时才会执行成功,其余时间都会执行失败。但其实并不影响实际使用。yaml片段如下所示)

apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb2-config
data:
  mongo-init-restart.sh: |
    #!/bin/sh
    # sh -c "mongod -f /data/conf/mongod.conf" &
    echo "111"
    mongod -f /data/conf/mongod.conf
    sleep 5
    printenv POD_IP
    echo "222"
    mongo --eval "rs.initiate({ _id: 'rs', members: [{_id:0,host:'$POD_IP:27017'}]})"
    printenv POD_IP
    echo "333"
    mongo --eval "config = rs.conf();config.members[0].host = '$POD_IP:27017';rs.reconfig(config,{'force':true})"
  mongod.conf: |
    # mongod.conf
    # 这里省略mongod配置文件部分
---
containers:
        - image: mongo:4.2.15
          name: mongodb2
          command: ["/bin/sh","-c"]
          # command: ["printenv POD_IP"]  单一个command执行命令不成功
          #- sh
          #- -c
            #- "exec mongod -f /data/conf/mongod.conf"
            #- "/conf/mongo-init-restart.sh"
            #args: ["mongod -f /data/conf/mongod.conf &","./data/conf/mongo-init-restart.sh"]
          args: ["./data/conf/mongo-init-restart.sh"]
            #args: ["printenv POD_IP"]
            #args: ["sleep 3600"]

理想很美好,现实很残酷,按照上述的想法为mongdb设置启动参数之后,pod启动无误,但是如mongo --eval "rs.initiate({ _id: 'rs', members: [{_id:0,host:'$POD_IP:27017'}]})"等mongo子命令并没有被执行,然后exec进入到容器内手动执行是可以的,然后我在该初始化脚本中添加了printenv、echo等命令用以排查命令是否被执行了。测试发现并没有被执行。

image.png

然后我咨询了一下老哥,他看了眼我的yaml文件,觉得有两处问题,一是变量$POD_IP能不能被获取到,二是你确认下mongod -f这个是不是前台的命令,如果是下面的就不会执行。问题一我是肯定的,变量值肯定能被获取到。当他说完问题二,我突然意识到了问题,mongod如果被放到前台执行了的话,肯定会把后边的所有操作都给阻塞掉为了让容器持续在后台运行,那就需要将运行的程序以 前台进程 的形式运行,经我测试,使用&和nohup将程序放入后台都会导致容器异常退出。Docker容器仅在它的1号进程(PID为1)运行时,会保持运行。如果1号进程退出了,Docker容器也就退出了。
image.png

当我正在发愁,如何在前台程序命令后面执行其他命令而不被阻塞的时候,老哥给我提了这么一个思路:

老哥建议 老哥建议

他说完这话的时候,我并没有完全弄懂他的想法,我还在疑惑两个问题,一是为什么要先启动mongod后台进程,然后关闭之后再重启;二是如果在shell结束之后,谁来当pid=1的进程?

当我详细问了之后,恍然大悟,搜嘎,这样好像确实没问题哎。

# 一、首先启动一个mongod后台进程,使用mongod的命令行参数--fork来完成
mongod -f /data/conf/mongod.conf --fork --syslog
(使用 --fork参数必须指定log的形式 ,比如使用--syslog使用系统日志引擎,或者--logpath指定日志文件位置)

# 二、然后执行我那些初始化的操作
mongo 127.0.0.1:27017/admin --eval "rs.initiate({ _id: 'rs', members: [{_id:0,host:'$POD_IP:27017'}]})"
mongo 127.0.0.1:27017/admin --eval "config = rs.conf();config.members[0].host = '$POD_IP:27017';rs.reconfig(config,{'force':true})"

# 三、关闭mongod进程
mongod --shutdown

# 四、以前台形式重新启动mongod进程
mongod -f /data/conf/mongod.conf

## 第一步不以前台执行是为了不阻塞后边初始化的操作
## 第三步关闭mongod进程是为了重新以前台形式启动mongod
## 第四步重新启动mongod进程是为了以前台形式驻留,作为mongodb容器的pid=1的进程(避免没有主进程容器被杀掉)

按照如上操作,果然成功了!所有问题全部解决,哈哈哈哈开心
image.png

6、总结

本文主要讲解了MongoShake程序的简介,MongoShake使用前的注意小事项,MongoShake的使用方法,同步小测试、k8s部署mongodb副本集单节点的过程及问题解决、以及最后附赠的k8s部署mongodb单节点副本模式的资源清单(Deployment)

其中在使用 MongoShake、k8s部署mongodb单节点副本模式 时遇到的问题及解决方法分别如下:

① 使用MongoShake的MongDB需要开启Oplog,副本模式才有Oplog,所以首先需要MongoDB是副本集模式

② MongoDB副本集需要进行初始化,IP如何填写呢?

答:将 Pod.IP 通过k8s资源清单的环境变量传入容器内部进行使用,MongoDB副本集初始化时使用Pod的IP地址

③ MongoDB命令行无法识别 Pod 环境变量怎么办?

答:通过mongo的对应参数--eval在shell环境下执行初始化命令,就可以识别Pod的环境变量了

④ MongoDB Pod如果重启了,还得手动初始化集群并重置主节点IP地址,要怎么处理?

答:通过rs.conf.members[].host命令来实现主节点IP地址的重置

⑤ 如何让 MongoDB Pod 每次重启之后自动完成初始化集群并重置主节点IP地址呢?

答:使用kubernetes的command启动参数来实现每次启动自动执行的目的

⑥ MongoDB 容器需要一个Pid为1的主进程驻留 及 mongod前台启动会阻塞后续命令要如何解决?

答:
老哥建议

老哥建议

7、k8s部署mongodb单节点副本模式

k8s部署单节点mongodb副本模式步骤:
① 首先拥有一个持久化存储(hostPath或者自己任选,这里选择nfs)
② 创建pv、pvc来持久化数据(nfs提供)
③ 将mongod配置文件、运行脚本通过ConfigMap挂载到容器中
④ 部署Mongo Pod资源清单

资源清单如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb2-config
data:
  mongo-init-restart.sh: |
    #!/bin/sh
    rm -rf /data/db/mongod.lock /data/dbWiredTiger.lock
    mongod --repair
    mongod -f /data/conf/mongod.conf --fork --syslog
    #echo "111"
    printenv POD_IP
    #echo "222"
    mongo 127.0.0.1:27017/admin --eval "rs.initiate({ _id: 'rs', members: [{_id:0,host:'$POD_IP:27017'}]})"
    #printenv POD_IP
    #echo "333"
    mongo 127.0.0.1:27017/admin --eval "config = rs.conf();config.members[0].host = '$POD_IP:27017';rs.reconfig(config,{'force':true})"
    mongod --shutdown
    mongod -f /data/conf/mongod.conf
  mongod.conf: |
    # mongod.conf

    # for documentation of all options, see:
    #   http://docs.mongodb.org/manual/reference/configuration-options/
    
    # Where and how to store data.
    storage:
      dbPath: /data/db
      journal:
        enabled: true
    #  engine:
    #  wiredTiger:
    
    # where to write logging data.
    #systemLog:
    #  destination: file
    #  logAppend: true
    #  path: /var/log/mongodb/mongod.log
    
    # network interfaces
    net:
      port: 27017
      bindIp: 0.0.0.0
    
    
    # how the process runs
    processManagement:
      timeZoneInfo: /usr/share/zoneinfo
    
    #security:
    
    #operationProfiling:
    
    replication:
      oplogSizeMB: 100
      replSetName: rs
    
    #sharding:
    
    ## Enterprise-Only Options:
    
    #auditLog:
    
    #snmp:
---
apiVersion: v1
kind: Service
metadata:
  name: mongodb2
spec:
  selector:
    app: mongodb2
  ports:
    - port: 27017
      protocol: TCP
      targetPort: 27017
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongodb2
spec:
  selector:
    matchLabels:
      app: mongodb2
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mongodb2
    spec:
      containers:
        - image: mongo:4.2.15
          name: mongodb2
          command: ["/bin/sh","-c"]
          args: ["./data/conf/mongo-init-restart.sh"]
          env:
            - name: MONGO_INITDB_ROOT_USERNAME # 生产环境中请使用 secret
              value: root
            - name: MONGO_INITDB_ROOT_PASSWORD
              value: "123456789"
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          ports:
            - containerPort: 27017
              name: mongodb2
          volumeMounts:
            - name: mongodb2-config
              mountPath: /data/conf/
      volumes:
        - name: mongodb2-config
          configMap:
            name: mongodb2-config
            defaultMode: 0755
参考:

1、阿里云使用MongoShake使用

2、MongoShake github issue

3、MongoDB 单机开启Oplog

4、mongodb dockerhub

5、MongoShake的配置文件collector.conf详细参数

6、mongodb配置文件选项

7、通过环境变量将 Pod 信息呈现给容器

8、MongoDB【第一篇】MongodDB初识

9、MongoDB配置项:systemLog Options

10、Mongodb后台daemon方式启动

11、docker容器中的前台程序和后台程序,为什么一定要前台运行

12、在shell中把操作mongodb的命令参数传递给mongo

13、MongoShake issue 677

14、MongoShake issue 697