MongoDB 高可用模式

MongoDB 的部署方式有:

Standalone 单节点部署

此种部署方式是最简单易用并且常见的部署了,直接使用 mongod 启动一个进程。

Master-Slave 主从结构

主从架构一般用于备份或者做读写分离。一般有一主一从和一主多从两种设计方式。

  • 主(Master):可读可写,当数据有修改的时候,会将 oplog 同步到所有连接的 slave 上。
  • 从(Slave):只读不可写,自动从 Master 同步数据。

对于 MongoDB 来说,并不推荐使用 Master-Slave 架构,因为 Master 宕机后不能自动恢复,推荐使用 Replica Set(除非 Replica 的节点数超过 50,才需要使用 Master-Slave 架构,正常情况下不可能用那么多节点)。

另外,Master-Slave 不支持链式结构,Slave 只能直接连接 Master。Redis 的 Master-Slave 支持链式结构,Slave 可以连接 Slave。

Replica Set 副本集

MongoDB 的 Replica Set(副本集)主要有两个目的:一是数据冗余做故障恢复,当发生硬件故障或其它原因造成的宕机时,可以使用副本进行恢复;二是做读写分离,读的请求分流到副本上,减轻主节点(Primary)的读压力。

Primary 和 Secondary 搭建的 Replica Set

Replica Set 是 mongod 的实例集合,它们有着同样的数据内容。包含三类角色:

  • 主节点(Primary):接收所有的写请求,然后把修改同步到所有 Secondary。一个 Replica Set 只能有一个 Primary 节点,当 Primary 挂掉后,其他 Secondary 或者 Arbiter 节点会重新选举出一个主节点。默认读请求也是发到 Primary 节点处理的,需要转发到 Secondary 时要修改客户端的连接配置。
  • 副本节点(Secondary):与主节点保持同样的数据集。当主节点挂掉的时候,参与选主。
  • 仲裁者(Arbiter):不保有数据,不参与选主,只进行选主投票。使用 Arbiter 可以减轻数据存储的硬件需求,Arbiter 跑起来几乎没什么大的硬件资源需求,但重要的一点是,在生产环境下它和其他数据节点不要部署在同一台机器上。

注意,一个自动 failover 的 Replica Set 节点数必须为奇数,目的是选主投票时要有一个大多数才能进行选主决策。

选主过程:Secondary 宕机不受影响,若 Primary 宕机,会进行重新选主。

使用 Arbiter 搭建 Replica Set

偶数个数据节点加一个 Arbiter 构成的 Replica Set 方式:

Shard Cluster 数据分片

当数据量比较大的时候,我们需要把数据分片运行在不同的机器中,以降低 CPU、内存和 IO 的压力,Sharding 就是数据库分片技术。

MongoDB 分片技术类似 MySQL 的水平切分和垂直切分,数据库主要由两种方式做 Sharding:垂直扩展和横向切分。

垂直扩展的方式就是进行集群扩展,添加更多的 CPU、内存、磁盘空间等。

横向切分则是通过数据分片的方式,通过集群统一提供服务:

MongoDB 的 Sharding 架构

MongoDB 分片架构中的角色

  • 数据分片(Shards):用来保存数据,保证数据的高可用性和一致性。可以是一个单独的 mongod 实例,也可以是一个副本集。在生产环境下 Shard 一般是一个 Replica Set,以防止该数据片的单点故障。所有 Shard 中有一个 PrimaryShard,里面包含未进行划分的数据集合:
  • 查询路由(Query Routers):路由就是 mongos 的实例,客户端直接连接 mongos,由 mongos 把读写请求路由到指定的 Shard 上去。一个 Sharding 集群可以有一个 mongos,也可以有多个 mongos 以减轻客户端请求的压力。
  • 配置服务器(Config servers):保存集群的元数据(metadata),包含各个 Shard 的路由规则。

安装部署单节点 MongoDB

创建 NFS 存储

NFS 存储主要是为了给 MongoDB 提供稳定的后端存储,当 MongoDB 的 Pod 发生故障重启或迁移后,依然能获得原先的数据。

记得保证所有可部署 Pod 的节点都要安装 NFS 服务端/客户端。

mongo默认配置(小记)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
# engine:
# mmapv1:
# wiredTiger:
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1
# how the process runs
processManagement:
timeZoneInfo: /usr/share/zoneinfo
#security:
#authorization: enabled
#operationProfiling:
#replication:
#sharding:
## Enterprise-Only Options:
#auditLog:
#snmp:

部署 MongoDB 应用服务

(1)首先创建一个名为 mongodb.yaml 的配置文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 创建PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: mongodb-pv
namespace: middleware
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-mongodb
nfs:
path: /data/k8s
server: 10.1.4.13
---
# 创建pvc
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mongodb-pvc
namespace: middleware
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: nfs-mongodb
---
# 创建Service
apiVersion: v1
kind: Service
metadata:
name: mongodb-svc
namespace: middleware
spec:
type: NodePort
ports:
- name: mongo
port: 27017
targetPort: 27017
nodePort: 30017
protocol: TCP
selector:
app: mongodb
---
# 创建Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo-deploy
namespace: middleware
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: mongo:4.4.13
imagePullPolicy: IfNotPresent
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-pvc
mountPath: /data/db
subPath: mongo-data
- name: mongo-config
mountPath: /etc/mongod.conf
subPath: mongo-data
volumes:
- name: mongo-pvc
persistentVolumeClaim:
claimName: mongodb-pvc
- name: mongo-config
persistentVolumeClaim:
claimName: mongodb-pvc

(2)接着执行如下命令对这个 YAML 文件进行部署:

1
kubectl apply -f mongodb.yaml

(3)稍等片刻,执行如下命令可以查看是否创建成功:

1
2
3
kubectl get pv 
kubectl get pods
kubectl get service

访问测试

(1)使用客户端工具连接上刚刚创建的 MongoDB,然后创建一个 hangge 数据库以及 test 集合,并给集合中插入一些数据,验证 MongoDB 部署成功。

(2)接着执行如下命令强制重启 Pod,重启后再次查看数据库可以发现数据没有丢失,说明数据持久化也是成功的。

1
kubectl replace --force -f mongodb.yaml

安装部署 MongoDB 副本集(3 节点)

Headless Service

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: mongodb-rs
namespace: middleware
spec:
publishNotReadyAddresses: false
clusterIP: None
selector:
app: mongodb-rs

ConfigMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: v1
kind: ConfigMap
metadata:
name: mongodb-rs-cm
namespace: middleware
data:
keyfile: |
dGhpcyBpcyBycyBzdXBlciBzZWNyZXQga2V5Cg==
mongod_rs.conf: |+
systemLog:
destination: file
logAppend: true
path: /data/mongod.log
storage:
dbPath: /data
journal:
enabled: true
directoryPerDB: true
wiredTiger:
engineConfig:
cacheSizeGB: 2
directoryForIndexes: true
processManagement:
fork: true
pidFilePath: /data/mongod.pid
net:
port: 27017
bindIp: 0.0.0.0
maxIncomingConnections: 5000
security:
keyFile: /data/configdb/keyfile
authorization: enabled
replication:
oplogSizeMB: 1024
replSetName: rs_prod

PV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
apiVersion: v1
kind: PersistentVolume
metadata:
finalizers:
- kubernetes.io/pv-protection
labels:
mongo-pvname: mongodb-pv0
name: mongodb-pv0
namespace: middleware
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: mongodb-rs-storage
nfs:
path: /data/k8s/mongo-data0
server: 10.1.4.13
readOnly: false
---
apiVersion: v1
kind: PersistentVolume
metadata:
finalizers:
- kubernetes.io/pv-protection
labels:
mongo-pvname: mongodb-pv1
name: mongodb-pv1
namespace: middleware
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: mongodb-rs-storage
nfs:
path: /data/k8s/mongo-data1
server: 10.1.4.13
readOnly: false
---
apiVersion: v1
kind: PersistentVolume
metadata:
finalizers:
- kubernetes.io/pv-protection
labels:
mongo-pvname: mongodb-pv2
name: mongodb-pv2
namespace: middleware
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: mongodb-rs-storage
nfs:
path: /data/k8s/mongo-data2
server: 10.1.4.13
readOnly: false

Statefulset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb-rs
namespace: middleware
spec:
serviceName: mongodb-rs
replicas: 3
selector:
matchLabels:
app: mongodb-rs
template:
metadata:
labels:
app: mongodb-rs
spec:
containers:
- name: mongo
image: mongo:4.4.13
ports:
- containerPort: 27017
name: client
command: ["sh"]
args:
- "-c"
- |
set -ex
mongod --config /data/configdb/mongod_rs.conf
sleep infinity
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
resources:
limits:
cpu: '2'
memory: 2000Mi
volumeMounts:
- name: conf
mountPath: /data/configdb
readOnly: false
- name: data
mountPath: /data
readOnly: false
volumes:
- name: conf
configMap:
name: mongodb-rs-cm
defaultMode: 0600
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
storageClassName: mongodb-rs-storage

