一、需求描述
1、自然語言來描述
-
是一個“主從復制”(Maser-Slave Replication)的 MySQL 集群;
-
有 1 個主節點(Master);
-
有多個從節點(Slave);
-
從節點需要能水平擴展;
-
所有的寫操作,只能在主節點上執行;
-
讀操作可以在所有節點上執行。
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 項目上,需要能夠“容器化”地解決下面的“三座大山”:
-
Master 節點和 Slave 節點需要有不同的配置文件(即:不同的 my.cnf);
-
Master 節點和 Salve 節點需要能夠傳輸備份信息文件;
-
在 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、關鍵點(坑)
人格分裂
閱后即焚
容器之間平等無序