StatefulSet的工作原理,和处理拓补状态和存储状态的方法,然后我们深入解读一下部署一个StatefulSet的完整流程

我们部署一个MySQL集群,是Kubernetes官方文档中的一个经典案例

但是难度并不低,因为相比较于Etcd等原生考虑了分布式需求的项目,MySQL在分布式上的搭建并不友好

所以我们看下如何将其集群搭建进行容器化

我们来看一下想要搭建的有状态的应用

首先是一个主从复制的MySql集群

然后有一个主节点

多个从节点

从节点支持水平扩展

所有的写操作,只在主节点上执行

我们,将上面的有状态应用的需求,通过一张图来表示

图片

常规的环境中,布置这套环境,只需要思考配置好主从节点的复制和同步

在安装好MySql的Master节点之后,需要利用XtraBackup将Master节点的数据备份到指定目录

这会在目标目录中生成一个备份信息文件,名为xtrabackup_binlog_info,这个文件汇包含两个信息

$ cat xtrabackup_binlog_info

TheMaster-bin.000001     481

然后配置Slave节点,Slave节点在第一次启动之前,需要先将Master节点的备份数据,连同备份信息,一同拷贝到自己的数据目录下,然后执行一句SQL

TheSlave|mysql> CHANGE MASTER TO

MASTER_HOST=’$masterip’,

MASTER_USER=’xxx’,

MASTER_PASSWORD=’xxx’,

MASTER_LOG_FILE=’TheMaster-bin.000001′,

MASTER_LOG_POS=481;

然后启动Slave节点

TheSlave|mysql> START SLAVE;

接下来,可以往这个集群中添加更多的Slave节点

接下来,我们将Slave节点的数据备份指定在一个目录,生成额外的备份信息,名为

xtrabackup_slave_info,这两个文件也包含了MASTER_LOG_FILE和MASTER_LOG_POS

两个字段

然后就可以执行如前面的两个SQL

开始启动和初始化新的SLAVE节点了

这样,部署过程中,会存在着一些难点

1.Master节点和Slave节点需要由不同的配置文件,来区分对应的节点信息

2.Master节点和Slave需要进行备份文件的交换

3.在Slave第一次启动之前,需要执行一些初始化的SQL操作

因为MySQL具有拓补的状态,所以需要通过StatefulSet来解决这三座大山的问题

第一座大山,Master节点和Slave节点需要有不同的配置文件,很容易处理,需要给主从节点准备不同的MySql配置文件,根据Pod的编号进行挂载即可

我们可以使用ConfigMap来进行相关的配置

apiVersion: v1

kind: ConfigMap

metadata:

name: mysql

labels:

app: mysql

data:

master.cnf: |

# 主节点MySQL的配置文件

[mysqld]

log-bin

slave.cnf: |

# 从节点MySQL的配置文件

[mysqld]

super-read-only

我们分别定义了master.cnf和slave.cnf两个MySQL的配置文件

master.cnf开启了log-bin,使用二进制日志文件的方式进行主从复制,这是一个标准的设置

slave.cnf开启了super-read-only,从节点会拒绝除了主节点数据同步之外的所有写操作,对用户是只读的

这是ConfigMap的data部分,是key-value的,master.cnf就是这个配置文件的key,而|后面二代内容,就这个配置文件的Vlaue,这个数据将来挂载进Master节点对应的Pod后,就会在Volume目录里生成一个master.cnf的文件

然后我们创建两个Service来供StatefulSet使用

分别定义如下

apiVersion: v1

kind: Service

metadata:

name: mysql

labels:

app: mysql

spec:

ports:

– name: mysql

port: 3306

clusterIP: None

selector:

app: mysql

apiVersion: v1

kind: Service

metadata:

name: mysql-read

labels:

app: mysql

spec:

ports:

– name: mysql

port: 3306

selector:

app: mysql

这两个Service都带了app=mysql标签的Pod,就是所有的mysql pod,端口映射都是对应的por的3306端口

