深入剖析Kubernetes學習筆記:StatefulSet-MySQL集群(20)


一、需求描述

1、自然語言來描述

  1. 是一個“主從復制”(Maser-Slave Replication)的 MySQL 集群;

  2. 有 1 個主節點(Master);

  3. 有多個從節點(Slave);

  4. 從節點需要能水平擴展;

  5. 所有的寫操作,只能在主節點上執行;

  6. 讀操作可以在所有節點上執行。

2、圖形描述

 

 

 二、需求分析

1、通過 XtraBackup 將 Master 節點的數據備份到指定目錄。

$ cat xtrabackup_binlog_info
TheMaster-bin.000001     481

2、配置 Slave 節點

Slave 節點在第一次啟動前,需要先把 Master 節點的備份數據,連同備份信息文件,一起拷貝到自己的數據目錄(/var/lib/mysql)下。然后,我們執行這樣一句 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;

3、啟動 Slave 節點

TheSlave|mysql> START SLAVE;

這樣,Slave 節點就啟動了。它會使用備份信息文件中的二進制日志文件和偏移量,與主節點進行數據同步。

4、在這個集群中添加更多的 Slave 節點

需要注意的是,新添加的 Slave 節點的備份數據,來自於已經存在的 Slave 節點

 通過上面的敘述,我們不難看到,將部署 MySQL 集群的流程遷移到 Kubernetes 項目上,需要能夠“容器化”地解決下面的“三座大山”:

  1. Master 節點和 Slave 節點需要有不同的配置文件(即:不同的 my.cnf);

  2. Master 節點和 Salve 節點需要能夠傳輸備份信息文件;

  3. 在 Slave 節點第一次啟動之前,需要執行一些初始化 SQL 操作;

三、第一座大山:Master 節點和 Slave 節點需要有不同的配置文件

1、思路

2、MySQL 的配置文件

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 的配置文件。 

3、ConfigMap

4、兩個 Service 定義

接下來,我們需要創建兩個 Service 來供 StatefulSet 以及用戶使用。這兩個 Service 的定義如下所示:

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

1、可以看到

2、不同點

3、讀寫分離

 

 四、第二座大山:Master 節點和 Salve 節點需要能夠傳輸備份信息文件(大致框架)

思路

 大致的框架

所以首先,我們先為 StatefulSet 對象規划一個大致的框架,如下圖所示:

 

selector

replicas

有狀態應用

管理存儲狀態

 

五、第二座大山:設計template 字段。

1、人格分裂

 2、從 ConfigMap 中,獲取 MySQL 的 Pod 對應的配置文件

為此,我們需要進行一個初始化操作,根據節點的角色是 Master 還是 Slave 節點,為 Pod 分配對應的配置文件。此外,MySQL 還要求集群里的每個節點都有一個唯一的 ID 文件,名叫 server-id.cnf。

而根據我們已經掌握的 Pod 知識,這些初始化操作顯然適合通過 InitContainer 來完成。所以,我們首先定義了一個 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

InitContainer

 其中,文件拷貝的源目錄 /mnt/config-map,正是 ConfigMap 在這個 Pod 的 Volume,如下所示:

      ...
      # template.spec
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mysql

通過這個定義,init-mysql 在聲明了掛載 config-map 這個 Volume 之后,ConfigMap 里保存的內容,就會以文件的方式出現在它的 /mnt/config-map 目錄當中。

3、在 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

在這個名叫 clone-mysql 的 InitContainer 里,我們使用的是 xtrabackup 鏡像(它里面安裝了 xtrabackup 工具)。

做判斷

傳輸數據 

 /var/lib/mysql 目錄,實際上正是一個名為 data 的 PVC,

 一致性狀態

 六、第三座大山:在 Slave 節點第一次啟動之前,需要執行一些初始化 SQL 操作

容器是一個單進程模型。

你可能已經想到了,我們可以為這個 MySQL 容器額外定義一個 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

可以看到,在這個名叫 xtrabackup 的 sidecar 容器的啟動命令里,其實實現了兩部分工作。

