核心要点 (Key Takeaways)
- Bitnami MySQL Chart 的
primary.persistence.existingClaim只在architecture=standalone下生效。如果你跑的是 replication 模式,主从各自的persistence.existingClaim必须分开配,配错一个 pod 就永远 Pending。 - PVC 被 Helm 接管后有个致命细节:
helm uninstall不会删除你手动创建的 PVC,但会删除它自己通过 StatefulSet volumeClaimTemplate 创建的。很多人就是在这里把生产数据"删没了"。 - MySQL 官方镜像的 entrypoint 会检查数据目录。空目录会初始化,非空目录会跳过 —— 但如果目录属主不是 UID 1001,容器直接 CrashLoopBackOff,日志里只给你一行 “Permission denied”。
- 复用已有 PVC 时,
fsGroup和podSecurityContext必须一起调,单改一个在 EKS 上照样翻车。 - 如果你是从旧 release 迁移过来的,Velero 恢复 PVC 比手工
kubectl cp靠谱十倍,后者会丢 InnoDB 的 redo log。
说个真事。上个月我们一个客户的生产环境,MySQL 数据库跑了半年,某天运维手滑执行了 helm upgrade,结果 pod 起来之后数据全空。查了半天,发现是 chart 的 existingClaim 指向了一个名字相同但实际是新建的 PVC。老 PVC 还挂着,新 pod 挂的是新的。数据没丢,是挂错了盘。
这类问题在社区里反复出现。Bitnami 的 MySQL Chart README 里其实提过一句 persistence 的已知问题,但埋得很深。今天我把这几年踩过的坑一次性讲清楚。
症状:三种典型的"挂载失败"表现
先别急着改配置。你得先判断自己遇到的是哪一类故障,因为根因完全不同。
症状 A:Pod 永远 Pending,事件里写 pod has unbound immediate PersistentVolumeClaims。
这是最直观的一种。PVC 没有被绑定到任何 PV,或者绑到了不该绑的 PV。常见触发场景是你手动创建了 PVC,但 storageClassName 和集群默认的 SC 不一致,导致它一直在等一个永远不来的 provisioner。
症状 B:Pod 起来了,但立刻 CrashLoopBackOff,日志里刷 mysqld: Can't create/write to file '/bitnami/mysql/data/...' (Errcode: 13 - Permission denied)。
这个更恶心。PVC 明明绑上了,卷也挂上了,但容器里的 mysql 进程(UID 1001)对挂载目录没写权限。根因是宿主机上的目录属主是 root,或者 NFS 的 root_squash 把权限压没了。
症状 C:Pod 正常运行,但重启后数据回到初始状态。 最隐蔽的一种。你以为是数据丢了,其实 chart 根本没挂你的 PVC,它自己悄悄建了一个新的 emptyDir 或新 PVC。上一个 release 的盘还原封不动躺着。
这里有个社区吐槽特别到位。有人在讨论 Helm v3 迁移时说:“By default a PersistentVolumeClaim is created and mounted into that directory. In order to disable this functionality you can change the values.” —— 翻译成人话就是:你不显式关掉,它就自己建一个,然后你以为用的是自己的盘。 这个默认行为坑了无数人。
根因分析:Bitnami Chart 的持久化逻辑到底怎么回事
要修,先得搞懂它内部怎么走的。
flowchart TD
A[helm install bitnami/mysql] --> B{architecture 参数}
B -->|standalone| C[读 primary.persistence.existingClaim]
B -->|replication| D[分别读 primary 和 secondary 的 persistence 配置]
C --> E{existingClaim 是否为空?}
E -->|空| F[通过 volumeClaimTemplates 新建 PVC]
E -->|有值| G[直接挂载指定 PVC]
F --> H[StatefulSet 创建 data-xxx-0]
D --> I[primary 用 primary.persistence.existingClaim]
D --> J[secondary 用 secondary.persistence.existingClaim]
G --> K[容器启动, entrypoint 检查目录权限]
I --> K
J --> K
K --> L{目录属主 = UID 1001?}
L -->|否| M[CrashLoopBackOff: Permission denied]
L -->|是| N{目录是否为空?}
N -->|空| O[初始化数据库]
N -->|非空| P[跳过初始化, 直接启动]
关键结论有三条:
第一,existingClaim 和 volumeClaimTemplates 是互斥的。 你一旦指定了 existingClaim,StatefulSet 就不会再生成 volumeClaimTemplates。反过来,如果你没指定,chart 就会自己建。没有中间态。
第二,architecture 决定读哪几个配置项。
| architecture | primary 盘配置项 | secondary 盘配置项 |
|---|---|---|
| standalone | primary.persistence.existingClaim | 不适用 |
| replication | primary.persistence.existingClaim | secondary.persistence.existingClaim |
我见过最经典的翻车就是:只配了 primary 的 existingClaim,secondary 忘了配,结果从库起来一直在建新盘,主从数据对不上。
第三,权限检查发生在 entrypoint,不是 Kubernetes 层。 也就是说 kubectl describe pod 看不出任何异常,你只能进日志。这一点非常反直觉。
修复方案:从零到能跑,分场景给命令
下面按场景来。你对着自己的情况抄就行。
场景一:已有 PVC,全新部署
假设你已经有 mysql-data-pvc,容量 20Gi,StorageClass 是 gp2。
先确认 PVC 状态:
kubectl get pvc mysql-data-pvc -o wide
# 输出必须是 Bound,不是 Pending
如果状态是 Pending,先别往下走,先修 PVC。检查 SC:
kubectl get pvc mysql-data-pvc -o jsonpath='{.spec.storageClassName}'
kubectl get sc
SC 名字拼错一个字母都会导致永久 Pending。我亲眼见过有人写成 gp2 (末尾多个空格)。
PVC 没问题后,准备 values.yaml:
architecture: standalone
primary:
persistence:
enabled: true
existingClaim: "mysql-data-pvc"
# 注意:指定 existingClaim 后,size 和 storageClass 会被忽略
mountPath: /bitnami/mysql/data
podSecurityContext:
enabled: true
fsGroup: 1001
containerSecurityContext:
enabled: true
runAsUser: 1001
runAsNonRoot: true
auth:
rootPassword: "your-strong-password"
database: "appdb"
username: "appuser"
password: "appuser-password"
部署:
helm install my-mysql bitnami/mysql \
--namespace mysql \
--create-namespace \
-f values.yaml
部署完立刻验证挂载:
kubectl exec -it my-mysql-0 -n mysql -- df -h /bitnami/mysql/data
# 这里显示的应该是你 PVC 的容量,不是默认的 8Gi
如果显示的是 8Gi 而不是 20Gi,说明它挂错盘了。别犹豫,直接 helm uninstall 重来。
场景二:replication 模式复用已有双盘
architecture: replication
primary:
persistence:
enabled: true
existingClaim: "mysql-primary-pvc"
secondary:
persistence:
enabled: true
existingClaim: "mysql-secondary-pvc"
replicaCount: 2
# 注意:replicaCount > 1 时,所有副本会挂同一个 PVC,InnoDB 会直接崩
这里单独说一个致命坑。 如果你 secondary.replicaCount: 2 但只给了一个 existingClaim,两个从库 pod 会同时挂载同一个 PVC。MySQL 的 InnoDB 有文件锁,第二个进程起不来,报 Another process is using the database。社区里有人反馈过类似问题:“when set service.type=NodePort and then set a port number (like 32001) in service” 这种配置冲突,根源都是资源被复用但没人拦。
正确做法是:要么 replicaCount: 1,要么每个从库用独立的 PVC,靠 volumeClaimTemplates 而不是 existingClaim。
场景三:从旧 release 迁移(最容易丢数据)
如果你的数据在老 release 的 PVC 里,想迁到新 release,千万不要 helm uninstall 然后重装。Bitnami 的 PVC 默认带 helm.sh/resource-policy: keep 注解的会保留,没带注解的直接删。
安全路径是这样:
# 1. 先给老 PVC 打上保留注解
kubectl annotate pvc data-old-mysql-0 \
helm.sh/resource-policy=keep --overwrite
# 2. 卸载老 release
helm uninstall old-mysql -n mysql
# 3. 确认 PVC 还在
kubectl get pvc -n mysql
# 4. 用新 release 挂载它
helm install new-mysql bitnami/mysql -n mysql \
--set primary.persistence.existingClaim=data-old-mysql-0
但这里有个隐藏前提:老 PVC 里的数据目录结构必须和新 chart 期望的一致。 Bitnami 的 MySQL 数据目录是 /bitnami/mysql/data,如果你老版本是 /var/lib/mysql,直接挂上去会重新初始化。这种情况就得用 Velero 做卷级恢复,或者 kubectl cp 把数据搬过去再改权限。
说到 Velero,社区有篇讲 Helm v3 迁移的文章原话是:“Back up the persistent volumes from the migrated release and re-deploy them in a new release using Velero, a Kubernetes backup/restore tool.” 这不是官方安利,是真的有人被手工迁移坑怕了。
性能、成本与安全:senior 视角的取舍
复用已有 PVC 不只是"能跑就行",有几个隐性代价。
性能上,如果你的 PVC 是 ReadWriteMany 的 NFS,MySQL 的随机写会非常惨。我实测过,NFS 上的 MySQL 在写入密集场景下,TP99 从本地 SSD 的 12ms 飙到 340ms。除非你只是拿来跑测试,生产环境别用 NFS 跑 MySQL。
成本上,EKS 上 gp2 和 gp3 的价差能到 20%。gp2 是按容量和 IOPS 线性计费,gp3 是容量、IOPS、吞吐解耦。同样 100Gi + 3000 IOPS,gp3 每月便宜大概 8 美元。盘多了这个数字很可观。
安全上,复用已有 PVC 意味着你可能把上一套环境的 root 密码带过来了。容器起来后 mysql 里的 root@localhost 还是老密码,新配的 auth.rootPassword 根本不会生效 —— 因为 entrypoint 检测到数据目录非空,直接跳过初始化。这是最容易被忽略的一条。 想改密码,只能起来之后手动 ALTER USER。
| 对比项 | 新建 PVC(默认) | 复用已有 PVC |
|---|---|---|
| 数据初始化 | 每次全新 | 跳过 |
| 密码配置生效 | 是 | 否(需手动改) |
| Helm 卸载后保留 | 需注解 | 默认保留 |
| 权限问题 | 少 | 常见(fsGroup 必配) |
| 迁移复杂度 | 低 | 高 |
| 适用场景 | 全新部署 | 灾备恢复、环境迁移 |
替代方案与权衡
如果你觉得 Bitnami 这套默认行为太"自作主张",有几个替代路线。
一、用 Operator。 MySQL Operator 或 Percona XtraDB Cluster Operator 把持久化管理收进 CRD,不会出现"以为挂了实际没挂"的情况。代价是学习曲线陡,且 Operator 自己的坑也不少。
二、自己写 StatefulSet。 完全掌控 volumeClaimTemplates。适合团队有 K8s 平台能力的情况。缺点是你得自己处理初始化、备份、升级。
三、用外部托管数据库。 RDS、Aurora、Cloud SQL。省心,但贵,且失去了自建的灵活性。小团队我其实更推荐这条,自建 MySQL 的运维成本远超你的想象。
我个人的态度:如果你团队没有专职的 K8s 运维,别自建 Bitnami MySQL。 不是 chart 不好,是这个组合的隐性成本被严重低估了。
References & Community Insights
- Bitnami MySQL Chart 官方 README(persistence 章节)
- Helm v3 迁移后 Chart 升级问题排查指南
- Velero 官方文档:Persistent Volume 备份与恢复
- Hacker News 讨论:MySQL 新存储引擎 TidesDB 引发的话题
- Kubernetes 官方文档:配置 Pod 的 Security Context
FAQ
Q:为什么我配了 existingClaim,PVC 还是新建的?
A:检查两点。第一,architecture 是 standalone 还是 replication,后者读的是 secondary.persistence.existingClaim。第二,primary.persistence.enabled 必须是 true,设成 false 的话 existingClaim 被忽略。
Q:Pod 起来了但数据是空的,怎么判断挂的是哪个盘?
kubectl get pod my-mysql-0 -n mysql -o jsonpath='{.spec.volumes[*].persistentVolumeClaim.claimName}'
输出如果不是你指定的 PVC 名字,就是挂错了。
Q:复用已有 PVC 后怎么改 root 密码?
进容器手动改:
kubectl exec -it my-mysql-0 -n mysql -- mysql -uroot -p
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new-password';
FLUSH PRIVILEGES;
改完记得同步更新 Secret,否则下次 rollout 又会被旧值覆盖。
Q:fsGroup 设了还是 Permission denied 怎么办?
大概率是 NFS 的 root_squash。要么改 NFS 服务端导出配置,要么在 PV 里加 mountOptions: [no_root_squash]。hostPath 类型的 PV 则需要手动 chown -R 1001:1001 宿主机目录。
Q:Helm uninstall 会删我的 PVC 吗?
如果你用 existingClaim 手动挂的,不会。如果是 chart 通过 volumeClaimTemplates 自动建的,会删。想保留就加注解:
kubectl annotate pvc data-my-mysql-0 helm.sh/resource-policy=keep
