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
mongod -f /data/conf/mongod.conf
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
可以额外选择监控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
这样每次重启之后都能及时获取到当前Pod的IP并绑定到mongodb的rs primary主节点上,测试结果显示,确实可以使用环境变量将当前的Pod.IP传入容器内部,同时使用
mongo --eval
参数指定要执行的子命令,如上图所示。
③ 解决了问题②之后又发现,因为mongodb的数据持久化了,rs.initiate()命令只有在第一次初始化才能执行成功
之后每次重启后,新的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一样。
紧接着查看mongo相关文档,发现mongo客户端命令可以将文件作为输入源,以此来执行多个子命令。但经过测试与验证,发现mongo客户端命令 基于文件来执行相应命令时,
并不能加载容器的环境变量
(这里即容器的IP地址 $POD_IP),这是因为实际上运行环境已经由当前shell变成了mongo的命令行模式。如下
由以上测试可得知,
mongo从文件执行多行命令的过程是,先使用mongo命令建立和mongod服务端的连接,随后在mongo命令行中执行文件中的多行命令
。既然使用mongo客户端命令从文件执行多行命令并不能加载mongodb容器的环境变量,就传递参数这个目的而言,使用mongo命令从文件执行多行命令的这条路算是走不通了
。之前搜索资料的时候(下图),我一直以为
--eval
只能执行一个命令,经过自己多次测试发现,多行命令使用;
隔开即可
这样的话,如果我们在shell环境使用
mongo --eval
并把这三条命令合为一行,这样不就能间接将容器环境的$POD_IP参数传递给mongo命令行了嘛,受这篇文章的启发,我做了以下测试,果然成功了。
虽然但是,总不能每次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
等命令用以排查命令是否被执行了。测试发现并没有被执行。
然后我咨询了一下老哥,他看了眼我的yaml文件,觉得有两处问题,一是变量$POD_IP能不能被获取到,二是
你确认下mongod -f这个是不是前台的命令,如果是下面的就不会执行
。问题一我是肯定的,变量值肯定能被获取到。当他说完问题二,我突然意识到了问题,mongod如果被放到前台执行了的话,肯定会把后边的所有操作都给阻塞掉
。为了让容器持续在后台运行,那就需要将运行的程序以 前台进程 的形式运行,经我测试,使用&和nohup将程序放入后台都会导致容器异常退出
。Docker容器仅在它的1号进程(PID为1)运行时,会保持运行。如果1号进程退出了,Docker容器也就退出了。
当我正在发愁,如何在前台程序命令后面执行其他命令而不被阻塞的时候,老哥给我提了这么一个思路:
![]()
![]()
他说完这话的时候,我并没有完全弄懂他的想法,我还在疑惑两个问题,一是为什么要先启动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的进程(避免没有主进程容器被杀掉)
按照如上操作,果然成功了!所有问题全部解决,哈哈哈哈开心
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
参考:
5、MongoShake的配置文件collector.conf详细参数
9、MongoDB配置项:systemLog Options
11、docker容器中的前台程序和后台程序,为什么一定要前台运行