部署高可用的MySQL_Kubernetes中文社群
1、MySQL簡介
MySQL 是一個開源的關係型資料庫管理系統,使用標準的sql語言,由瑞典 MySQL AB 公司開發,當前屬於 Oracle 公司。能夠 支援大型的資料庫,可以處理上千萬條的資料記錄。可以運行於在Windows、Linux等多種系統上;支援C、C++、Python、Java、Perl、PHP、Eiffel、Ruby和Tcl等程式語言。對於32位系統,MySQL的表文件最大可支援4GB,對於64位系統,MySQL支援最大的表文件為8TB。
2、MySQL的高可用方案
本文的MySQL高可用方案為主從複製+讀寫分離,即由單一的master和多個slave所構成。其中,客戶端通過master對資料庫進行寫操作,通過slave端進行讀操作。master出現問題後,可以將應用切換到slave端。 此方案是MySQL官方提供的一種高可用解決方案,節點間的資料同步採用MySQL Replication技術。
MySQL Replication從一個MySQL資料庫伺服器(master)的資料複製到一個或多個MySQL資料庫伺服器(slave)。在預設情況下,複製是非同步的;slave不需要一直接收來自主機的更新。根據配置,可以複製資料庫中的所有資料庫、選定的資料庫,或者特定的表。
MySQL中複製的優點包括:
- 擴容解決方案:在多個slave之間擴充套件負載以提高效能。在這種模式下,所有的寫入和更新操作都必須在主伺服器上進行。然而,讀取操作通過slave映象。該模型可以提高寫入操作的效能,同時,也能夠通過增加slave的節點數量,從而顯著地提升讀取速度。
- 資料安全:資料從master被複制到slave,並且slave可以暫停複製過程。因此,可以在不損壞master的情況下,在slave上執行備份服務。
- 分析:現場資料可以在master上建立,而對資訊的分析可以在slave進行,而不影響master的效能。
- 遠端資料分發:可以使用複製為遠端站點建立本地資料的副本,而不必一直通過訪問master。
此高可用的解決方案適用於對資料實時性要求不是特別嚴格的場景,在使用時可以通過廉價的硬體來擴充套件slave的節點數量,將讀壓力分散到多臺slave的機器上面。此方案能夠在很大的程度上解決資料庫讀取資料的壓力瓶頸問題,這是因為在大多的應用系統中,讀壓力要比寫壓力大很多多。
3、安裝部署
3.1 環境要求
- 已有Kubernetes 1.6+環境;
- 在Kubernetes中提供多個(具體數量根據有狀態副本集的個數而定)容量大於10g的持久化儲存卷。
3.2 部署MySql
此示例由一個ConfigMap、兩個Service和一個StatefulSet所組成。
3.2.1 建立ConfigMap
通過YAML檔案建立名為mysql的ConfigMap:
$ kubectl create -f {path}/mysql-configmap.yaml --namespace=kube-public
apiVersion: v1 kind: ConfigMap metadata: name: mysql labels: app: mysql data: master.cnf: | # Apply this config only on the master. [mysqld] log-bin log_bin_trust_function_creators=1 lower_case_table_names=1 slave.cnf: | # Apply this config only on slaves. [mysqld] super-read-only log_bin_trust_function_creators=1
3.2.2 建立Services
通過yaml檔案建立mysql和mysql-read這兩個Service:
$ kubectl create -f {path}/mysql-services.yaml --namespace=kube-public
# 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 --- # Client service for connecting to any MySQL instance for reads. # For writes, you must instead connect to the master: mysql-0.mysql. apiVersion: v1 kind: Service metadata: name: mysql-read labels: app: mysql spec: ports: - name: mysql port: 3306 selector: app: mysql
StatefulSet控制器為Pod建立了一個DNS條目,而Headless服務為DNS條目提供一個主機。因為Headless服務的名稱為mysql,其他Pod通過<pod-name>.mysql訪問此Pod。客戶端訪問被稱為mysql-read,客戶端服務通過訪問mysql-read讀取資料。通過連線myql執行寫入資料的操作。
3.2.3 建立StatefulSet
通過yaml檔案建立名為mysql的StatefulSet:
$ kubectl create -f {path}/mysql-statefulset.yaml --namespace=kube-public
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 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: gcr.io/google-samples/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 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: gcr.io/google-samples/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 ]]; then # XtraBackup already generated a partial "CHANGE MASTER TO" query # because we're cloning from an existing slave. mv xtrabackup_slave_info change_master_to.sql.in # Ignore xtrabackup_binlog_info in this case (it's useless). rm -f 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 xtrabackup_binlog_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" # In case of container restart, attempt this at-most-once. mv change_master_to.sql.in change_master_to.sql.orig 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 # 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
通過執行如下的命令可以檢視啟動過程:
$ kubectl get pods -l app=mysql --watch --namespace=kube-public
在啟動後,應該能夠看到如下的資訊:
4、理解有狀態Pod的初始化
StatefulSet控制器按Pod的序號索引一次啟動一個Pod,控制器每個Pod指派一個唯一的、穩定的名稱,名稱的格式為<statefulset-name>-<ordinal-index>。在此示例中,Pod的名稱為mysql-0,此節點為master主節點;mysql-1和mysql-2,這兩個節點為slave從節點。
4.1 建立配置檔案
在開始啟動Pod規格中的任何容器之前,Pod首先會按照YAML配置中定義的順序執行初始化容器。
第一個初始化容器為init-mysql,將以順序索引建立MySQL配置檔案。
指令碼從Pod名稱的結尾處獲取並確定它的順序索引,順序索引通過hostname命令獲取。然後,它會按照順序儲存在conf.d目錄下的server-id.cnf檔案中。此行為將StatefulSet控制器提供的唯一和穩定的身份標識轉為mysql服務Id的域。在init-mysql容器中,指令碼使用來自於ConfigMap中master.cnf或slave.cnf。
在此例子的拓撲關係中,存在一個MySQL master節點和多個MySQL slave節點,指令碼簡單的指派順序0給主節點。這能夠保證MySQL主節點在建立從節點之前就已經準備就緒。
4.2 克隆已存在的資料
一般來說,當一個新的Pod加入進來作為從節點時,必須假設MySQL master已經有關於它的資料。也假設slave副本的日誌必須重新開始的。這些假設對於StatefulSet的擴縮容是很關鍵。
第二個初始化容器是clone-mysql,它在空的PersistentVolume上執行克隆從節點Pod的行為。這意味著它將從已在執行的Pod中拷貝資料,因此,它的當前狀態能夠與從master開始的副本節點一致。
MySQL自身並沒有提供能夠做到上述能力的機制,因此,此例子使用開源的Percona XtraBackup工具來實現。在克隆的過程中,為了對MySQL主節點影響的最小化,指令碼會要求每一個新的Pod從順序索引值小的Pod中進行克隆。這樣做的原因是,StatefulSet控制器需要一直保證Pod N需要在Pod N+1之前準備就緒。
4.3 啟動副本
在初始化容器完成後,容器將正常執行。MySQL Pod由執行實際mysqld服務的MySQL容器組成,xtrabacekup容器只是作為備份的工具。xtrabackup負責監控克隆資料檔案,並確定是否在從節點初始化MySQL副本。如果需要,它將等待MySQL就緒,然後執行 CHANGE MASTER TO和START SLAVE命令。
一旦一個從節點開始複製,它將記住MySQL master,並自動進行重新連線,因為從節點尋找主節點作為穩定DNS名稱(mysql-0.mysql),它們自動的發現主節點。最後,在啟動副本後,xtrabackup容器也監聽來自於其它Pod對資料克隆的請求。
5、MySQL部署環境驗證
1)通過執行一個臨時的容器(使用mysql:5.7映象),使用MySQL 客戶端傳送測試請求給MySQL master節點(主機名為mysql-0.mysql;跨名稱空間的話,主機名請使用mysql-0.mysql.kube-public)
$ kubectl run mysql-client --image=mysql:5.7 -it --rm --restart=Never -- mysql -h mysql-0.mysql.kube-public
CREATE DATABASE demo; CREATE TABLE demo.messages (message VARCHAR(250)); INSERT INTO demo.messages VALUES ('hello');
在master節點上建立demo資料庫,並建立一個只有message欄位的demo.messages的表,併為message欄位插入hello值。
2)使用主機名為mysql-read來發送測試請求給伺服器:
$ kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never -- mysql -h mysql-read.kube-public
6、擴容slave的數量
1)對於mysql副本,通過新增從節點進行擴容。
$ kubectl scale statefulset mysql --replicas=5 --namespace=kube-public
2)通過下面的命令檢視新的Pod:
$ kubectl get pods -l app=mysql --watch --namespace=kube-public
3)縮容也是無縫的:
$ kubectl scale statefulset mysql --replicas=3 --namespace=kube-public
參考資料
作者簡介:
季向遠,北京神舟航天軟體技術有限公司產品經理。本文版權歸原作者所有。