不过第一个mysql的service是一个Headless Service,作用是,通过Pod分配DNS来固定拓补装填,比如mysql-0.mysql和mysql-1.mysql,这样的DNS名字,其中编号0就是主节点

然后mysql-read就是一个普通的Service,会将访问这个DNS的请求转发到任何一个Mysql的主从节点上

所有用户的写请求,通过第一个DNS来寻找mysql-0的DNS记录

然后看第二个问题

Master和Slave节点的传输问题

我们需要定义一个标准的Mysql主从节点的StatuefulSet框架

图片

如上所示

我们先定义了一些通用的字段,比如selector表示,这个StatefulSet表示Pod必须要带着app=mysql的标签,使用的Headless Service的名字是mysql

StatefulSet的replicas的值是3,表示mysql集群有三个节点,一个Master节点,两个Slave节点

StatefulSet管理的有状态应用是利用了同一份Pod模板创建出来的,使用的是同一个镜像,如果需要不同节点不同的镜像,先别考虑StatefulSet了

除了这些基本的字段外,作为一个由存储状态的MySql集群,还需要考虑存储状态,我们需要定义PVC模板,来给每一个Pod分配指定的硬盘空间,比如PVC的resources.request.strorage指定存储的大小为10GB,并且声明了ReadWriteOnce

然后是对应的Pod模板 template字段

由于StatefulSet管理的Pod需要来自同一个镜像,于是我们编写Pod的时候,需要分别思考不同的情况

Pod是Master节点该怎么办

Pod是Slave节点,该怎么办

首先说,由于StatefulSet的Pod模板中声明了Service,保证了启动的顺序

所以我们编写一个initContainer来利用容器的启动序号来进行不同节点的启动

      …

# template.spec

initContainers:

– name: init-mysql

image: mysql:5.7

command:

– bash

– “-c”

– |

set -ex

# 从Pod的序号,生成server-id

[[ `hostname` =~ -([0-9]+)$ ]] || exit 1

ordinal=${BASH_REMATCH[1]}

echo [mysqld] > /mnt/conf.d/server-id.cnf

# 由于server-id=0有特殊含义,我们给ID加一个100来避开它

echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf

# 如果Pod序号是0,说明它是Master节点,从ConfigMap里把Master的配置文件拷贝到/mnt/conf.d/目录;

# 否则,拷贝Slave的配置文件

if [[ $ordinal -eq 0 ]]; then

cp /mnt/config-map/master.cnf /mnt/conf.d/

else

cp /mnt/config-map/slave.cnf /mnt/conf.d/

fi

volumeMounts:

– name: conf

mountPath: /mnt/conf.d

– name: config-map

mountPath: /mnt/config-map

通过这个名为init-mysql的initContainer容器,从Pod的hostname里面,读取到了Pod的序号,并以此作为MySQL的Server-id

然后init通过这个序号,判定是否是Master节点,并且,生成了对应的server-id.cnf

然后根据序号,拷贝不同的conf.d

文件拷贝的源目录/mnt/config-map

正是挂载了Config-map的的Volume

conf则是自己自动生成的空目录

然后,在Slave Pod启动前,从Master或者Slave Pod中拷贝数据库数据在自己的目录下

为了实现这个操作,需要定义第二个initContainer

      …

# template.spec.initContainers

– name: clone-mysql

image: gcr.io/google-samples/xtrabackup:1.0

command:

– bash

– “-c”

– |

set -ex

# 拷贝操作只需要在第一次启动时进行,所以如果数据已经存在,跳过

[[ -d /var/lib/mysql/mysql ]] && exit 0

# Master节点(序号为0)不需要做这个操作

[[ `hostname` =~ -([0-9]+)$ ]] || exit 1

ordinal=${BASH_REMATCH[1]}

[[ $ordinal -eq 0 ]] && exit 0

# 使用ncat指令,远程地从前一个节点拷贝数据到本地

ncat –recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql

# 执行–prepare,这样拷贝来的数据就可以用作恢复了

xtrabackup –prepare –target-dir=/var/lib/mysql

volumeMounts:

– name: data

mountPath: /var/lib/mysql

