K8S 有狀態的應用和示例:多副本MySQL


本頁展示如何使用 StatefulSet 控制器運行一個有狀態的應用程序。此例是多副本的 MySQL 數據庫。 示例應用的拓撲結構有一個主服務器和多個副本,使用異步的基於行(Row-Based) 的數據復制。

說明:

這不是生產環境下配置。 尤其注意,MySQL 設置都使用的是不安全的默認值,這是因為我們想把重點放在 Kubernetes 中運行有狀態應用程序的一般模式上。

一、准備

你必須擁有一個 Kubernetes 的集群(至少 5 個子節點,1 主 4 子),同時你的 Kubernetes 集群必須帶有 kubectl 命令行工具。

如果你還沒有集群,參考 用 kubeadm 在 Debian 或 Ubuntu 中創建 k8s 集群不要用 Debian,用 Ubuntu

要完成本教程,你應該已經熟悉 PodServiceStatefulSet

您正在使用默認命名空間或不包含任何沖突對象的另一個命名空間。

二、目標

  • 使用 StatefulSet 控制器部署多副本 MySQL 拓撲架構。
  • 發送 MySQL 客戶端請求
  • 觀察對宕機的抵抗力
  • 擴縮 StatefulSet 的規模

三、教程

1 部署 MySQL

MySQL 示例部署包含一個 ConfigMap、兩個 Service 與一個 StatefulSet。

1.1 ConfigMap

使用以下的 YAML 配置文件創建 ConfigMap :

mysql-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql
  labels:
    app: mysql
data:
  master.cnf: |
    # Apply this config only on the master.
    [mysqld]
    log-bin
    skip_name_resolve
  slave.cnf: |
    # Apply this config only on slaves.
    [mysqld]
    super-read-only
    skip_name_resolve
kubectl apply -f ./mysql-configmap.yaml

這個 ConfigMap 提供 my.cnf 覆蓋設置,使你可以獨立控制 MySQL 主服務器和從服務器的配置。 在這里,你希望主服務器能夠將復制日志提供給副本服務器,並且希望副本服務器拒絕任何不是通過復制進行的寫操作。

ConfigMap 本身沒有什么特別之處,因而也不會出現不同部分應用於不同的 Pod 的情況。 每個 Pod 都會在初始化時基於 StatefulSet 控制器提供的信息決定要查看的部分。

1.2 服務

使用以下 YAML 配置文件創建服務:

mysql-services.yaml

# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None
  selector:
    app: mysql
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv-2
  labels:
    type: local
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/tmp"
apiVersion: apps/v1
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
        args:
          - "--ignore-db-dir=lost+found"
        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: registry.cn-hangzhou.aliyuncs.com/google_samples_thepoy/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
        args:
          - "--ignore-db-dir=lost+found"
        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: registry.cn-hangzhou.aliyuncs.com/google_samples_thepoy/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 && "x$(<xtrabackup_slave_info)" != "x" ]]; then
            # XtraBackup already generated a partial "CHANGE MASTER TO" query
            # because we're cloning from an existing slave. (Need to remove the tailing semicolon!)
            cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
            # Ignore xtrabackup_binlog_info in this case (it's useless).
            rm -f xtrabackup_slave_info 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 -f xtrabackup_binlog_info xtrabackup_slave_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"
            mysql -h 127.0.0.1 \
                  -e "$(<change_master_to.sql.in), \
                          MASTER_HOST='mysql-0.mysql', \
                          MASTER_USER='root', \
                          MASTER_PASSWORD='', \
                          MASTER_CONNECT_RETRY=10; \
                        START SLAVE;" || exit 1
            # In case of container restart, attempt this at-most-once.
            mv change_master_to.sql.in change_master_to.sql.orig
          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

要使用 MySQL 5.6 鏡像,不然在初始化時會出錯。如果一定要使用 5.7 鏡像,需要添加參數"--ignore-db-dir=lost+found"

name: mysql
image: mysql:5.7
args:
  - "--ignore-db-dir=lost+found"
kubectl apply -f ./mysql-statefulset.yaml

