mysql on k8s statefulset部署实践

Posted _雪辉_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mysql on k8s statefulset部署实践相关的知识,希望对你有一定的参考价值。

文章目录

一、mysql on k8s 背景介绍

  一般情况下Kubernetes可以通过ReplicaSet以一个Pod模板创建多个Pod副本,但是它们都是无状态的,任何时候它们都可以被一个全新的Pod替换。然而有状态的Pod需要另外的方案确保当一个有状态的Pod挂掉后,这个Pod实例需要在别的节点上重建,但是新的实例必须与被替换的实例拥有相同的名称、网络标识和状态。这就是StatefulSet管理Pod的手段。对于容器集群,有状态服务的挑战在于,通常集群中的任何节点都并非100%可靠的,服务所需的资源也会动态地更新改变。当节点由于故障或服务由于需要更多的资源而无法继续运行在原有节点上时,集群管理系统会为该服务重新分配一个新的运行位置,从而确保从整体上看,集群对外的服务不会中断。

  • 搭建一个主从复制(Master-Slave)的MySQL集群
  • 从节点可以水平扩展
  • 所有的写操作只能在MySQL主节点上执行
  • 读操作可以在MySQL主从节点上执行
  • 从节点能同步主节点的数据

二、mysql on k8s部署实践


存储配置忽略,可以使用云盘,本地盘,由k8s集群维护者配置

2.1 创建namespace

mysql-namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: mysql
  labels:
    app: mysql

kubectl apply -f mysql-namespace.yaml

2.2 创建configmap

  给主从节点分别准备两份不同的 MySQL 配置文件,然后根据 Pod 的序号(Index)挂载进去即可。
mysql-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql
  namespace: mysql
  labels:
    app: mysql
data:
  master.cnf: |
    # Master配置
    [mysqld]
    log-bin=mysql_bin
    skip-name-resolve
  slave.cnf: |
    # Slave配置
    [mysqld]
    read-only
    skip-name-resolve
    log-bin=mysql-bin

kubectl apply -f mysql-configmap.yaml

2.3 创建secret

mysql-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
  namespace: mysql
  labels:
    app: mysql
type: Opaque
data:
  password: MTIzNDU2 # echo -n "123456" | base64

kubectl apply -f mysql-secret.yaml

2.4 创建service

mysql-services.yaml

apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: mysql
  labels:
    app: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None
  selector:
    app: mysql
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
  namespace: mysql
  labels:
    app: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql

 &#8195第一个名叫mysql的 Service 是一个 Headless Service(即:clusterIP= None)。它的作用是通过为 Pod 分配 DNS 记录来固定它的拓扑状态,比如“mysql-0.mysql”和“mysql-1.mysql”这样的 DNS 名字。其中,编号为 0 的节点就是我们的主节点。
 &#8195第二个名叫“mysql-read”的 Service,则是一个常规的 Service。并且我们规定,所有用户的读请求,都必须访问第二个 Service 被自动分配的 DNS 记录。这样,读请求就可以被转发到任意一个 MySQL 的主节点或者从节点上。

2.5 创建statefulset

mysql-statefulset.yaml

piVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: mysql
  labels:
    app: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        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目录下
          # 否则,拷贝ConfigMap里的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
      - name: clone-mysql
        image: registry.cn-shenzhen.aliyuncs.com/jbjb/csi:xtrabackup-1.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        command:
        - bash
        - "-c"
        - |
          set -ex
          # 拷贝操作只需要在第一次启动时进行,所以数据已经存在则跳过
          [[ -d /var/lib/mysql/mysql ]] && exit 0
          # Master 节点(序号为 0)不需要这个操作
          [[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
          ordinal=$BASH_REMATCH[1]
          [[ $ordinal == 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
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        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", "-uroot", "-p$MYSQL_ROOT_PASSWORD"]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command: ["mysqladmin", "ping", "-uroot", "-p$MYSQL_ROOT_PASSWORD"]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
      - name: xtrabackup
        image:registry.cn-shenzhen.aliyuncs.com/jbjb/csi:xtrabackup-1.0
        ports:
        - name: xtrabackup
          containerPort: 3307
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        command:
        - bash
        - "-c"
        - |
          set -ex
          cd /var/lib/mysql
          # 从备份信息文件里读取MASTER_LOG_FILE和MASTER_LOG_POS这2个字段的值,用来拼装集群初始化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_info文件,说明备份来自于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 -uroot -p$MYSQL_ROOT_PASSWORD -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 -uroot -p$MYSQL_ROOT_PASSWORD << EOF
          $(< change_master_to.sql.orig),
            MASTER_HOST='mysql-0.mysql.mysql',
            MASTER_USER='root',
            MASTER_PASSWORD='$MYSQL_ROOT_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 --password=$MYSQL_ROOT_PASSWORD"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: conf
        emptyDir: 
      - name: config-map
        configMap:
          name: mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - "ReadWriteOnce"
      storageClassName: cbs
      resources:
        requests:
          storage: 10Gi

  这一步是mysql在k8s集群中部署中的关键,主要有以下步骤:

  • 从 Pod 的 hostname 里,读取到Pod 的序号,以此作为 MySQL 节点的 server-id。然后,init-mysql 通过这个序号,判断当前 Pod 到底是 Master 节点(即:序号为 0)还是 Slave 节点(即:序号不为 0),从而把对应的配置文件从 /mnt/config-map 目录拷贝到 /mnt/conf.d/ 目录下。
  • 当初始化所需的数据(/var/lib/mysql/mysql 目录)已经存在,或者当前 Pod 是 Master 节点的时候,不需要做拷贝操作。接下来,clone-mysql 会使用 Linux 自带的 ncat 指令,向 DNS 记录为“mysql-< 当前序号减一 >.mysql”的 Pod,也就是当前 Pod 的前一个 Pod,发起数据传输请求,并且直接用 xbstream 指令将收到的备份数据保存在 /var/lib/mysql 目录下。
  • 在这个名叫 xtrabackup 的 sidecar 容器的启动命令里,首先会判断当前 Pod 的 /var/lib/mysql 目录下,是否有 xtrabackup_slave_info 这个备份信息文件。如果有,则说明这个目录下的备份数据是由一个 Slave 节点生成的。这种情况下,XtraBackup 工具在备份的时候,就已经在这个文件里自动生成了"CHANGE MASTER TO" SQL 语句。所以,我们只需要把这个文件重命名为 change_master_to.sql.in,后面直接使用即可。如果没有 xtrabackup_slave_info 文件、但是存在 xtrabackup_binlog_info 文件,那就说明备份数据来自于 Master 节点。这种情况下,sidecar 容器就需要解析这个备份信息文件,读取 MASTER_LOG_FILE 和 MASTER_LOG_POS 这两个字段的值,用它们拼装出初始化 SQL 语句,然后把这句 SQL 写入到 change_master_to.sql.in 文件中。,这时候,sidecar 容器只需要读取并执行 change_master_to.sql.in 里面的“CHANGE MASTER TO”指令,再执行一句 START SLAVE 命令,一个 Slave 节点就被成功启动了。在完成 MySQL 节点的初始化后,sidecar 容器会使用 ncat 命令启动一个工作在 3307 端口上的网络发送服务。一旦收到数据传输请求时,sidecar 容器就会调用 xtrabackup --backup 指令备份当前 MySQL 的数据,然后把这些备份数据返回给请求者。
  • 另外,我们为它定义了一个 livenessProbe,通过 mysqladmin ping 命令来检查它是否健康;还定义了一个 readinessProbe,通过查询 SQL(select 1)来检查 MySQL 服务是否可用。当然,凡是 readinessProbe 检查失败的 MySQL Pod,都会从 Service 里被摘除掉。

2.6 扩容从节点

kubectl scale statefulset mysql --replicas=5

三、总结

  在实际环境中,还需要考虑很多情况,比如说:

  • 依靠k8s自身的拉起是否能提供生产需要的高可用
  • headless service 在集群外部无法访问,pod ip重启后会改变,业务连接数据库是否能通过service name的方式连接
  • 实际mysql有很多的生态工具,是否能在k8s环境中适配

Kubernetes:使用 MySQL 容器创建 StatefulSet 时出错

【中文标题】Kubernetes:使用 MySQL 容器创建 StatefulSet 时出错【英文标题】:Kubernetes: Error when creating a StatefulSet with a MySQL container 【发布时间】:2019-04-10 21:13:27 【问题描述】:

早上好,

我是 Docker 和 Kubernetes 的新手,我真的不知道从哪里开始寻求帮助。我用 Docker 创建了一个数据库容器,我想用 Kubernetes 对其进行管理和扩展。我开始在我的机器上安装 minikube,并尝试先创建一个 Deployment,然后为数据库容器创建一个 StatefulSet。但是在使用数据库(mariadb 或 mysql)创建 Pod 时,我遇到了 StatefulSet 的问题。当我使用部署时,Pod 已加载并且工作正常。但是,当在 StatefulSet 中使用它们时,相同的 Pod 无法正常工作,返回询问 MYSQL 常量的错误。这是部署,我使用命令kubectl create -f deployment.yaml

apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: mydb-deployment
spec:
 template:
  metadata:
   labels: 
    name: mydb-pod
  spec:
   containers:
    - name: mydb
      image: ignasiet/aravomysql
      ports:
       - containerPort: 3306

列出部署时:kubectl get Deployments:

NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
mydb-deployment    1         1         1            1           2m

还有豆荚:kubectl get pods:

NAME                                READY   STATUS    RESTARTS   AGE
mydb-deployment-59c867c49d-4rslh    1/1     Running   0          50s

但由于我想创建一个持久化数据库,我尝试使用相同的容器和一个持久化卷创建一个 statefulSet 对象。 因此,当使用kubectl create -f statefulset.yaml 创建以下 StatefulSet 时:

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
 name: statefulset-mydb
spec:
 serviceName: mydb-pod
 template:
  metadata:
   labels: 
    name: mydb-pod
  spec:
   containers:
    - name: aravo-database
      image: ignasiet/aravomysql
      ports:
       - containerPort: 3306
      volumeMounts:
       - name: volume-mydb
         mountPath: /var/lib/mysql
   volumes:
    - name: volume-mydb
      persistentVolumeClaim: 
       claimName: config-mydb

使用服务kubectl create -f service-db.yaml

apiVersion: v1
kind: Service
metadata:
 name: mydb
spec:
 type: ClusterIP
 ports:
  - port: 3306
 selector:
  name: mydb-pod

还有权限文件kubectl create -f permissions.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: config-mydb
spec:
 accessModes: 
  - ReadWriteOnce
 resources:
  requests:
   storage: 3Gi

吊舱不工作。他们给出了一个错误:

NAME                    READY   STATUS             RESTARTS   AGE
statefulset-mydb-0      0/1     CrashLoopBackOff   1          37s

并且在分析日志时 kubectl logs statefulset-mydb-0:

`error: database is uninitialized and password option is not specified
You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD`

当容器已经有一个初始化脚本并且工作正常时,它怎么可能要求这些变量?为什么它只在作为 statefulSet 启动时询问,而不是在启动 Deployment 时询问?

提前致谢。

【问题讨论】:

您的初始化脚本是 Dockerfile 的一部分吗?我的意思是你在构建 docker 镜像时运行这个脚本。 是的,我在/docker-entrypoint-initdb.d 中添加了一个script.sql 我还添加了一个MYSQL_ROOT_PASSWORD,现在它正在运行。 我添加了一个答案来解释这里发生了什么。请耐心阅读完整答案。我希望它能让你理解为什么你会看到这种奇怪的行为。 【参考方案1】:

我提取了您的图片ignasiet/aravomysql 试图找出问题所在。事实证明,您的图像已经在/var/lib/mysql 有一个初始化的 MySQL 数据目录:

$ docker run -it --rm --entrypoint=sh ignasiet/aravomysql:latest
# ls -al /var/lib/mysql 
total 110616
drwxr-xr-x 1 mysql mysql      240 Nov  7 13:19 .
drwxr-xr-x 1 root  root        52 Oct 29 18:19 ..
-rw-rw---- 1 root  root     16384 Oct 29 18:18 aria_log.00000001
-rw-rw---- 1 root  root        52 Oct 29 18:18 aria_log_control
-rw-rw---- 1 root  root      1014 Oct 29 18:18 ib_buffer_pool
-rw-rw---- 1 root  root  50331648 Oct 29 18:18 ib_logfile0
-rw-rw---- 1 root  root  50331648 Oct 29 18:18 ib_logfile1
-rw-rw---- 1 root  root  12582912 Oct 29 18:18 ibdata1
-rw-rw---- 1 root  root         0 Oct 29 18:18 multi-master.info
drwx------ 1 root  root      2696 Nov  7 13:19 mysql
drwx------ 1 root  root        12 Nov  7 13:19 performance_schema
drwx------ 1 root  root        48 Nov  7 13:19 yypy

但是,当将 PersistentVolume 或简单的 Docker 卷挂载到 /var/lib/mysql 时,它最初是空的,因此脚本认为您的数据库未初始化。您可以通过以下方式重现此问题:

$ docker run -it --rm --mount type=tmpfs,destination=/var/lib/mysql ignasiet/aravomysql:latest
error: database is uninitialized and password option is not specified 
  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD

如果您需要运行大量脚本来初始化数据库,您有两种选择:

    根据mysql Dockerfile 创建一个Dockerfile,并在/docker-entrypoint-initdb.d 中添加shell 脚本或SQL 脚本。更多详细信息可在“初始化新实例”下的here 中找到。 在 PodTemplateSpec 中使用 initContainers 属性,类似于:
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: statefulset-mydb
spec:
  serviceName: mydb-pod
  template:
  metadata:
    labels: 
    name: mydb-pod
  spec:
    containers:
    - name: aravo-database
      image: ignasiet/aravomysql
      ports:
        - containerPort: 3306
      volumeMounts:
        - name: volume-mydb
          mountPath: /var/lib/mysql
    initContainers:
    - name: aravo-database-init
      command:
        - /script/to/initialize/database
      image: ignasiet/aravomysql
      volumeMounts:
        - name: volume-mydb
          mountPath: /var/lib/mysql
    volumes:
    - name: volume-mydb
      persistentVolumeClaim: 
        claimName: config-mydb

【讨论】:

感谢您的回答。我尝试了第二种方法,使用 initContainers: command: - /docker-entrypoint-initdb.d/script.sql 但是它在初始化 Pod 时出错:Error from server (BadRequest): container "aravo-database" in pod "statefulset-aravodb-0" is waiting to start: PodInitializing @Ignasi 对不起,应该更清楚。我在第二种方法中指的“脚本”类似于 shell 脚本,而不是 SQL 脚本。无论如何,如果您在/docker-entrypoint-initdb.d 中有脚本,则根本不需要initContainers,但您需要提供MYSQL_ROOT_PASSWORDMYSQL_ALLOW_EMPTY_PASSWORDMYSQL_RANDOM_ROOT_PASSWORD 谢谢,它成功了。我将脚本放在那个文件夹中,并添加了一个 MYSQL_ROOT_PASSWORD。【参考方案2】:

您面临的问题并非特定于 StatefulSet。这是因为持久的音量。如果你在没有持久卷的情况下使用 StatefulSet,你就不会遇到这个问题。或者,如果您将 Deployment 与持久卷一起使用,您将面临这个问题。

为什么?好的,让我解释一下。

设置这些环境变量MYSQL_ROOT_PASSWORDMYSQL_ALLOW_EMPTY_PASSWORDMYSQL_RANDOM_ROOT_PASSWORD 之一是创建新数据库所必需的。阅读环境变量部分here。

但是,如果您从脚本初始化数据库,则不需要提供它。看docker-entrypont.shhere这一行。它检查/var/lib/mysql 目录中是否已经存在数据库。如果没有,它将尝试创建一个。如果您不提供任何指定的环境变量,那么它将给出您得到的错误。但是,如果它已经在那里找到了一个数据库,它将不会尝试创建一个,您也不会看到错误。

现在的问题是,你已经初始化了数据库,那为什么还抱怨环境变量呢?

在这里,持久音量开始发挥作用。由于您已将持久卷安装在 /var/lib/mysql 目录中,现在该目录指向您当前为空的持久卷。因此,当您的容器运行docker-entrypoint.sh 脚本时,它在/var/lib/mysql 目录上找不到任何数据库,因为它现在指向持久卷而不是您的docker 映像的原始/var/lib/mysql 目录,该目录已在此目录上初始化数据库。因此,它会尝试创建一个新数据库,并会因为您没有提供MYSQL_ROOT_PASSWORD 环境变量而报错。

当您不使用任何持久卷时,您的/var/lib/mysql 目录指向包含初始化数据库的原始目录。所以,你看不到错误。

那么,如何正确初始化mysql数据库呢?

为了从脚本初始化 MySQL,您只需要将脚本放入/docker-entrypoint-initdb.d。只需使用 vanilla mysql 映像,将初始化脚本放入卷中,然后将卷挂载到 /docker-entrypoint-initdb.d 目录。 MySQL 将被初始化。

查看此答案以获取有关如何从脚本初始化的详细信息:https://***.com/a/45682775/7695859

【讨论】:

谢谢!根据您的回答,我安装了两个具有不同路径的卷:一个用于存储mountPath: /var/lib/mysql,带有PersistenVolumeClaim,另一个用于初始化/docker-entrypoint-initdb.d,带有ConfigMap。它似乎正在工作,当 Pod 死亡时,信息是持久的。

以上是关于mysql on k8s statefulset部署实践的主要内容,如果未能解决你的问题,请参考以下文章

K8S statefulset 详解

K8S使用Statefulset管理集群pod模式

k8s 控制器-Statefulset

k8s集群StatefulSets的Pod调度查询丢失问题?

k8s集群StatefulSets的Pod优雅调度问题思考

k8s之StatefulSet