阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |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:40799

评论

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

python文件操作知多少

迷彩

Python基础 文件操作 7月月更

strlen()

謓泽

7月月更

Python 迭代器介绍及其作用

宇宙之一粟

Python 迭代器 7月月更

在线摇骰子色子工具

入门小站

工具

鲲鹏代码迁移工具基础知识

乌龟哥哥

7月月更

「分享」从Mybatis源码中,学习到的10种设计模式

小傅哥

设计模式 小傅哥 mybatis 大厂面试 面试问题

3大类15小类前端代码规范,让团队代码统一规范起来!

南极一块修炼千年的大冰块

7月月更

【Docker 那些事儿】容器网络(下篇)

Albert Edison

Docker Kubernetes 容器 云原生 7月月更

C#入门系列(二十三) -- 分部类和抽象类

陈言必行

7月月更

Unity实战问题-WebGL问题集锦-上篇

芝麻粒儿

Unity 7月月更

presto+yanagishima环境安装

怀瑾握瑜的嘉与嘉

presto 7月月更

面试突击66:请求转发和请求重定向有什么区别?

王磊

Java面试题

NFT市场格局仍未变化,Okaleido能否掀起新一轮波澜?

股市老人

Dockerfile中的保留字指令讲解

宁在春

Docker Dockerfile 7月月更

关于InnoDB表数据和索引数据的存储

程序员欣宸

MySQL innodb MySQL InnoDB 7月月更

静态成员函数访问非静态数据成员【C++】

攻城狮杰森

c++ 7月月更

Qt | 控件之QComboBox

YOLO.

qt 7月月更

分布式系统中数据存储方案实践

Java 架构

Spring cloud 之限流

Damon

7月月更

一款强大的mock数据生成工具

Xd

linux之realpath命令

入门小站

Linux

云原生(六) | Docker篇之实战Dockerfile

Lansonli

Docker 云原生 7月月更

zookeeper-watcher的javaApi相关使用

zarmnosaj

7月月更

在线SQL转文本工具

入门小站

工具

Okaleido或杀出NFT重围,你看好它吗?

EOSdreamer111

Android gradle常用

沃德

android Gradle 7月月更

Block在开发中的实践应用

NewBoy

ios 前端 移动端 iOS 知识体系 7月月更

使用kitti数据集实现自动驾驶——发布照片、点云、IMU、GPS、显示2D和3D侦测框

秃头小苏

7月月更 kitti

QComboBox 样式表

小肉球

qt 7月月更

工作流引擎在vivo营销自动化中的应用实践 | 引擎篇03

vivo互联网技术

工作流引擎 workflow Activiti 流程引擎

认识区块链和比特币

沃德

程序员 7月月更

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