AI 年度盘点与2025发展趋势展望,50+案例解析亮相AICon 了解详情
写点什么

在 Kubernetes 上运行高可用的 WordPress 和 MySQL

  • 2020-04-12
  • 本文字数:8443 字

    阅读完需:约 28 分钟

在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=${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-1mysql-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 时获得一些惊喜的体验,当然,更希望你的运行一切正常。


2020-04-12 20:40975

评论

发布
暂无评论
发现更多内容

wrk压力测试工具介绍

乌龟哥哥

6月月更

Android Target 31 升级全攻略 —— 记阿里首个超级 App 的坎坷升级之路

阿里巴巴终端技术

android App target

OneFlow源码解析:算子签名的自动推断

OneFlow

源码解析 算子 Relu

Mac中Git如何忽略.DS_Store文件

坚果

git git 规范 6月月更

构建实战化防御体系之立体防渗透

穿过生命散发芬芳

6月月更 攻防演练

Zadig 正式推出 VS Code 插件,本地开发更高效

Zadig

vscode 插件 热部署 本地化开发 Zadig

Zadig 构建究竟何强大?一起来实践

Zadig

gitlab 云原生 jenkins Zadig

稳!上千微服务如何快速接入 Zadig(Helm Chart 篇)

Zadig

DevOps 微服务架构 持续交付 自动化运维 Zadig

基于管线的混合渲染

Finovy Cloud

gpu 渲染器 GPU服务器 显卡、gpu

微博评论的高性能高可用计算架构方案

joak

为什么要使用 Rust 语言?

面向加薪学习

rust

华为云的AI深潜之旅

脑极体

Zadig + SonarQube,为开发过程安全保驾

Zadig

DevOps 代码扫描 SonarQube 质量内建

软件测试的三个沟通技巧

FunTester

电商秒杀系统架构设计

哈喽

「架构实战营」

直播预告|SQL也能玩转工业级机器学习?MLOps meetup V3带你一探究竟!

星策开源社区

人工智能 机器学习 sql 特征平台 MLOps

穿越过后,她说多元宇宙真的存在

脑极体

00 后云原生工程师:用开源 Zadig 为思创科技(广州公交)研发开源节流

Zadig

DevOps 研发效能 工程师 自动化运维

穿越过后,她说多元宇宙真的存在

脑极体

安全 创新 实践|海泰方圆受邀参加“数字时代的网信创新与价值共创”技术交流研讨会

电子信息发烧客

Zadig + 洞态 IAST:让安全溶于持续交付

Zadig

DevSecOps 代码安全检测 安全测试 Zadig

带链接跳转的微信红包封面制作教程和使用指南

boshi

小程序 微信红包封面 微信红包

十大靠谱“计算机视觉数据集”榜单

澳鹏Appen

人工智能 机器学习 计算机视觉 数据集 训练数据

应用实践 | 10 亿数据秒级关联,货拉拉基于 Apache Doris 的 OLAP 体系演进(附 PPT 下载)

SelectDB

数据库 flink OLAP Doris 数仓建设

Java Core「19」使用 Java IO API 创建 C/S 程序的方法

Samson

学习笔记 Java core 6月月更

如何高效优雅地管理接口文档

Liam

测试 开发工具 API接口管理 API文档 免费API接口

如何设计业务高性能高可用计算架构 - 作业

阿拉阿拉幽幽

OUT了吧,Kafka能实现消息延时了

华为云开发者联盟

云计算 开发

2022最新Java面试突击手册,1000道面试题+优质面经

Java全栈架构师

Java 程序员 面试 算法 计算机网络

Zadig 面向开发者的自测联调子环境技术方案详解

Zadig

DevOps Service Mesh CI/CD 测试环境治理

稳!上千微服务如何快速接入 Zadig(K8s YAML 篇)

Zadig

DevOps 微服务架构 k8s 持续交付 自动化运维

在Kubernetes上运行高可用的WordPress和MySQL_文化 & 方法_Rancher_InfoQ精选文章