K8s部署业务应用程序非常方便,但是一些有状态应用比如数据库、消息队列等应用就相对复杂的多,这些应用普遍对数据处理性能、数据一致性要求很高,这就是K8s原生存储要解决的问题。

StatefulSet特点

StatefulSet 用来管理某 Pod 集合的部署和扩缩, 并为这些 Pod 提供持久存储和持久标识符。和 Deployment 类似, StatefulSet 管理基于相同容器规约的一组 Pod。但和 Deployment 不同的是, StatefulSet 为它们的每个 Pod 维护了一个有粘性的 ID。这些 Pod 是基于相同的规约来创建的, 但是不能相互替换:无论怎么调度,每个 Pod 都有一个永久不变的 ID。

image-20240922115634792

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

说明:

  1. PV没有namespace,但是PVC需要有,不指定时默认为default
  2. PV和PVC的accessModes、storageClassName等必须相同,否则无法匹配
  3. 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

说明:

  1. 这里selector中app必须是确定的pod name,而不能是 statefulset name
  2. 因为clusterIP设置成了None,顾ports项只需要指定port和targetPort即可,一般不写targetPort时其值取port值。
  3. 此时port值其实无意义,因为没有clusterIP,无法通过clusterIP:port访问服务。
  4. 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

说明:

  1. 想在Docker中运行systemctl命令,需要在容器配置中加上command: ["/sbin/init"]privileged: truename: cgroup等配置,当然这不是Pod该有的运行方式。
  2. 更典型的做法是用标准的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  

说明:

  1. 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

(完)