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=${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
复制代码
将该文件存为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 <container_name>
主节点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: <IP of the NFS Service> 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=<number of 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 时获得一些惊喜的体验,当然,更希望你的运行一切正常。
评论