执行副本集初始化操作

上面已经完成了副本集的部署,通过 StatefulSet 建立了三个 Pod 运行 mongod 服务,此时三个 mongod 是没有任何联系的,需要进行初始化配置:

1
2
3
4
5
6
7
8
9
10
mongo

config = { _id:"rs_prod", members:[
{_id:0,host:"mongodb-rs-0.mongodb-rs.middleware.svc.cluster.local:27017",priority:90},
{_id:1,host:"mongodb-rs-1.mongodb-rs.middleware.svc.cluster.local:27017",priority:80},
{_id:2,host:"mongodb-rs-2.mongodb-rs.middleware.svc.cluster.local:27017",priority:70}
]
}

rs.initiate(config);

读写权限

  • PRIMARY:默认情况下,PRIMARY 节点有读写权限,SECONDARY 节点没有读写权限。
  • SECONDARYSECONDARY 节点可以成功使用账户密码登录,但进行查询时会报错 NotMasterNoSlaveOk(提示不是 master 并且 slaveOk 也没打开)。此时使用 rs.secondaryOk(); 可以临时打开读权限。

问题 1:MongoSocketException: mongodb-rs-0.mongodb-rs.middleware.svc.cluster.local

解决方案一

参考:https://stackoverflow.com/questions/60413876/kubernetes-mongodb-connection-issue-from-java

部署副本集时,rs.initiate 命令中的 host 必须使用正确的 K8s DNS 主机名:

1
2
3
4
5
rs.initiate({_id: "MainRepSet", version: 1, members: [
{ _id: 0, host: "mongod-0.mongodb-service.default.svc.cluster.local:27017" },
{ _id: 1, host: "mongod-1.mongodb-service.default.svc.cluster.local:27017" },
{ _id: 2, host: "mongod-2.mongodb-service.default.svc.cluster.local:27017" }
]})

解决方案二

配合 read_preference=pymongo.ReadPreference.PRIMARYdirectConnection=True 解决:

1
2
3
4
REPLICASET_URL = 'mongodb://root:root@xxx:27017/admin?replicaSet=rs_prod&slaveOk=true&connectTimeoutMS=3000&socketTimeoutMS=3000&maxPoolSize=1'

# 获取mongoclient
client = MongoClient(REPLICASET_URL, read_preference=pymongo.ReadPreference.PRIMARY, directConnection=True)

问题 2:rs.status() 报 "Our replica set config is invalid or we are not a member of it"

解决方案

使用 rs.status() 查看各 host 是否正确,不正确的话重新配置:

1
2
3
4
5
6
7
8
9
reConfig = { _id:"rs_prod", members:[
{_id:0,host:"mongodb-rs-0.mongodb-rs.middleware.svc.cluster.local:27017",priority:90},
{_id:1,host:"mongodb-rs-1.mongodb-rs.middleware.svc.cluster.local:27017",priority:80},
{_id:2,host:"mongodb-rs-2.mongodb-rs.middleware.svc.cluster.local:27017",priority:70}
]
}

# force -> 非主节点强制请求重新分配主节点
rs.reconfig(reConfig, { force:true })

附:开启用户权限认证

如果是集群部署,记得连接到 Primary 节点:rs.isMaster() # 判断 rs 主节点

(1)注意如果之前已经使用没有鉴权的方式部署过 MongoDB,并且做了持久化,那么再改动 YAML 文件重新部署是不会起作用的。这种情况需要进入容器操作:

1
kubectl -n middleware exec -it xxx /bin/bash

(2)进入 MongoDB 客户端:

1
mongo

(3)执行如下命令创建用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use admin

// 创建 root 账户
db.createUser({"user":"root","pwd":"EIEU#@(#234werq","roles":["root"]})

// 登录 root
db.auth("root","EIEU#@(#234werq")

// 创建 admin 账户
db.createUser({user:"admin", pwd:"EIEU#@(#234werq", roles:[{role: "userAdminAnyDatabase", db:"admin" }]})

// 创建日常使用账户
db.createUser(
{
user: "mongouser",
pwd: "EIEU#@(#234werq",
roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
}
)

// 删除用户
use {账号数据所在的数据库}
db.dropUser('python')