你可以通過運行以下命令查看啟動進度:

kubectl get pods -l app=mysql --watch

一段時間后,你應該看到 3 個 Pod 都進入了 Running 狀態:

NAME      READY   STATUS    RESTARTS   AGE
mysql-0   2/2     Running   0          15m
mysql-1   2/2     Running   0          15m
mysql-2   2/2     Running   0          15m

3 個副本分別運行在一個子節點上

2 了解有狀態的 Pod 初始化

StatefulSet 控制器按序數索引順序地啟動每一個 Pod。 它一直等待前一個 Pod 准備好后才啟動下一個 Pod。

此外,控制器為每個 Pod 分配一個唯一、穩定的名稱,形如 <statefulset 名稱>-<序數索引>, 其結果是 Pods 名為 mysql-0mysql-1mysql-2

上述 StatefulSet 清單中的 Pod 模板利用這些屬性來執行 MySQL 副本的有序啟動。

2.1 生成配置

在啟動 Pod 規約中的任何容器之前,Pod 首先按順序運行所有的 Init 容器

第一個名為 init-mysql 的 Init 容器根據序號索引生成特殊的 MySQL 配置文件。

該腳本通過從 Pod 名稱的末尾提取索引來確定自己的序號索引,而 Pod 名稱由 hostname 命令返回。 然后將序數(帶有數字偏移量以避免保留值)保存到 MySQL conf.d 目錄中的文件 server-id.cnf。 這一操作將 StatefulSet 所提供的唯一、穩定的標識轉換為 MySQL 服務器的 ID, 而這些 ID 也是需要唯一性、穩定性保證的。

通過將生成的配置文件復制到 conf.d 目錄中,init-mysql 容器中的腳本也可以應用 ConfigMap 中的 primary.cnfreplica.cnf。 由於示例部署結構由單個 MySQL 主節點和任意數量的副本節點組成, 因此腳本僅將序數 0 指定為主節點,而將其他所有節點指定為副本節點。

與 StatefulSet 控制器的 部署順序保證 相結合, 可以確保 MySQL 主服務器在創建副本服務器之前已准備就緒,以便它們可以開始復制。

2.2 克隆現有數據

通常,當新 Pod 作為副本節點加入集合時,必須假定 MySQL 主節點可能已經有數據。 還必須假設復制日志可能不會一直追溯到時間的開始。

這些保守的假設是允許正在運行的 StatefulSet 隨時間擴大和縮小而不是固定在其初始大小的關鍵。

第二個名為 clone-mysql 的 Init 容器,第一次在帶有空 PersistentVolume 的副本 Pod 上啟動時,會在從屬 Pod 上執行克隆操作。 這意味着它將從另一個運行中的 Pod 復制所有現有數據,使此其本地狀態足夠一致, 從而可以開始從主服務器復制。

MySQL 本身不提供執行此操作的機制,因此本示例使用了一種流行的開源工具 Percona XtraBackup。 在克隆期間,源 MySQL 服務器性能可能會受到影響。 為了最大程度地減少對 MySQL 主服務器的影響,該腳本指示每個 Pod 從序號較低的 Pod 中克隆。 可以這樣做的原因是 StatefulSet 控制器始終確保在啟動 Pod N + 1 之前 Pod N 已准備就緒。

2.3 開始復制

Init 容器成功完成后,應用容器將運行。 MySQL Pod 由運行實際 mysqld 服務的 mysql 容器和充當 輔助工具 的 xtrabackup 容器組成。

xtrabackup sidecar 容器查看克隆的數據文件,並確定是否有必要在副本服務器上初始化 MySQL 復制。 如果是這樣,它將等待 mysqld 准備就緒,然后使用從 XtraBackup 克隆文件中提取的復制參數 執行 CHANGE MASTER TOSTART SLAVE 命令。

一旦副本服務器開始復制后,它會記住其 MySQL 主服務器,並且如果服務器重新啟動或 連接中斷也會自動重新連接。 另外,因為副本服務器會以其穩定的 DNS 名稱查找主服務器(mysql-0.mysql), 即使由於重新調度而獲得新的 Pod IP,它們也會自動找到主服務器。

