4.6. WordPress 综合实战
WordPress 是最好的「全对象练习」:它足够简单,却同时需要无状态前端、有状态数据库、共享存储、初始化密码和外部入口。本章把七个 YAML 按依赖顺序跑通,再故意删掉 Pod、备份数据库、滚动更新,看数据到底在谁手里。
- 层级:
L4 · 实战
- 难度:
进阶
- 预计:
60 分钟
- 前置:
StatefulSet、PVC、Service、Secret
4.6.1. 你将学到
MySQL 为什么用 StatefulSet + headless Service,
WORDPRESS_DB_HOST该填什么ephemeral与持久化两条路径的取舍,以及 RWX 与 RWO 的真实差别备份与恢复的最小可用做法
4.6.2. 为什么需要它
单跑一个 kubectl run 只能验证镜像能启动。真实应用有一串依赖:数据库要先于应用就绪,密码不能写进镜像,上传的文件必须活过 Pod 重建,外部域名要能进来。这些依赖任何一环错位,表现都是「页面打不开」,而原因可能藏在 Secret、PVC 或 DNS 里。
仓库给了两套材料,代表两条路径:
路径 |
目录 |
组成 |
存储依赖 |
|---|---|---|---|
单容器 |
|
|
|
三层拆分 |
|
nginx + |
|
临时验证 |
|
同上,但用 |
无 |
本章实验走 examples/wordpress,原理部分会把三层拆分的差异讲清楚。
图:单容器路径的请求与存储关系。应用 Pod 无状态,状态分别落在 MySQL 数据卷和 WordPress 上传目录。
4.6.3. 核心概念
4.6.3.1. 为什么 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 直接失败。
4.6.3.2. 初始化只发生一次
MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER 这些环境变量只在数据目录为空时生效。/var/lib/mysql 里已经有数据时,镜像会跳过初始化,你改 Secret 里的密码不会同步到数据库。
这是一个高频事故:有人发现密码错了,改完 Secret 重启 Pod,结果应用还是连不上。要么进库改密码,要么连 PVC 一起删掉重来。
4.6.3.3. RWX 与 RWO 的差别
访问模式 |
含义 |
本案例用在哪 |
用错的后果 |
|---|---|---|---|
|
单节点可读写,同节点多 Pod 可挂 |
MySQL 数据卷 |
滚动更新时新 Pod 调度到别的节点 → |
|
多节点同时读写 |
WordPress 上传目录 |
后端不支持 RWX 时 PVC 永远 |
单容器路径里其实只有一个 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 没有这个问题。
4.6.4. 动手实验
实验:按 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,下面这份可以直接用:
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
4.6.4.1. 验证清单
blog命名空间里 mysql 与 wordpress 两个 Pod 均1/1 Running安装页返回 200,完成安装后前台页面正常
删除
mysql-0后 PVC 不变、数据仍可读rollout status成功,回滚可用
4.6.5. 常见问题与排错
现象 |
可能原因 |
处理方式 |
|---|---|---|
PVC 一直 |
StorageClass 名不存在;后端不支持 RWX |
|
|
|
在 Pod 内 |
Pod 卡在 |
RWO 卷多节点挂载冲突 |
|
重启后图片丢失 |
用了 |
检查 Deployment 的 |
4.6.6. 进阶挑战
把 WordPress 扩到 2 副本,验证 RWX 卷共享上传目录;再故意换成 RWO,观察两个 Pod 的调度结果。
用
CronJob+mysqldump+mc(MinIO 客户端)做每日备份,保留 7 天,并写一份恢复演练步骤。
4.6.7. 小结
应用链路的价值不在「装起来」,而在每个对象解决一个具体依赖:Secret 解决凭据、headless Service 解决稳定标识、PVC 解决状态。
MySQL 必须用 StatefulSet +
volumeClaimTemplates,Pod 名与 PVC 名一一对应,删 Pod 不删数据。MYSQL_*初始化变量只在空数据目录生效,改密码要进库改,别指望重启。RWX 是「多 Pod 共享上传目录」的前提,也是滚动更新不触发
Multi-Attach的前提。