在Kubernetes上運行高可用的WordPress和MySQL
WordPress是用於編輯和發布Web內容的主流平台。在本教程中,我將逐步介紹如何使用Kubernetes來構建高可用性(HA)WordPress部署。
WordPress由兩個主要組件組成:WordPress PHP伺服器和用於存儲用戶信息、帖子和網站數據的資料庫。我們需要讓整個應用程序中這兩個組件在高可用的同時都具備容錯能力。
在硬體和地址發生變化的時候,運行高可用服務可能會很困難:非常難維護。藉助Kubernetes以及其強大的網路組件,我們可以部署高可用的WordPress站點和MySQL資料庫,而無需(幾乎無需)輸入單個IP地址。
在本教程中,我將向你展示如何在Kubernetes中創建存儲類、服務、配置映射和集合,如何運行高可用MySQL,以及如何將高可用WordPress集群掛載到資料庫服務上。如果你還沒有Kubernetes集群,你可以在Amazon、Google或者Azure上輕鬆找到並且啟動它們,或者在任意的伺服器上使用Rancher Kubernetes Engine (RKE)
架構概述
現在我來簡要介紹一下我們將要使用的技術及其功能:
WordPress應用程序文件的存儲:具有GCE持久性磁碟備份的NFS存儲
資料庫集群:帶有用於奇偶校驗的xtrabackup的MySQL
應用程序級別:掛載到NFS存儲的WordPress DockerHub映像
負載均衡和網路:基於Kubernetes的負載均衡器和服務網路
該體系架構如下所示:
在K8s中創建存儲類、服務和配置映射
在Kubernetes中,狀態集提供了一種定義pod初始化順序的方法。我們將使用一個有狀態的MySQL集合,因為它能確保我們的數據節點有足夠的時間在啟動時複製先前pods中的記錄。我們配置這個狀態集的方式可以讓MySQL主機在其他附屬機器之前先啟動,因此當我們擴展時,可以直接從主機將克隆發送到附屬機器上。
首先,我們需要創建一個持久卷存儲類和配置映射,以根據需要應用主從配置。我們使用持久卷,避免資料庫中的數據受限於集群中任何特定的pods。這種方式可以避免資料庫在MySQL主機pod丟失的情況下丟失數據,當主機pod丟失時,它可以重新連接到帶xtrabackup的附屬機器,並將數據從附屬機器拷貝到主機中。MySQL的複製負責主機-附屬的複製,而xtrabackup負責附屬-主機的複製。
要動態分配持久卷,我們使用GCE持久磁碟創建存儲類。不過,Kubernetes提供了各種持久性卷的存儲方案:
# storage-class.yamlkind: StorageClassapiVersion: storage.k8s.io/v1metadata:
name: slowprovisioner: kubernetes.io/gce-pdparameters:
type: pd-standard zone: us-central1-a
創建類,並且使用指令:$ kubectl create -f storage-class.yaml部署它。
接下來,我們將創建configmap,它指定了一些在MySQL配置文件中設置的變數。這些不同的配置由pod本身選擇有關,但它們也為我們提供了一種便捷的方式來管理潛在的配置變數。
創建名為mysql-configmap.yaml的YAML文件來處理配置,如下:
# mysql-configmap.yamlapiVersion: v1kind: ConfigMapmetadata:
name: mysql labels:
app: mysqldata:
master.cnf: | # Apply this config only on the master.
[mysqld]
log-bin
skip-host-cache
skip-name-resolve slave.cnf: | # Apply this config only on slaves.
[mysqld]
skip-host-cache
skip-name-resolve
創建configmap並使用指令:$ kubectl create -f mysql-configmap.yaml
來部署它。
接下來我們要設置服務以便MySQL pods可以互相通信,並且我們的WordPress pod可以使用mysql-services.yaml與MySQL通信。這也為MySQL服務啟動了服務負載均衡器。
# mysql-services.yaml# Headless service for stable DNS entries of StatefulSet members.apiVersion: v1kind: Servicemetadata:
name: mysql labels:
app: mysqlspec:
ports:
- name: mysql port: 3306 clusterIP: None selector:
app: mysql
通過此服務聲明,我們就為實現一個多寫入、多讀取的MySQL實例集群奠定了基礎。這種配置是必要的,每個WordPress實例都可能寫入資料庫,所以每個節點都必須準備好讀寫。
執行命令$ kubectl create -f mysql-services.yaml來創建上述的服務。
到這為止,我們創建了卷聲明存儲類,它將持久磁碟交給所有請求它們的容器,我們配置了configmap,在MySQL配置文件中設置了一些變數,並且我們配置了一個網路層服務,負責對MySQL伺服器請求的負載均衡。上面說的這些只是準備有狀態集的框架, MySQL伺服器實際在哪裡運行,我們接下來將繼續探討。
配置有狀態集的MySQL
本節中,我們將編寫一個YAML配置文件應用於使用了狀態集的MySQL實例。
我們先定義我們的狀態集:
1,創建三個pods並將它們註冊到MySQL服務上。
2,按照下列模版定義每個pod:
?為主機MySQL伺服器創建初始化容器,命名為init-mysql.
?給這個容器使用mysql:5.7鏡像
?運行一個bash腳本來啟動xtrabackup
?為配置文件和configmap掛載兩個新卷
3,為主機MySQL伺服器創建初始化容器,命名為clone-mysql.
?為該容器使用Google Cloud Registry的xtrabackup:1.0鏡像
?運行bash腳本來克隆上一個同級的現有xtrabackups
?為數據和配置文件掛在兩個新卷
?該容器有效地託管克隆的數據,便於新的附屬容器可以獲取它
4,為附屬MySQL伺服器創建基本容器
?創建一個MySQL附屬容器,配置它連接到MySQL主機
?創建附屬xtrabackup容器,配置它連接到xtrabackup主機
5,創建一個卷聲明模板來描述每個卷,每個卷是一個10GB的持久磁碟
下面的配置文件定義了MySQL集群的主節點和附屬節點的行為,提供了運行附屬客戶端的bash配置,並確保在克隆之前主節點能夠正常運行。附屬節點和主節點分別獲得他們自己的10GB卷,這是他們在我們之前定義的持久卷存儲類中請求的。
apiVersion: apps/v1beta1kind: StatefulSetmetadata:
name: mysqlspec:
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=$
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=$
[[ $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="$", MASTER_LOG_POS=$" > 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
$(
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
將該文件存為mysql-statefulset.yaml,輸入kubectl create -f mysql-statefulset.yaml並讓Kubernetes部署你的資料庫。
現在當你調用$ kubectl get pods,你應該看到3個pods啟動或者準備好,其中每個pod上都有兩個容器。主節點pod表示為mysql-0,而附屬的pods為mysql-1和mysql-2.讓pods執行幾分鐘來確保xtrabackup服務在pod之間正確同步,然後進行WordPress的部署。
您可以檢查單個容器的日誌來確認沒有錯誤消息拋出。 查看日誌的命令為$ kubectl logs -f -c
主節點xtrabackup容器應顯示來自附屬的兩個連接,並且日誌中不應該出現任何錯誤。
部署高可用的WordPress
整個過程的最後一步是將我們的WordPress pods部署到集群上。為此我們希望為WordPress的服務和部署進行定義。
為了讓WordPress實現高可用,我們希望每個容器運行時都是完全可替換的,這意味著我們可以終止一個,啟動另一個而不需要對數據或服務可用性進行修改。我們也希望能夠容忍至少一個容器的失誤,有一個冗餘的容器負責處理slack。
WordPress將重要的站點相關數據存儲在應用程序目錄/var/www/html中。對於要為同一站點提供服務的兩個WordPress實例,該文件夾必須包含相同的數據。
當運行高可用WordPress時,我們需要在實例之間共享/var/www/html文件夾,因此我們定義一個NGS服務作為這些卷的掛載點。
下面是設置NFS服務的配置,我提供了純英文的版本:
# nfs.yaml# Define the persistent volume claimapiVersion: v1kind: PersistentVolumeClaimmetadata:
name: nfs labels:
demo: nfs annotations:
volume.alpha.kubernetes.io/storage-class: anyspec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 200Gi---# Define the Replication ControllerapiVersion: v1kind: ReplicationControllermetadata:
name: nfs-serverspec:
replicas: 1 selector:
role: nfs-server template:
metadata:
labels:
role: nfs-server spec:
containers:
- name: nfs-server image: gcr.io/google_containers/volume-nfs:0.8 ports:
- name: nfs containerPort: 2049 - name: mountd containerPort: 20048 - name: rpcbind containerPort: 111 securityContext:
privileged: true volumeMounts:
- mountPath: /exports name: nfs-pvc volumes:
- name: nfs-pvc persistentVolumeClaim:
claimName: nfs---# Define the Servicekind: ServiceapiVersion: v1metadata:
name: nfs-serverspec:
ports:
- name: nfs port: 2049 - name: mountd port: 20048 - name: rpcbind port: 111 selector:
role: nfs-server
使用指令$ kubectl create -f nfs.yaml部署NFS服務。現在,我們需要運行$ kubectl describe services nfs-server獲得IP地址,這在後面會用到。
注意:將來,我們可以使用服務名稱講這些綁定在一起,但現在你需要對IP地址進行硬編碼。
# wordpress.yamlapiVersion: v1kind: Servicemetadata:
name: wordpress labels:
app: wordpressspec:
ports:
- port: 80 selector:
app: wordpress tier: frontend type: LoadBalancer---apiVersion: v1kind: PersistentVolumemetadata:
name: nfsspec:
capacity:
storage: 20G accessModes:
- ReadWriteMany nfs:
# FIXME: use the right IP
server: path: "/"---apiVersion: v1kind: PersistentVolumeClaimmetadata:
name: nfsspec:
accessModes:
- ReadWriteMany storageClassName: ""
resources:
requests:
storage: 20G---apiVersion: apps/v1beta1 # for versions before 1.8.0 use apps/v1beta1kind: Deploymentmetadata:
name: wordpress labels:
app: wordpressspec:
selector:
matchLabels:
app: wordpress tier: frontend strategy:
type: Recreate template:
metadata:
labels:
app: wordpress tier: frontend spec:
containers:
- image: wordpress:4.9-apache name: wordpress env:
- name: WORDPRESS_DB_HOST value: mysql - name: WORDPRESS_DB_PASSWORD value: ""
ports:
- containerPort: 80 name: wordpress volumeMounts:
- name: wordpress-persistent-storage mountPath: /var/www/html volumes:
- name: wordpress-persistent-storage persistentVolumeClaim:
claimName: nfs
我們現在創建了一個持久卷聲明,和我們之前創建的NFS服務建立映射,然後將卷附加到WordPress pod上,即/var/www/html根目錄,這也是WordPress安裝的地方。這裡保留了集群中WordPress pods的所有安裝和環境。有了這些配置,我們就可以對任何WordPress節點進行啟動和拆除,而數據能夠留下來。因為NFS服務需要不斷使用物理卷,該卷將保留下來,並且不會被回收或錯誤分配。
使用指令$ kubectl create -f wordpress.yaml部署WordPress實例。默認部署只會運行一個WordPress實例,可以使用指令$ kubectl scale --replicas=
deployment/wordpress擴展WordPress實例數量。
要獲得WordPress服務負載均衡器的地址,你需要輸入$ kubectl get services wordpress
並從結果中獲取EXTERNAL-IP欄位來導航到WordPress。
彈性測試
OK,現在我們已經部署好了服務,那我們來拆除一下它們,看看我們的高可用架構如何處理這些混亂。在這種部署方式中,唯一剩下的單點故障就是NFS服務(原因總結在文末結論中)。你應該能夠測試其他任何的服務來了解應用程序是如何響應的。現在我已經啟動了WordPress服務的三個副本,以及MySQL服務中的一個主兩個附屬節點。
首先,我們先kill掉其他而只留下一個WordPress節點,來看看應用如何響應:$ kubectl scale --replicas=1 deployment/wordpress現在我們應該看到WordPress部署的pod數量有所下降。$ kubectl get pods應該能看到WordPress pods的運行變成了1/1。
點擊WordPress服務IP,我們將看到與之前一樣的站點和資料庫。如果要擴展復原,可以使用$ kubectl scale --replicas=3 deployment/wordpress再一次,我們可以看到數據包留在了三個實例中。
下面測試MySQL的狀態集,我們使用指令縮小備份的數量:$ kubectl scale statefulsets mysql --replicas=1我們會看到兩個附屬從該實例中丟失,如果主節點在此時丟失,它所保存的數據將保存在GCE持久磁碟上。不過就必須手動從磁碟恢複數據。
如果所有三個MySQL節點都關閉了,當新節點出現時就無法複製。但是,如果一個主節點發生故障,一個新的主節點就會自動啟動,並且通過xtrabackup重新配置來自附屬節點的數據。因此,在運行生產資料庫時,我不建議以小於3的複製係數來運行。在結論段中,我們會談談針對有狀態數據有什麼更好的解決方案,因為Kubernetes並非真正是為狀態設計的。
結論和建議
到現在為止,你已經完成了在Kubernetes構建並部署高可用WordPress和MySQL的安裝!
不過儘管取得了這樣的效果,你的研究之旅可能還遠沒有結束。可能你還沒注意到,我們的安裝仍然存在著單點故障:NFS伺服器在WordPress pods之間共享/var/www/html目錄。這項服務代表了單點故障,因為如果它沒有運行,在使用它的pods上html目錄就會丟失。教程中我們為伺服器選擇了非常穩定的鏡像,可以在生產環境中使用,但對於真正的生產部署,你可以考慮使用GlusterFS對WordPress實例共享的目錄開啟多讀多寫。
這個過程涉及在Kubernetes上運行分散式存儲集群,實際上這不是Kubernetes構建的,因此儘管它運行良好,但不是長期部署的理想選擇。
對於資料庫,我個人建議使用託管的關係資料庫服務來託管MySQL實例,因為無論是Google的CloudSQL還是AWS的RDS,它們都以更合理的價格提供高可用和冗餘處理,並且不需擔心數據的完整性。Kuberntes並不是圍繞有狀態的應用程序設計的,任何建立在其中的狀態更多都是事後考慮。目前有大量的解決方案可以在選擇資料庫服務時提供所需的保證。
也就是說,上面介紹的是一種理想的流程,由Kubernetes教程、web中找到的例子創建一個有關聯的現實的Kubernetes例子,並且包含了Kubernetes 1.8.x中所有的新特性。
我希望通過這份指南,你能在部署WordPress和MySQL時獲得一些驚喜的體驗,當然,更希望你的運行一切正常。
TAG:RancherLabs |