subPath: mysql

– name: conf

mountPath: /etc/mysql/conf.d

在这个镜像中,我们使用xtrabackup的镜像,安装了xtrabackup镜像

在其中,我们进行了一个判断,当初始化的数据已经存在的时候,或者自己是Mater节点,就不需要进行拷贝操作

接下来拉取对应的备份数据

然后我们进行拷贝到本地的/mysql节点,这个节点实际上一个名为data的PVC,也是我们声明的持久化存储

利用这里面的数据进行恢复操作

不过clone-mysql容器还要对/var/lib/mysql目录进行一个xtrabackup –prepare操作,来让拷贝来的数据进入一致性状态

这样,就通过InitContainer完成了对主 从 节点备份文件传输的操作处理过程

然后我们要启动这个MySQL

只不过在启动Slave节点之前,我们需要利用拷贝来的数据进行初始化

如何进行初始化呢?

这就是第三座大山,在第一次启动之前,执行初始化SQL

我们可以额外定义一个sidecar容器,完成这个操作

      …

# template.spec.containers

– name: xtrabackup

image: gcr.io/google-samples/xtrabackup:1.0

ports:

– name: xtrabackup

containerPort: 3307

command:

– bash

– “-c”

– |

set -ex

cd /var/lib/mysql

# 从备份信息文件里读取MASTER_LOG_FILEM和MASTER_LOG_POS这两个字段的值,用来拼装集群初始化SQL

if [[ -f xtrabackup_slave_info ]]; then

# 如果xtrabackup_slave_info文件存在,说明这个备份数据来自于另一个Slave节点。这种情况下,XtraBackup工具在备份的时候,就已经在这个文件里自动生成了”CHANGE MASTER TO” SQL语句。所以,我们只需要把这个文件重命名为change_master_to.sql.in,后面直接使用即可

mv xtrabackup_slave_info change_master_to.sql.in

# 所以,也就用不着xtrabackup_binlog_info了

rm -f xtrabackup_binlog_info

elif [[ -f xtrabackup_binlog_info ]]; then

# 如果只存在xtrabackup_binlog_inf文件,那说明备份来自于Master节点,我们就需要解析这个备份信息文件,读取所需的两个字段的值

