K8s部署业务应用程序非常方便,但是一些有状态应用比如数据库、消息队列等应用就相对复杂的多,这些应用普遍对数据处理性能、数据一致性要求很高,这就是K8s原生存储要解决的问题。
StatefulSet特点
StatefulSet 用来管理某 Pod 集合的部署和扩缩, 并为这些 Pod 提供持久存储和持久标识符。和 Deployment 类似, StatefulSet 管理基于相同容器规约的一组 Pod。但和 Deployment 不同的是, StatefulSet 为它们的每个 Pod 维护了一个有粘性的 ID。这些 Pod 是基于相同的规约来创建的, 但是不能相互替换:无论怎么调度,每个 Pod 都有一个永久不变的 ID。
Headless Service
有状态的资源通常由两个组件构成:Headless Service和StatefulSet。Headless Service用于为各个Pod资源分配唯一固定的标识,然后生成DNS 解析记录。StatefulSet用于编排Pod 对象,并借助persistentVolumeClaim自动为Pod资源创建专有的存储。
StatefulSet部署MySQL主从
持久化卷
Kubernetes为了能更好的支持有状态应用的数据存储问题,除了基本的HostPath和EmptyDir提供的数据持久化方案之外,还提供了PV,PVC和StorageClass资源对象来对存储进行管理。
PV的全称是Persistent Volume(持久化卷),是对底层数据存储的抽象,PV由管理员创建、维护以及配置,它和底层的数据存储实现方法有关,比如Ceph,NFS,ClusterFS等,都是通过插件机制完成和共享存储对接。
PVC的全称是Persistent Volume Claim(持久化卷声明),我们可以将PV比喻为接口,里面封装了我们底层的数据存储,PVC就是调用接口实现数据存储操作,PVC消耗的是PV的资源。
StorageClass是为了满足用于对存储设备的不同需求,比如快速存储,慢速存储等,通过对StorageClass的定义,管理员就可以将存储设备定义为某种资源类型,用户根据StorageClass的描述可以非常直观的知道各种存储资源的具体特性,这样就可以根据应用特性去申请合适的资源了。
Create PV && PVC
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
|
# 1. create namespace mysql
kubectl create namespace mysql
# 2. create PV && PVC
# PV ++++++++++++++++++++++++++++++++++++++++++++++++++++
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-ha1-pv1
namespace: mysql
labels:
type: local
spec:
storageClassName: scn-mysql-ha1
capacity:
storage: 30Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/k8s-data/mysql-ha1"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-ha1-pv2
namespace: mysql
labels:
type: local
spec:
storageClassName: scn-mysql-ha1
capacity:
storage: 30Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/k8s-data/mysql-ha1"
# PVC ++++++++++++++++++++++++++++++++++++++++++++++++++++
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-ha1-pvc1
namespace: mysql
spec:
storageClassName: scn-mysql-ha1
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-ha1-pvc2
namespace: mysql
spec:
storageClassName: scn-mysql-ha1
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
|
说明:
- PV没有namespace,但是PVC需要有,不指定时默认为default
- PV和PVC的accessModes、storageClassName等必须相同,否则无法匹配
- PVC和PV是一对一的关系,而且PVC指定的大小必须小于等于PV的大小
Create Service
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
|
# mysql master service
---
apiVersion: v1
kind: Service
metadata:
name: mysql-ha1-svr1
namespace: mysql
spec:
selector:
app: mysql-ha1-pod1
clusterIP: None
ports:
- name: svr1-3306
port: 3306
targetPort: 3306
# mysql slave services
---
apiVersion: v1
kind: Service
metadata:
name: mysql-ha1-svr2
namespace: mysqls
spec:
selector:
app: mysql-ha1-pod2
clusterIP: None
ports:
- port: 3306
# 查询服务对应的endpoints
[root@k8smaster1 bmc]# kubectl get endpoints -n mysql
NAME ENDPOINTS AGE
mysql-ha1-svr1 10.244.2.9:3306 59s
mysql-ha1-svr2 10.244.1.7:3306 59s
|
说明:
- 这里selector中app必须是确定的pod name,而不能是 statefulset name
- 因为clusterIP设置成了None,顾ports项只需要指定port和targetPort即可,一般不写targetPort时其值取port值。
- 此时port值其实无意义,因为没有clusterIP,无法通过clusterIP:port访问服务。
- clusterIP=None的意义是查询endpoints,找到当前服务Pods的ip:port
Create 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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
# master
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql-ha1-stf1
namespace: mysql
spec:
serviceName: mysql-ha1-svr1
replicas: 1
selector:
matchLabels:
app: mysql-ha1-pod1
template:
metadata:
labels:
app: mysql-ha1-pod1
spec:
containers:
- name: mysql-ha1-pod1
image: 10.10.200.11:5000/ctos83.mysql8.20231017
command: ["/sbin/init"]
securityContext:
privileged: true
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-ha1-pvc1-v
mountPath: /home/bmc/mysql-server
- name: cgroup
mountPath: /sys/fs/cgroup
readOnly: true
volumes:
- name: mysql-ha1-pvc1-v
persistentVolumeClaim:
claimName: mysql-ha1-pvc1
- name: cgroup
hostPath:
path: /sys/fs/cgroup
type: Directory
# slave
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql-ha1-stf2
namespace: mysql
spec:
serviceName: mysql-ha1-svr2
replicas: 1
selector:
matchLabels:
app: mysql-ha1-pod2
template:
metadata:
labels:
app: mysql-ha1-pod2
spec:
containers:
- name: mysql-ha1-pod2
image: 10.10.200.11:5000/ctos83.mysql8.20231017
command: ["/sbin/init"]
securityContext:
privileged: true
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-ha1-pvc2-v
mountPath: /home/bmc/mysql-server
- name: cgroup
mountPath: /sys/fs/cgroup
readOnly: true
volumes:
- name: mysql-ha1-pvc2-v
persistentVolumeClaim:
claimName: mysql-ha1-pvc2
- name: cgroup
hostPath:
path: /sys/fs/cgroup
type: Directory
|
说明:
- 想在Docker中运行systemctl命令,需要在容器配置中加上
command: ["/sbin/init"]
,privileged: true
,name: cgroup
等配置,当然这不是Pod该有的运行方式。
- 更典型的做法是用标准的MySQL镜像,将my.cnf配置文件写入ConfigMap存储,root的密码用Secret存储。用这些配置来编排K8s yaml文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
kubectl create secret generic mysql-passwd --namespace=mysql \
--from-literal=mysql_root_password=root123 --dry-run=client -o=yaml
# 创建一个 secret,name=mysql-password
# –-from-literal=mysql_root_password=root 后面的root为密码
# –-dry-run 不执行,只是校验 -o=yaml 输出配置文件
apiVersion: v1
data:
mysql_root_password: cm9vdDEyMw==
kind: Secret
metadata:
creationTimestamp: null
name: mysql-passwd
namespace: mysql
|
查看DNS解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Master pot domain
[root@k8smaster1 bmc]# nslookup mysql-ha1-svr1.mysql.svc.cluster.local 10.96.0.10
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: mysql-ha1-svr1.mysql.svc.cluster.local
Address: 10.244.2.5
# Slave pot domain
[root@k8smaster1 bmc]# nslookup mysql-ha1-svr2.mysql.svc.cluster.local 10.96.0.10
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: mysql-ha1-svr2.mysql.svc.cluster.local
Address: 10.244.1.5
|
启动MySQL并配置主从
这里用MySQL8.0(mysql8.x)配置主主模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 1. 如果是 mysql 8.0 以上版本,创建用户和授权必须分开设置。后面从服务器的设置同理。
create user hot@'B-IP' identified by 'xxx';
grant replication slave,replication client on *.* to hot@'B-IP' with grant option;
flush privileges;
# 2. 从服务器配置数据同步用户
create user hot@'A-IP' identified by 'xxx';
grant replication slave,replication client on *.* to hot@'A-IP' with grant option;
flush privileges;
# 3. 两台服务器分别执行同步
# ServerA
CHANGE MASTER TO MASTER_HOST='B-IP',MASTER_USER='hot',MASTER_PASSWORD='xxx',
MASTER_LOG_FILE='ON.000001',MASTER_LOG_POS=156;
# ServerB
CHANGE MASTER TO MASTER_HOST='A-IP',MASTER_USER='hot',MASTER_PASSWORD='xxx',
MASTER_LOG_FILE='ON.000001',MASTER_LOG_POS=156;
|
需要注意下面的几个参数:
- master_host:这个参数是master的地址,k8s提供的解析规则是
[pod].[service].[namespace].svc.cluster.local
,所以 master 的 mysql 地址是mysql-ha1-stf1-0.mysql-ha1-svr1.mysql.svc.cluster.local
- master_port:主节点的 MySQL 端口,没改默认 3306
- master_user:登录到主节点的 mysql 用户
- master_password:登录到主节点的用户密码
- master_log_file:之前查看 mysql 主节点状态时的 file 字段
- master_log_pos:之前查看 mysql 主节点状态时的 Position 字段
- master_connect-retry:主节点重连时间,比如30S
- get_master_public_key:连接主 mysql 的公钥获取方式
可问题是,这里的两个Pod没有不定IP地址,如何写上面的脚本呢?这里就需要用到StatefulSet中Pod的特性,永不改变的网络标识符。具体参考下面的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- ServerA
create user hot@'10.244.%' identified by 'root123';
grant replication slave,replication client on *.* to hot@'10.244.%' with grant option;
flush privileges;
CHANGE MASTER TO MASTER_HOST='mysql-ha1-stf1-0.mysql-ha1-svr1.mysql.svc.cluster.local',
MASTER_USER='hot',MASTER_PASSWORD='root123',MASTER_LOG_FILE='ON.000001',MASTER_LOG_POS=156;
-- ServerB
create user hot@'10.244.%' identified by 'root123';
grant replication slave,replication client on *.* to hot@'10.244.%' with grant option;
flush privileges;
CHANGE MASTER TO MASTER_HOST='mysql-ha1-stf2-0.mysql-ha1-svr2.mysql.svc.cluster.local',
MASTER_USER='hot',MASTER_PASSWORD='root123',MASTER_LOG_FILE='ON.000001',MASTER_LOG_POS=156;
|
Node服务器故障演练
Node服务器丢失
如果将承载Slave数据库的服务器网络断掉,很快statefulset状态会报告Pod失效;但是pod的状态会在几分钟之内都显示running,比较长时间后会显示Terminating,并且会一直显示此状态,而且该Pod服务不会自动迁移到新服务器。如下所示:
1
2
3
4
5
6
7
8
|
[root@k8smaster1 bmc]# kubectl get statefulsets.apps -n mysql
NAME READY AGE
mysql-ha1-stf1 0/1 41m
mysql-ha1-stf2 1/1 41m
[root@k8smaster1 home]# kubectl get po --all-namespaces |grep mysql
mysql mysql-ha1-stf1-0 1/1 Terminating 0 40m
mysql mysql-ha1-stf2-0 1/1 Running 0 40m
|
Pod被删除
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
|
# 1. 执行删除一个POD
kubectl delete pod -n mysql mysql-ha1-stf1-0
# 2. 删除之前在一个控制台监控这个POD变化
[root@k8smaster1 mysql-ha1]# kubectl get po mysql-ha1-stf1-0 -n mysql --watch
NAME READY STATUS RESTARTS AGE
mysql-ha1-stf1-0 1/1 Running 2 (75m ago) 3h
mysql-ha1-stf1-0 1/1 Terminating 2 (76m ago) 3h1m
mysql-ha1-stf1-0 0/1 Terminating 2 (77m ago) 3h2m
mysql-ha1-stf1-0 0/1 Terminating 2 (77m ago) 3h2m
mysql-ha1-stf1-0 0/1 Terminating 2 (77m ago) 3h2m
mysql-ha1-stf1-0 0/1 Terminating 2 (77m ago) 3h2m
mysql-ha1-stf1-0 0/1 Pending 0 0s
mysql-ha1-stf1-0 0/1 Pending 0 0s
mysql-ha1-stf1-0 0/1 ContainerCreating 0 0s
mysql-ha1-stf1-0 1/1 Running 0 1s
# 3. 删除之前POD信息
[root@k8smaster1 home]# kubectl get po --all-namespaces -o wide |grep mysql
mysql mysql-ha1-stf1-0 1/1 Running 2 (75m ago) 3h1m 10.244.2.8 k8snode12
mysql mysql-ha1-stf2-0 1/1 Running 0 3h49m 10.244.1.7 k8snode11
# 4. 删除之后POD信息
[root@k8smaster1 home]# kubectl get po --all-namespaces -o wide |grep mysql
mysql mysql-ha1-stf1-0 1/1 Running 0 14s 10.244.2.9 k8snode12
mysql mysql-ha1-stf2-0 1/1 Running 0 3h50m 10.244.1.7 k8snode11
|
说明:
- POD被删除之后,K8s快速拉起新POD,因为是有状态应用,这个POD会在上次运行的主机上拉起,K8s中POD名称没有变化,但是IP地址变了。
总结
因为数据库这一类有状态的服务,其数据持久化要求非常高,如果采用共享网络磁盘的方式,虽然能在K8s中灵活调度,但其性能远远无法和本地磁盘相比。K8s的优点是资源调度管理,服务弹性伸缩,并不太适合数据库这一类对一致性和性能要求高的场景,没必要强硬将其部署到K8s中。
参考阅读:
https://www.cnblogs.com/qiuhom-1874/p/17464924.html
https://blog.csdn.net/qq_40610415/article/details/134085053
(完)