最后,開始復制后,xtrabackup 容器監聽來自其他 Pod 的連接,處理其數據克隆請求。 如果 StatefulSet 擴大規模,或者下一個 Pod 失去其 PersistentVolumeClaim 並需要重新克隆, 則此服務器將無限期保持運行。

3 發送客戶端請求

你可以通過運行帶有 mysql:5.7 鏡像的臨時容器並運行 mysql 命令, 將測試查詢發送到 MySQL 主服務器(主機名 mysql-0.mysql):

kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
  mysql -h mysql-0.mysql <<EOF
CREATE DATABASE IF NOT EXISTS test;
CREATE TABLE IF NOT EXISTS test.messages (message VARCHAR(250));
INSERT INTO test.messages VALUES ('hello');
EOF

使用主機名 mysql-read 將測試查詢發送到任何報告為就緒的服務器:

kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  mysql -h mysql-read -e "SELECT * FROM test.messages"

你應該能看到下面的輸出:

+---------+
| message |
+---------+
| hello   |
+---------+
pod "mysql-client" deleted

為了演示 mysql-read 服務在服務器之間分配連接,你可以在循環中運行 SELECT @@server_id

+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2021-05-27 03:53:38 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2021-05-27 03:53:39 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2021-05-27 03:53:40 |
+-------------+---------------------+
+-------------+---------------------+

要停止循環時可以按 Ctrl+C ,但是讓它在另一個窗口中運行非常有用, 這樣你就可以看到以下步驟的效果。

4 模擬 Pod 和 Node 的宕機時間

為了證明從副本節點緩存而不是單個服務器讀取數據的可用性提高,請在使 Pod 退出 Ready 狀態時,保持上述 SELECT @@server_id 循環一直運行。

4.1 破壞就緒態探測

mysql 容器的 就緒態探測 運行命令 mysql -h 127.0.0.1 -e 'SELECT 1',以確保服務器已啟動並能夠執行查詢。

迫使就緒態探測失敗的一種方法就是中止該命令:

kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off

此命令會進入 Pod mysql-2 的實際容器文件系統,重命名 mysql 命令,導致就緒態探測無法找到它。 幾秒鍾后, Pod 會報告其中一個容器未就緒。你可以通過運行以下命令進行檢查:

kubectl get pod mysql-2

READY 列中查找 1/2

NAME      READY     STATUS    RESTARTS   AGE
mysql-2   1/2       Running   0          3m

此時,你應該會看到 SELECT @@server_id 循環繼續運行,但不再報告 102。 回想一下,init-mysql 腳本將 server-id 定義為 100 + $ordinal, 因此服務器 ID 102 對應於 Pod mysql-2

現在修復 Pod,幾秒鍾后它應該重新出現在循環輸出中:

kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql.off /usr/bin/mysql

4.2 刪除 Pods

如果刪除了 Pod,則 StatefulSet 還會重新創建 Pod,類似於 ReplicaSet 對無狀態 Pod 所做的操作。

kubectl delete pod mysql-2

StatefulSet 控制器注意到不再存在 mysql-2 Pod,於是創建一個具有相同名稱並鏈接到相同 PersistentVolumeClaim 的新 Pod。 你應該看到服務器 ID 102 從循環輸出中消失了一段時間,然后又自行出現。

4.3 節點宕機

如果你的 Kubernetes 集群具有多個節點,則可以通過發出以下 drain 命令來模擬節點停機(就好像節點在被升級)。

或者將節點關機。

首先確定 MySQL Pod 之一在哪個節點上:

kubectl get pod mysql-2 -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP           NODE         NOMINATED NODE   READINESS GATES
mysql-2   2/2     Running   0          49m   10.244.3.2   k8s-node-2   <none>           <none>

如果將節點關機,則不需要執行下面的命令。

然后通過運行以下命令騰空節點,該命令將其保護起來,以使新的 Pod 不能調度到該節點, 然后逐出所有現有的 Pod。將 <節點名稱> 替換為在上一步中找到的節點名稱。