[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1

rm xtrabackup_binlog_info

# 把两个字段的值拼装成SQL,写入change_master_to.sql.in文件

echo “CHANGE MASTER TO MASTER_LOG_FILE=’${BASH_REMATCH[1]}’,\

MASTER_LOG_POS=${BASH_REMATCH[2]}” > change_master_to.sql.in

fi

# 如果change_master_to.sql.in,就意味着需要做集群初始化工作

if [[ -f change_master_to.sql.in ]]; then

# 但一定要先等MySQL容器启动之后才能进行下一步连接MySQL的操作

echo “Waiting for mysqld to be ready (accepting connections)”

until mysql -h 127.0.0.1 -e “SELECT 1”; do sleep 1; done

echo “Initializing replication from clone position”

# 将文件change_master_to.sql.in改个名字,防止这个Container重启的时候,因为又找到了change_master_to.sql.in,从而重复执行一遍这个初始化流程

mv change_master_to.sql.in change_master_to.sql.orig

# 使用change_master_to.sql.orig的内容,也是就是前面拼装的SQL,组成一个完整的初始化和启动Slave的SQL语句

mysql -h 127.0.0.1 <<EOF

$(<change_master_to.sql.orig),

MASTER_HOST=’mysql-0.mysql’,

MASTER_USER=’root’,

MASTER_PASSWORD=”,

MASTER_CONNECT_RETRY=10;

START SLAVE;

EOF

fi

# 使用ncat监听3307端口。它的作用是,在收到传输请求的时候,直接执行”xtrabackup –backup”命令,备份MySQL的数据并发送给请求者

exec ncat –listen –keep-open –send-only –max-conns=1 3307 -c \

“xtrabackup –backup –slave-info –stream=xbstream –host=127.0.0.1 –user=root”

volumeMounts:

– name: data

mountPath: /var/lib/mysql

subPath: mysql

– name: conf

mountPath: /etc/mysql/conf.d

第一部分,mysql的初始化工作,需要将sql保存在一个名为change_master_to.sql.in文件里面

sidecar在保存之前,看自己的/var/lib/mysql目录下,是否有xtrabackup_slave_info这个备份信息

如果有,说明是一个Slave节点上的备份信息,这时候XtraBackup已经在这个文件中自动生成了CHANGE MASTER TO SQL语句,我们只需要重命名一下即可

如果没有xtrabackup_slave_info文件,但是存在xtrabackup_binlong_info文件,说明备份数据来自于Master节点,这时候,sidecar容器需要解析这个备份信息文件,读取MASTER_LOG_FILE和MASTER_LOG_POS这两个字段的值,拼出初始化SQL语句,写入到change_master_to.sql.in文件中

然后只要有这个change_master_to_sql.in的文件的存在,就需要进行集群初始化的操作

这时候sidecar容器需要读取并执行change_master_to.sql.in里面的CHANGE MASTER TO 指令

在执行一句START SLAVE命令,一个SLAVE节点就被启动成功了

Pod中的容器并没有先后顺序,在执行初始化SQL之前,需要执行下select 1来检查MySQL服务是否可用

然后我们需要删除掉之前的备份信息文件,包括我们拼出来的change_master_to.sql.in,避免下次容器重启后,发现文件存在,重新执行一次数据恢复和集群初始化的操作

然后sidecar容器执行了开启了一个数据传输服务

sidecar容器会启动ncat命令启动一个工作在3307端口上的网络传输服务,方便传递xtrabackup –backup指令备份当前MySQL数据,然后给请求者

这样就解决了第三个大山

完成了Slave节点第一次启动前的初始化工作

最后,我们定义一下Pod中的MySQL容器,MySQL半身的定义不难

      …

# template.spec

containers:

– name: mysql

image: mysql:5.7

env:

– name: MYSQL_ALLOW_EMPTY_PASSWORD

value: “1”

ports:

– name: mysql

containerPort: 3306

volumeMounts:

– name: data

mountPath: /var/lib/mysql

subPath: mysql

– name: conf

mountPath: /etc/mysql/conf.d

resources:

requests:

cpu: 500m

memory: 1Gi

livenessProbe:

exec:

command: [“mysqladmin”, “ping”]

initialDelaySeconds: 30

periodSeconds: 10

timeoutSeconds: 5

readinessProbe:

exec:

# 通过TCP连接的方式进行健康检查

command: [“mysql”, “-h”, “127.0.0.1”, “-e”, “SELECT 1”]

initialDelaySeconds: 5

periodSeconds: 2

timeoutSeconds: 1

我们使用了一个标准的MySQL5.7的镜像,数据目录为默认的/var/lib/mysql

配置文件目录则是/etc/mysql/conf.d

基本的volumes已经给其定义好了

然后,还定义了一个livenessProbe,通过mysqladmin ping命令检查是否健康

然后利用一个readinessProbe来查询SQL select 1检查MySQL服务是否可用,凡事readiness Probe检查失败的MySQL Pod,都会被摘除掉

一个完整的主从复制模式mysql集群基本定义完了

我们使用kubectl 运行一下这个StatefulSet

只需要在Kubernetes集群中创建对应的PV,方便去获取存储空间

比如使用Rook

$ kubectl create -f rook-storage.yaml

$ cat rook-storage.yaml

apiVersion: ceph.rook.io/v1beta1

kind: Pool

metadata:

name: replicapool

namespace: rook-ceph

spec:

replicated:

size: 3

apiVersion: storage.k8s.io/v1

kind: StorageClass

metadata:

name: rook-ceph-block

provisioner: ceph.rook.io/block

parameters:

pool: replicapool

clusterNamespace: rook-ceph

storageClass会自动为每一个PVC创建对应的PV

配合的,要使用这个storageClass,需要在mysql-statefulset.yaml中声明

storageClassName=rook-ceph-block

才能使用这个Rook提供的持久化存储

这样,我们就可以创建这个StatefulSet了

kubectl create -f mysql-statefulSet,yaml

kubectl get pod -l app=mysql

这样,StatefulSet启动后,会有三个Pod运行

然后可以尝试着向这个MySql集群发起请求,执行一些SQL操作来检测是否正常

kubectl run mysql-client –image=mysql:1.5.7 -i –rm –restart=Never –\

mysql -h mysql-0.mysql <<EOF

CREATE DATABASE test;

CREATE TABLE test.messages (message VARCHAR(250));

INSERT INTO test.message VALUES(‘hello’)

EOF

这样,我们连接上0节点的mysql,然后执行了创建数据库表的操作,并插入了一条数据

这样,我们连接上从节点,从从节点上进行读取操作

$ kubectl run mysql-client –image=mysql:5.7 -i -t –rm –restart=Never –\

mysql -h mysql-read -e “SELECT * FROM test.messages”

Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false

+———+

| message |

+———+

| hello   |

+———+

pod “mysql-client” deleted

而且,我们还能再次基础上,扩展我们的Mysql

kubectl scale statefulset mysql –replicas=5

这样,新的Slave Pod mysql-3和mysql-4就出现了

而且,直接连接mysql-3,能看到我之前的操作

$ kubectl run mysql-client –image=mysql:5.7 -i -t –rm –restart=Never –\

mysql -h mysql-3.mysql -e “SELECT * FROM test.messages”

Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false

+———+

| message |

+———+

| hello   |

+———+

pod “mysql-client” deleted

这样,新创建的mysql-3,也能读取到之前的插入记录,数据备份和恢复,都是有效的

今天的文章中,我们以MySQL集群为例,分享了一个实际的StatefulSet的编写过程,对应的完整的YAML文件

apiVersion: apps/v1beta2

kind: StatefulSet

metadata:

name: mysql

spec:

selector:

matchLabels:

app: mysql

serviceName: mysql

replicas: 3

template:

metadata:

labels:

app: mysql

spec:

initContainers:

– name: init-mysql

image: mysql:5.7

command:

– bash

– “-c”

– |

set -ex

# Generate mysql server-id from pod ordinal index.

[[ `hostname` =~ -([0-9]+)$ ]] || exit 1

ordinal=${BASH_REMATCH[1]}

echo [mysqld] > /mnt/conf.d/server-id.cnf

# Add an offset to avoid reserved server-id=0 value.

echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf

# Copy appropriate conf.d files from config-map to emptyDir.

if [[ $ordinal -eq 0 ]]; then

cp /mnt/config-map/master.cnf /mnt/conf.d/

else

cp /mnt/config-map/slave.cnf /mnt/conf.d/

fi

volumeMounts:

– name: conf

mountPath: /mnt/conf.d

– name: config-map

mountPath: /mnt/config-map

– name: clone-mysql

image: gcr.io/google-samples/xtrabackup:1.0

command:

– bash

– “-c”

– |

set -ex

# Skip the clone if data already exists.

[[ -d /var/lib/mysql/mysql ]] && exit 0

# Skip the clone on master (ordinal index 0).

[[ `hostname` =~ -([0-9]+)$ ]] || exit 1

ordinal=${BASH_REMATCH[1]}

[[ $ordinal -eq 0 ]] && exit 0

# Clone data from previous peer.

ncat –recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql

# Prepare the backup.

xtrabackup –prepare –target-dir=/var/lib/mysql

volumeMounts:

– name: data

mountPath: /var/lib/mysql

subPath: mysql

– name: conf

mountPath: /etc/mysql/conf.d

containers:

– name: mysql

image: mysql:5.7

env:

– name: MYSQL_ALLOW_EMPTY_PASSWORD

value: “1”

ports:

– name: mysql

containerPort: 3306

volumeMounts:

– name: data

mountPath: /var/lib/mysql

subPath: mysql

– name: conf

mountPath: /etc/mysql/conf.d

resources:

requests:

cpu: 500m

memory: 1Gi

livenessProbe:

exec:

command: [“mysqladmin”, “ping”]

initialDelaySeconds: 30

periodSeconds: 10

timeoutSeconds: 5

readinessProbe:

exec:

# Check we can execute queries over TCP (skip-networking is off).

command: [“mysql”, “-h”, “127.0.0.1”, “-e”, “SELECT 1”]

initialDelaySeconds: 5

periodSeconds: 2

timeoutSeconds: 1

– name: xtrabackup

image: gcr.io/google-samples/xtrabackup:1.0

ports:

– name: xtrabackup

containerPort: 3307

command:

– bash

– “-c”

– |

set -ex

cd /var/lib/mysql

# Determine binlog position of cloned data, if any.

if [[ -f xtrabackup_slave_info ]]; then

# XtraBackup already generated a partial “CHANGE MASTER TO” query

# because we’re cloning from an existing slave.

mv xtrabackup_slave_info change_master_to.sql.in

# Ignore xtrabackup_binlog_info in this case (it’s useless).

rm -f xtrabackup_binlog_info

elif [[ -f xtrabackup_binlog_info ]]; then

# We’re cloning directly from master. Parse binlog position.

[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1

rm xtrabackup_binlog_info

echo “CHANGE MASTER TO MASTER_LOG_FILE=’${BASH_REMATCH[1]}’,\

MASTER_LOG_POS=${BASH_REMATCH[2]}” > change_master_to.sql.in

fi

# Check if we need to complete a clone by starting replication.

if [[ -f change_master_to.sql.in ]]; then

echo “Waiting for mysqld to be ready (accepting connections)”

until mysql -h 127.0.0.1 -e “SELECT 1”; do sleep 1; done

echo “Initializing replication from clone position”

# In case of container restart, attempt this at-most-once.

mv change_master_to.sql.in change_master_to.sql.orig

mysql -h 127.0.0.1 <<EOF

$(<change_master_to.sql.orig),

MASTER_HOST=’mysql-0.mysql’,

MASTER_USER=’root’,

MASTER_PASSWORD=”,

MASTER_CONNECT_RETRY=10;

START SLAVE;

EOF

fi

# Start a server to send backups when requested by peers.

exec ncat –listen –keep-open –send-only –max-conns=1 3307 -c \

“xtrabackup –backup –slave-info –stream=xbstream –host=127.0.0.1 –user=root”

volumeMounts:

– name: data

mountPath: /var/lib/mysql

subPath: mysql

– name: conf

mountPath: /etc/mysql/conf.d

resources:

requests:

cpu: 100m

memory: 100Mi

volumes:

– name: conf

emptyDir: {}

– name: config-map

configMap:

name: mysql

volumeClaimTemplates:

– metadata:

name: data

spec:

accessModes: [“ReadWriteOnce”]

resources:

requests:

storage: 10Gi

在编写这个YAML中,有几个关键的点

首先在编写的时候,需要针对Pod在整个集群中的不同角色来书写不同的配置和操作

然后需要区分容器重启和容器启动两个不同操作带来的后果

对于需要启动之前的操作,使用initContainer,对于启动后的额外操作,使用sidecar容器

StatefulSet在Deploement的基础上,固定了Pod的拓补关系,固定了Pod的访问方式,保证了PV和Pod对应关系,在Pod删除重建的时候,这些状态都保持不变

但是只能使用相同的镜像去在StatefulSet中使用,如果想要管理不同的应用,可能需要的是Operator

对于上面的StatefulSet来说,其实做成两个statefulSet来处理,会简单一些

所有的读请求,只由 Slave 节点处理;所有的写请求,只由 Master 节点处理。那么,你需要在今天这篇文章的基础上再做哪些改动呢?

对于课后问题来说,我们可以利用重新划分Service的方式来实现

给主节点划分一个mysql-master的service来保证绑定每次访问指访问主节点

将mysql-read改为绑定所有的从节点,就是不知道这样能不能走得通,不行的话,就将主节点和从节点划分为两个StatefulSet

发表评论

邮箱地址不会被公开。 必填项已用*标注