跳转至

WordPress 综合实战

WordPress 是最好的「全对象练习」:它足够简单,却同时需要无状态前端、有状态数据库、共享存储、初始化密码和外部入口。本章把七个 YAML 按依赖顺序跑通,再故意删掉 Pod、备份数据库、滚动更新,看数据到底在谁手里。

难度 · 进阶 预计 60 分钟 前置:StatefulSet、PVC、Service、Secret

你将学到

  • MySQL 为什么用 StatefulSet + headless Service,WORDPRESS_DB_HOST 该填什么
  • ephemeral 与持久化两条路径的取舍,以及 RWX 与 RWO 的真实差别
  • 备份与恢复的最小可用做法

为什么需要它

单跑一个 kubectl run 只能验证镜像能启动。真实应用有一串依赖:数据库要先于应用就绪,密码不能写进镜像,上传的文件必须活过 Pod 重建,外部域名要能进来。这些依赖任何一环错位,表现都是「页面打不开」,而原因可能藏在 Secret、PVC 或 DNS 里。

仓库给了两套材料,代表两条路径:

路径 目录 组成 存储依赖
单容器 examples/wordpress/ wordpress:6-apache + mysql:8.0 StatefulSet openebs-rwx、openebs-jiva-csi
三层拆分 wordpress/mysql、wordpress/wordpress、wordpress/nginx nginx + wordpress:5.8-fpm + mysql nfs-csi(RWX)
临时验证 wordpress/mysql-ephemeral、wordpress/wordpress-apache-ephemeral 同上,但用 emptyDir 无

本章实验走 examples/wordpress,原理部分会把三层拆分的差异讲清楚。

flowchart LR
  B[浏览器] --> I[Ingress 或 LoadBalancer]
  I --> W[Pod wordpress:apache]
  W -->|WORDPRESS_DB_HOST| M[mysql-0.mysql]
  M --> P[(PVC data 5Gi RWO)]
  W --> U[(PVC wordpress-pvc 5Gi RWX)]

图:单容器路径的请求与存储关系。应用 Pod 无状态,状态分别落在 MySQL 数据卷和 WordPress 上传目录。

核心概念

为什么 MySQL 是 StatefulSet

StatefulSet 给 Pod 三样 Deployment 给不了的东西:稳定的名字(mysql-0)、稳定的网络标识(mysql-0.mysql)、以及每个副本独立的 PVC(volumeClaimTemplates)。数据库需要「这个 Pod 永远对应这份数据」,所以用 StatefulSet。

配套的 Service 是 headless(clusterIP: None),它不做负载均衡,只为每个 Pod 提供 DNS 记录。于是:

  • mysql 解析到所有就绪 Pod 的 IP 列表;
  • mysql-0.mysql 精确解析到 0 号 Pod。

示例里 WORDPRESS_DB_HOST: mysql-0.mysql 就是后者。它依赖一个前提:应用和数据库在同一个 namespace(这里是 blog)。跨 namespace 必须写全 mysql-0.mysql.blog.svc.cluster.local,否则 DNS 直接失败。

初始化只发生一次

MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER 这些环境变量只在数据目录为空时生效。/var/lib/mysql 里已经有数据时,镜像会跳过初始化,你改 Secret 里的密码不会同步到数据库。

这是一个高频事故:有人发现密码错了,改完 Secret 重启 Pod,结果应用还是连不上。要么进库改密码,要么连 PVC 一起删掉重来。

RWX 与 RWO 的差别

访问模式 含义 本案例用在哪 用错的后果
ReadWriteOnce 单节点可读写,同节点多 Pod 可挂 MySQL 数据卷 滚动更新时新 Pod 调度到别的节点 → Multi-Attach error
ReadWriteMany 多节点同时读写 WordPress 上传目录 后端不支持 RWX 时 PVC 永远 Pending

单容器路径里其实只有一个 Pod 挂载上传目录,用 RWO 也能跑;之所以给 RWX,是为了扩到两副本时两个节点都能挂。三层拆分路径则是必须 RWX:nginx 和 php-fpm 两个 Pod 都要挂 /var/www/html,而且它们通常不在同一节点。

三层拆分的关键在 nginx 配置:ConfigMap 里 fastcgi_pass wordpress:9000 把 PHP 请求转给 fpm,静态文件由 nginx 直接返回;fpm 的 WORDPRESS_DB_HOST 则指向 mysql 这个 ClusterIP Service,而不是 headless 的 mysql-0.mysql。

仓库里的存储类对不上

examples/wordpress/README.md 说 MySQL 用 openebs-hostpath,但 04-statefulset-mysql.yaml 的 volumeClaimTemplates 写的是 openebs-jiva-csi。这是仓库的一处不一致,实验前用 kubectl get sc 核对,把清单里的名字改成你集群真实存在的那个。

顺带说 07-deployment-wordpress.yaml 的策略:maxSurge: 1、maxUnavailable: 0,先起新 Pod 再杀老的。这对可用性友好,但对存储有要求——RWO 卷的新 Pod 落到另一节点会卡在 Multi-Attach,更新永远不完成;RWX 没有这个问题。

动手实验

实验:按 01–07 部署,验证数据、备份与更新

目标:把七个对象串起来,并用三次「破坏」验证持久化真的生效。

步骤 1 · 检查存储类

kubectl get storageclass
openebs-hostpath   openebs.io/local   Delete   12d
openebs-rwx        openebs.io/nfsrwx  Delete   12d

没有 RWX 存储类时,把 06-pvc-wordpress.yaml 的 storageClassName 换成默认 SC,并把 accessModes 改成 ReadWriteOnce,单副本照样跑得通。

步骤 2 · 按顺序应用 01–07

文件名前缀就是依赖顺序:namespace 必须最先,Secret 必须在 StatefulSet 之前,PVC 必须在 Deployment 之前。

for f in examples/wordpress/0*.yaml; do
  echo "==> $f"
  kubectl apply -f "$f"
done
namespace/blog created
secret/mysql-secret created
statefulset.apps/mysql created
persistentvolumeclaim/wordpress-pvc created
deployment.apps/wordpress created

步骤 3 · 验证 Pod、PVC、Service

kubectl get pods,pvc,svc -n blog
NAME                             READY   STATUS    AGE
pod/mysql-0                      1/1     Running   2m
pod/wordpress-6c8f7d9b5f-7kx2p   1/1     Running   2m

NAME                                  STATUS   CAPACITY   ACCESS MODES
persistentvolumeclaim/data-mysql-0                  Bound    5Gi        RWO
persistentvolumeclaim/wordpress-pvc                 Bound    5Gi        RWX

步骤 4 · 用 Ingress 打开安装页

仓库没有提供 Ingress,下面这份可以直接用:

wordpress-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: wordpress
  namespace: blog
spec:
  ingressClassName: nginx
  rules:
    - host: blog.magedu.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: wordpress
                port:
                  number: 80
kubectl apply -f wordpress-ingress.yaml
echo "192.168.1.240 blog.magedu.com" | sudo tee -a /etc/hosts  # 换成 Ingress 入口 IP
curl -I http://blog.magedu.com/wp-admin/install.php
HTTP/1.1 200 OK

看到语言选择页就说明整条链路通了;走完向导后数据写进 wpdb。

步骤 5 · 删除 Pod,验证数据还在

kubectl delete pod -n blog -l app=wordpress
kubectl get pods -n blog -w

Deployment 立刻重建一个同名不同后缀的 Pod;因为 /var/www/html 挂在 wordpress-pvc 上,上传的图片和 wp-config.php 都还在。刷新页面仍是已安装状态。

再狠一点,删数据库 Pod:

kubectl delete pod mysql-0 -n blog
kubectl get pvc data-mysql-0 -n blog

StatefulSet 会用同一个 PVC 重建 mysql-0,文章、用户都还在;等它就绪后再刷新页面。PVC 被删,数据才真的没了。

步骤 6 · 备份数据库

kubectl exec -n blog mysql-0 -- sh -c \
  'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --databases wpdb' \
  > wordpress-$(date +%F).sql

ls -lh wordpress-*.sql
head -5 wordpress-*.sql
-rw-r--r-- 1 user user 1.2M Nov  7 16:20 wordpress-2026-11-07.sql
-- MySQL dump 10.13  Distrib 8.0.36, for Linux (x86_64)

--single-transaction 让 InnoDB 表在不锁表的情况下导出一致快照。上传目录不在数据库里,必须另存:kubectl exec -n blog deploy/wordpress -- tar czf - -C /var/www/html wp-content/uploads > uploads-$(date +%F).tar.gz。

步骤 7 · 滚动更新

kubectl set image deployment/wordpress -n blog wordpress=wordpress:6.4-apache
kubectl rollout status deployment/wordpress -n blog
deployment "wordpress" successfully rolled out

更新期间持续 curl -I http://blog.magedu.com,因为 maxUnavailable: 0,应该不会看到 5xx。出问题就回滚:

kubectl rollout undo deployment/wordpress -n blog

验证清单

  • blog 命名空间里 mysql 与 wordpress 两个 Pod 均 1/1 Running
  • 安装页返回 200,完成安装后前台页面正常
  • 删除 mysql-0 后 PVC 不变、数据仍可读
  • rollout status 成功,回滚可用

常见问题与排错

现象 可能原因 处理方式
PVC 一直 Pending StorageClass 名不存在;后端不支持 RWX kubectl describe pvc;把 SC 换成集群已有的
Error establishing a database connection WORDPRESS_DB_HOST 写错;Secret 键名不匹配 在 Pod 内 getent hosts mysql-0.mysql;核对 secretKeyRef
Pod 卡在 ContainerCreating RWO 卷多节点挂载冲突 kubectl describe pod 找 Multi-Attach;改用 RWX 或 Recreate
重启后图片丢失 用了 emptyDir 的临时路径 检查 Deployment 的 volumes 是否真挂了 PVC

进阶挑战

  1. 把 WordPress 扩到 2 副本,验证 RWX 卷共享上传目录;再故意换成 RWO,观察两个 Pod 的调度结果。
  2. 用 CronJob + mysqldump + mc(MinIO 客户端)做每日备份,保留 7 天,并写一份恢复演练步骤。

小结

  • 应用链路的价值不在「装起来」,而在每个对象解决一个具体依赖:Secret 解决凭据、headless Service 解决稳定标识、PVC 解决状态。
  • MySQL 必须用 StatefulSet + volumeClaimTemplates,Pod 名与 PVC 名一一对应,删 Pod 不删数据。
  • MYSQL_* 初始化变量只在空数据目录生效,改密码要进库改,别指望重启。
  • RWX 是「多 Pod 共享上传目录」的前提,也是滚动更新不触发 Multi-Attach 的前提。

延伸阅读