第一部分工作,當然是 MySQL 節點的初始化工作

這個初始化需要使用的 SQL,是 sidecar 容器拼裝出來、保存在一個名為 change_master_to.sql.in 的文件里的,具體過程如下所示:

sidecar 容器首先會判斷當前 Pod 的 /var/lib/mysql 目錄下,是否有 xtrabackup_slave_info 這個備份信息文件。

MySQL 節點的初始化流程

 

 接下來,sidecar 容器就可以執行初始化了。從上面的敘述中可以看到,只要這個 change_master_to.sql.in 文件存在

所以,這時候,sidecar 容器只需要讀取並執行 change_master_to.sql.in 里面的“CHANGE MASTER TO”指令,再執行一句 START SLAVE 命令,一個 Slave 節點就被成功啟動了。 

初始化操作完成后

 在完成 MySQL 節點的初始化后,這個 sidecar 容器的第二個工作,則是啟動一個數據傳輸服務。

1、具體做法

2、值得一提

 至此,我們也就翻越了“第三座大山”,完成了 Slave 節點第一次啟動前的初始化工作。

七、定義MySQL容器

扳倒了這“三座大山”后,我們終於可以定義 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

鏡像

 如果 MySQL 容器是 Slave 節點的話

 livenessProbe

 readinessProbe

 至此,一個完整的主從復制模式的 MySQL 集群就定義完了。

八、運行 StatefulSet 

首先,我們需要在 Kubernetes 集群里創建滿足條件的 PV

如果你使用的是我們在第 11 篇文章《從 0 到 1:搭建一個完整的 Kubernetes 集群》里部署的 Kubernetes 集群的話,你可以按照如下方式使用存儲插件 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,調用存儲插件(Rook)創建對應的 PV,從而省去了我們手動創建 PV 的機械勞動。我在后續講解容器存儲的時候,會再詳細介紹這個機制。

備注:在使用 Rook 的情況下,mysql-statefulset.yaml 里的 volumeClaimTemplates 字段需要加上聲明 storageClassName=rook-ceph-block,才能使用到這個 Rook 提供的持久化存儲

然后,我們就可以創建這個 StatefulSet 了,如下所示:

$ kubectl create -f mysql-statefulset.yaml
$ kubectl get pod -l app=mysql
NAME      READY     STATUS    RESTARTS   AGE
mysql-0   2/2       Running   0          2m
mysql-1   2/2       Running   0          1m
mysql-2   2/2       Running   0          1m

可以看到,StatefulSet 啟動成功后,會有三個 Pod 運行。

接下來,我們可以嘗試向這個 MySQL 集群發起請求,執行一些 SQL 操作來驗證它是否正常:

$ kubectl run mysql-client --image=mysql: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.messages VALUES ('hello');
EOF

如上所示,我們通過啟動一個容器,使用 MySQL client 執行了創建數據庫和表、以及插入數據的操作。需要注意的是,我們連接的 MySQL 的地址必須是 mysql-0.mysql(即:Master 節點的 DNS 記錄)。因為,只有 Master 節點才能處理寫操作。

而通過連接 mysql-read 這個 Service,我們就可以用 SQL 進行讀操作,如下所示:

$ 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

在有了 StatefulSet 以后,你就可以像 Deployment 那樣,非常方便地擴展這個 MySQL 集群,比如:

$ kubectl scale statefulset mysql  --replicas=5

這時候,你就會發現新的 Slave Pod mysql-3 和 mysql-4 被自動創建了出來。

而如果你像如下所示的這樣,直接連接 mysql-3.mysql,即 mysql-3 這個 Pod 的 DNS 名字來進行查詢操作:

$ 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

就會看到,從 StatefulSet 為我們新創建的 mysql-3 上,同樣可以讀取到之前插入的記錄。也就是說,我們的數據備份和恢復,都是有效的。

九、總結

1、用一句話總結

2、關鍵點(坑)

人格分裂

閱后即焚

容器之間平等無序

3、最后


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM