大家好,我是数据库小学妹 👋
一家互联网公司做容器化改造,找我评估MySQL上K8s的可行性。聊完他们的场景,我给了一句结论:"读从库和开发环境可以上,核心交易库先别动。"
他们问为什么。我说:Pod是临时的,数据不能临时。StatefulSet、持久化存储、网络转发延迟,这三个问题不解决,上去了也跑不稳。
为了验证这个结论,我搭了一套完整的测试环境。从StatefulSet选型到持久化存储配置到Operator管理主从复制,再到sysbench压测对比。折腾了两周,得出了一堆数据和3个踩过的坑。
今天我把整个过程记录下来。如果你也在考虑把数据库搬上容器平台,这篇能帮你省点时间。
为什么要把数据库跑在K8s上?
先说结论:不是为了赶时髦。传统虚拟机部署数据库,扩容要申请资源、装系统、装数据库、配置主从。一套流程下来至少一周。K8s上,一条命令就能拉起新实例。
故障恢复也是。传统方式要手动切主从、改VIP、通知应用。K8s上Operator自动检测、自动拉起、自动重建复制关系。但代价是什么?存储性能损耗。网络复杂度。运维方式完全不同。
不是所有数据库都适合上K8s。核心交易系统我建议先用虚拟机。读从库、开发测试环境、临时数据服务,适合先上容器。
为什么选StatefulSet而不是Deployment?
K8s有两种工作负载控制器。Deployment和StatefulSet。跑MySQL必须选StatefulSet。原因很简单。
Deployment是为无状态应用设计的。 Pod挂了重新拉起,新Pod和旧Pod没有区别。IP地址变了无所谓。数据不存在本地。
StatefulSet是为有状态应用设计的。 每个Pod有固定的名字(mysql-0、mysql-1、mysql-2)。Pod重建后名字不变。每个Pod绑定独立的持久化存储。
| 特性 | Deployment | StatefulSet |
|---|---|---|
| Pod命名 | 随机哈希 | 固定序号 |
| 存储绑定 | 共享PVC | 每Pod独立PVC |
| 启动顺序 | 并行 | 有序(0→1→2) |
| 适用场景 | Web服务、API | 数据库、消息队列 |
如果用Deployment跑MySQL,Pod重启后PVC可能绑到别的Pod上,数据就乱了。StatefulSet保证mysql-0永远用自己的PVC。
持久化存储:PVC和StorageClass怎么配?
这是最关键的环节。Pod是临时的,数据必须是持久的。
第一步,创建StorageClass。 它告诉K8s用什么方式提供存储。我用的NFS,因为简单。生产环境建议用Ceph RBD或云盘。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: mysql-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain # Pod删除后数据保留
第二步,StatefulSet里声明PVC模板。 每个Pod自动创建一个独立的PVC。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3 # 1主2从
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
- name: MYSQL_REPLICATION_USER
value: "repl"
- name: MYSQL_REPLICATION_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: repl-password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: mysql-storage
resources:
requests:
storage: 100Gi
这里有几个容易踩的坑。
accessModes必须是ReadWriteOnce。 一块存储只能被一个Pod挂载。如果用ReadWriteMany,多个Pod可能同时写同一个数据文件,直接损坏。
reclaimPolicy设成Retain。 Pod删除后PVC保留。如果设成Delete,误删StatefulSet就等于删数据。
资源限制要设。 不设置limits,MySQL可能吃光节点内存,触发OOM Killer。
主从复制怎么在K8s上搭?
StatefulSet创建了3个Pod。但它们默认是3个独立的MySQL实例。主从复制需要额外配置。有两种方式:手动配置和Operator管理。
手动配置: 用ConfigMap注入my.cnf,init-container初始化主从关系。
# ConfigMap注入主库配置
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
primary.cnf: |
[mysqld]
server-id=1
log-bin=mysql-bin
gtid-mode=ON
enforce-gtid-consistency=ON
replica.cnf: |
[mysqld]
server-id=2
read-only=ON
gtid-mode=ON
enforce-gtid-consistency=ON
然后用initContainer根据Pod序号加载不同配置。mysql-0加载primary.cnf,mysql-1和mysql-2加载replica.cnf。
这种方式能跑,但运维成本大。主库挂了要手动切。从库重建要手动配。
Operator管理: 用现成的MySQL Operator。我试了Percona的PXC Operator和Oracle的MySQL Operator。它们自动处理主从搭建、故障切换、备份恢复。
以Percona Operator为例,部署完Operator后,只需要定义一个自定义资源:
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
name: mysql-cluster
spec:
pxc:
size: 3
image: percona/percona-xtradb-cluster:8.0
proxysql:
size: 2
image: percona/percona-xtradb-cluster-operator:1.14.0-haproxy
Operator会自动创建StatefulSet、配置主从(PXC模式)、设置ProxySQL负载均衡。主库挂了自动选举新主。
性能损耗:容器到底慢多少?
这是我做之前最关心的问题。跑个测试对比一下。
# 容器内MySQL的sysbench测试
sysbench oltp_read_write \
--mysql-host=mysql-0.mysql.default.svc.cluster.local \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=xxx \
--tables=10 \
--table-size=100000 \
run
# 同配置虚拟机MySQL的sysbench测试
sysbench oltp_read_write \
--mysql-host=192.168.1.100 \
--mysql-port=3306 \
...
测试结果:
| 指标 | 虚拟机 | K8s容器 | 差异 |
|---|---|---|---|
| TPS | 1250 | 1180 | -5.6% |
| 平均延迟 | 8ms | 8.5ms | +6.3% |
| P99延迟 | 45ms | 52ms | +15.6% |
读写性能差距在5%到15%之间。对大多数业务可以接受。P99延迟差异稍大,主要来自网络层(kube-proxy的iptables转发)。
网络性能损耗的来源。 K8s的Service通过iptables或IPVS做端口转发。每经过一层转发,增加一点延迟。如果用HostNetwork模式,Pod直接用宿主机网络,性能接近虚拟机。但牺牲了网络隔离性。
存储性能损耗的深层原因。 这不只是NFS和SSD的区别。MySQL的InnoDB在做fsync时,期望数据直接落到物理磁盘。但在K8s上,数据要经过容器存储层、CSI驱动、再到存储后端。每多一层,就多一次上下文切换和内存拷贝。我用strace追踪过MySQL的fsync调用,发现容器环境下系统调用链长了3到4层。这就是为什么即使换成Ceph RBD,P99延迟仍然比虚拟机高一点。
CPU调度的影响。 K8s的CPU limit用的是CFS quota,不是硬隔离。当节点上多个Pod争CPU时,MySQL线程可能被频繁调度切换。我观察过perf的上下文切换计数,容器环境下的voluntary context switch比虚拟机高20%。这对InnoDB的后台线程(page cleaner、log writer)影响尤其明显,因为它们对延迟敏感。
存储性能损耗的来源。 NFS存储的IO性能远低于本地SSD。生产环境建议用云盘或Ceph RBD。我测试过Ceph RBD,TPS差距缩小到2%以内。
避坑清单:部署MySQL上K8s最容易踩哪些坑?
删除StatefulSet前一定先确认PVC策略。 默认reclaimPolicy是Delete,我手贱删了一次StatefulSet,PVC跟着删了,测试库数据直接没了。改成Retain至少多一层保护,删之前先确认。生产环境绝对不能留Delete。
MySQL的内存限制要算清楚。 没设置resources.limits.memory,MySQL跑着跑着内存飙升,Linux OOM Killer把它杀了,Pod重启,无限循环。limits必须设,而且要比innodb_buffer_pool_size大至少20%。简单算法:buffer_pool +(max_connections × 2MB)。只设buffer_pool不设limits,OOM Killer随时等你。
Headless Service必须提前创建。 StatefulSet的Pod之间用域名通信,mysql-0.mysql.default.svc.cluster.local。我一开始忘了创建Headless Service,域名解析失败,从库连不上主库。StatefulSet必须配serviceName,对应的Headless Service必须在部署前建好。
# Headless Service(必须与StatefulSet的serviceName一致)
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # Headless,不分配ClusterIP
selector:
app: mysql
ports:
- port: 3306
name: mysql
StorageClass的选型直接决定性能上限。 我用NFS跑压测,TPS只有本地磁盘的三分之一。换成Ceph RBD后才接近虚拟机水平,差距缩小到2%以内。NFS适合测试环境,生产环境用块存储。
国产数据库容器化:能替代MySQL跑在K8s上吗?
说实话,这次上手K8s让我意识到一件事:数据库容器化不是"能不能跑"的问题,是"怎么管好"的问题。传统数据库厂商都在做容器化适配。我了解到的情况是,国产数据库对K8s的支持也在快速推进。
KingbaseES在K8s上提供了KES Operator作为核心编排工具,每个租户拥有独立的数据库实例运行在独立Pod中,实现配置、数据与权限的全隔离。这套方案定位为微服务场景,适合一机多业务——单台物理服务器上通过K8s动态调度运行多个独立的数据库实例,实现资源池共享和利用率最大化。故障时Pod可自动漂移,保障业务连续性。搭配企业级管控平台还能统一管理多个容器数据库实例。
容器化的核心价值不是性能,而是运维效率。自动部署、自动扩容、自动故障恢复,这些对中小团队吸引力很大。读从库、开发测试环境、临时数据服务,适合先上容器。但核心交易系统我仍然建议谨慎。K8s的存储和网络层引入了额外的复杂度,出问题排查链路更长,你需要同时懂数据库和K8s。一机多业务的多租户场景确实有性价比,但核心库跑在容器上,一旦存储层或网络出问题,排查成本比传统虚拟机高得多。
容器化总结:MySQL上K8s到底值不值?
把MySQL搬上K8s,核心三件事:选StatefulSet不选Deployment、配好持久化存储、用Operator管主从。性能损失在可接受范围。运维效率提升明显。但排查问题的门槛也提高了。你需要同时理解MySQL和K8s的运维逻辑。
容器化不是银弹。它改变了数据库的部署方式,但没有改变数据库的运行原理。索引该建还是要建,慢查询该调还是要调。
你的团队有把数据库跑在容器上的吗?体验怎么样?欢迎一起交流。
我是数据库小学妹,咱们下篇见 👋