本頁展示如何使用 StatefulSet 控制器運行一個有狀態的應用程序。此例是多副本的 MySQL 數據庫。 示例應用的拓撲結構有一個主服務器和多個副本,使用異步的基於行(Row-Based) 的數據復制。
說明:
這不是生產環境下配置。 尤其注意,MySQL 設置都使用的是不安全的默認值,這是因為我們想把重點放在 Kubernetes 中運行有狀態應用程序的一般模式上。
一、准備
你必須擁有一個 Kubernetes 的集群(至少 5 個子節點,1 主 4 子),同時你的 Kubernetes 集群必須帶有 kubectl 命令行工具。
如果你還沒有集群,參考 用 kubeadm 在 Debian 或 Ubuntu 中創建 k8s 集群,不要用 Debian,用 Ubuntu。
要完成本教程,你應該已經熟悉 Pod, Service和 StatefulSet。
您正在使用默認命名空間或不包含任何沖突對象的另一個命名空間。
二、目標
- 使用 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-0
、mysql-1
和 mysql-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.cnf
或 replica.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 TO
和 START 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 102
從 SELECT @@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 103
和 104
開始出現在 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 時釋放下層資源。