這可能會影響節點上的其他應用程序,因此最好 僅在測試集群中執行此操作

kubectl drain <節點名稱> --force --delete-local-data --ignore-daemonsets

現在,你可以看到 Pod 被重新調度到其他節點上:

kubectl get pod mysql-2 -o wide --watch

響應類似:

mysql-0   0/2     Terminating   4          65m   10.244.3.10   k8s-node-3 
...
mysql-0   0/2     Pending       0          0s    <none>        <none>    
mysql-0   0/2     Pending       0          0s    <none>        k8s-node-4 
mysql-0   0/2     Init:0/2      0          0s    <none>        k8s-node-4 
mysql-0   0/2     Init:1/2      0          30m   10.244.4.2    k8s-node-4 
mysql-0   0/2     PodInitializing   0          30m   10.244.4.2    k8s-node-4 
mysql-0   1/2     Running           0          30m   10.244.4.2    k8s-node-4 
mysql-0   2/2     Running           0          30m   10.244.4.2    k8s-node-4 

再次,你應該看到服務器 ID 102SELECT @@server_id 循環輸出 中消失一段時間,然后自行出現。

現在去掉節點保護(Uncordon),使其恢復為正常模式:

kubectl uncordon <節點名稱>

5 擴展副本節點數量

擴展副本需要先添加對應數量的 PV,如果之前使用的是 StorageClass,則不需要進行此操作。

使用 MySQL 復制,你可以通過添加副本節點來擴展讀取查詢的能力。 使用 StatefulSet,你可以使用單個命令執行此操作:

kubectl scale statefulset mysql --replicas=5

查看新的 Pod 的運行情況:

kubectl get pods -l app=mysql --watch

一旦 Pod 啟動,你應該看到服務器 IDs 103104 開始出現在 SELECT @@server_id 循環輸出中。

你還可以驗證這些新服務器在存在之前已添加了數據:

kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  mysql -h mysql-3.mysql -e "SELECT * FROM test.messages"
+---------+
| message |
+---------+
| hello   |
+---------+
pod "mysql-client" deleted

向下壓縮操作也是很簡單:

kubectl scale statefulset mysql --replicas=3

但是請注意,如果你用的是 StorageClass,按比例擴大會自動創建新的 PersistentVolumeClaims,而按比例縮小不會自動刪除這些 PVC。 這使你可以選擇保留那些初始化的 PVC,以更快地進行縮放,或者在刪除它們之前提取數據。

你可以通過運行以下命令查看此信息:

kubectl get pvc -l app=mysql

這表明,盡管將 StatefulSet 縮小為3,所有5個 PVC 仍然存在:

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
data-mysql-0   Bound     pvc-8acbf5dc-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-1   Bound     pvc-8ad39820-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-2   Bound     pvc-8ad69a6d-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-3   Bound     pvc-50043c45-b1c5-11e6-93fa-42010a800002   10Gi       RWO           2m
data-mysql-4   Bound     pvc-500a9957-b1c5-11e6-93fa-42010a800002   10Gi       RWO           2m

如果你不打算重復使用多余的 PVC,則可以刪除它們:

kubectl delete pvc data-mysql-3
kubectl delete pvc data-mysql-4

6 刪除本示例

通過在終端上按 Ctrl+C 取消 SELECT @@server_id 循環,或從另一個終端運行以下命令:

kubectl delete pod mysql-client-loop --now

刪除 StatefulSet。這也會開始終止 Pod。

kubectl delete statefulset mysql

驗證 Pod 消失。他們可能需要一些時間才能完成終止。

kubectl get pods -l app=mysql

刪除 ConfigMap、Services 和 PersistentVolumeClaims。

kubectl delete configmap,service,pvc -l app=mysql

如果你是手動創建的 PersistentVolume,則還需要手動刪除它們,並釋放下層資源。 如果你使用了動態預配器,當得知你刪除 PersistentVolumeClaims 時,它將自動刪除 PersistentVolumes。 一些動態預配器(例如用於 EBS 和 PD 的預配器)也會在刪除 PersistentVolumes 時釋放下層資源。


免責聲明!

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



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