WordPress 综合实战¶
WordPress 是最好的「全对象练习」:它足够简单,却同时需要无状态前端、有状态数据库、共享存储、初始化密码和外部入口。本章把七个 YAML 按依赖顺序跑通,再故意删掉 Pod、备份数据库、滚动更新,看数据到底在谁手里。
你将学到¶
- 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 · 检查存储类
没有 RWX 存储类时,把 06-pvc-wordpress.yaml 的 storageClassName 换成默认 SC,并把 accessModes 改成 ReadWriteOnce,单副本照样跑得通。
步骤 2 · 按顺序应用 01–07
文件名前缀就是依赖顺序:namespace 必须最先,Secret 必须在 StatefulSet 之前,PVC 必须在 Deployment 之前。
namespace/blog created
secret/mysql-secret created
statefulset.apps/mysql created
persistentvolumeclaim/wordpress-pvc created
deployment.apps/wordpress created
步骤 3 · 验证 Pod、PVC、Service
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
看到语言选择页就说明整条链路通了;走完向导后数据写进 wpdb。
步骤 5 · 删除 Pod,验证数据还在
Deployment 立刻重建一个同名不同后缀的 Pod;因为 /var/www/html 挂在 wordpress-pvc 上,上传的图片和 wp-config.php 都还在。刷新页面仍是已安装状态。
再狠一点,删数据库 Pod:
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
更新期间持续 curl -I http://blog.magedu.com,因为 maxUnavailable: 0,应该不会看到 5xx。出问题就回滚:
验证清单¶
-
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 |
进阶挑战¶
- 把 WordPress 扩到 2 副本,验证 RWX 卷共享上传目录;再故意换成 RWO,观察两个 Pod 的调度结果。
- 用
CronJob+mysqldump+mc(MinIO 客户端)做每日备份,保留 7 天,并写一份恢复演练步骤。
小结¶
- 应用链路的价值不在「装起来」,而在每个对象解决一个具体依赖:Secret 解决凭据、headless Service 解决稳定标识、PVC 解决状态。
- MySQL 必须用 StatefulSet +
volumeClaimTemplates,Pod 名与 PVC 名一一对应,删 Pod 不删数据。 MYSQL_*初始化变量只在空数据目录生效,改密码要进库改,别指望重启。- RWX 是「多 Pod 共享上传目录」的前提,也是滚动更新不触发
Multi-Attach的前提。