1. 为什么要在 Kubernetes 里跑 PXC 集群把 MySQL 跑在 Kubernetes 上,很多人第一反应是“有状态服务上容器,这不是自找麻烦吗”。我一开始也这么想,直到手里几个业务系统的数据库单点故障越来越频繁,主从切换靠脚本加人工,半夜被叫起来处理主库挂掉的事发生了好几次,才下决心把数据库这层也搬到 K8s 里统一编排。选 PXC(Percona XtraDB Cluster)而不是普通 MySQL 主从,核心原因就一个:它自带多主同步复制和自动选主能力,节点挂了集群自己收敛,不需要外部中间件来管故障转移。PXC 的本质是 Galera Cluster 的一个发行版,三个及以上节点组成一个集群,任意节点都能读写,写入时通过认证协议在集群内广播事务,多数节点确认后才提交。这套机制决定了它对网络延迟和节点数量很敏感,也决定了它在 K8s 里的部署方式和普通无状态应用完全不同——你需要 StatefulSet 来保证稳定的网络标识和存储,需要 Headless Service 来做节点发现,需要 PVC 来保证数据持久化,还需要一个初始化引导逻辑来决定“谁是第一个启动的节点”。这篇文章面向的是已经对 Kubernetes 有基本操作能力、手里有真实数据库迁移或高可用需求的运维和开发人员。如果你只是想在本地跑个 MySQL 玩玩,那用单实例 Deployment 就够了,没必要上 PXC。但如果你面对的是生产环境、要求 RPO 接近零、能接受一定程度的写入延迟换取高可用,那 PXC on K8s 这套方案值得认真研究。下面我会从整体设计思路讲起,把每个关键环节拆开,包括我实际部署时踩过的坑和调优参数,尽量让你照着做就能跑起来。2. 整体架构设计与核心组件选型2.1 为什么是 StatefulSet 而不是 Deployment无状态应用用 Deployment,Pod 名字随机、IP 随机、存储共享,挂了重建就行。但 PXC 每个节点都有自己的数据副本,节点之间通过固定的域名互相通信,Pod 重启后必须还能被集群里其他节点认出来。StatefulSet 正好解决这个问题:它给每个 Pod 分配一个稳定的序号(pxc-0、pxc-1、pxc-2),配合 Headless Service 生成稳定的 DNS 记录(pxc-0.pxc-service.namespace.svc.cluster.local),Pod 重建后域名不变,其他节点还能找到它。这里有个细节很多人忽略:StatefulSet 的 Pod 默认是有序启动的,pxc-0 起来之后才起 pxc-1。这个特性对 PXC 初始化特别关键,因为集群第一个节点需要以 bootstrap 模式启动来初始化集群,后面的节点才能加入。如果你用 Deployment,所有 Pod 同时起来,谁都不认识谁,集群根本组不起来。2.2 Headless Service 的作用与配置要点普通 Service 会给一个 ClusterIP,流量通过 kube-proxy 负载均衡到后端 Pod。但 PXC 节点之间需要直接通信,每个节点要知道其他节点的具体地址,不能走负载均衡。Headless Service 就是把clusterIP: None设上,这样 Service 不给虚拟 IP,而是直接返回所有后端 Pod 的 DNS A 记录。PXC 节点通过查询这个 Service 的 DNS 就能拿到集群里所有节点的 IP,实现自动发现。配置上要注意两点:一是selector必须匹配 StatefulSet 的 Pod 标签,二是端口要暴露 PXC 通信需要的几个端口。PXC 用到 3306(MySQL 客户端)、4567(Galera 复制)、4568(IST 增量同步)、4444(SST 全量同步)。这几个端口少一个集群都起不来,我见过有人只开了 3306,结果节点一直卡在 joining 状态。2.3 存储方案选择:Local PV 还是网络存储数据库对磁盘 IO 极其敏感,存储选型直接决定集群性能。K8s 里常见的有几类:存储类型延迟可用性适用场景Local PV(本地 SSD)极低节点故障数据丢失对性能要求极高、有副本冗余网络块存储(如 Ceph RBD)中等跨节点可迁移通用生产环境NFS较高共享方便测试环境、低并发我的建议是:生产环境优先用网络块存储,虽然延迟比本地盘高一些,但 Pod 漂移到其他节点时数据还能挂载回来。Local PV 性能最好,但节点挂了数据就没了,除非你的 PXC 副本数足够多、能接受单节点数据丢失后重新同步。测试环境用 NFS 就够了,别在测试环境纠结性能。存储容量上,PXC 每个节点都存全量数据,所以 PVC 大小要按单节点全量数据量来算,不是总量除以节点数。比如你有 500G 数据,三个节点每个都要 500G 以上的 PVC,再留 30% 余量给 binlog 和临时表。2.4 镜像选择:Percona 官方镜像 vs 自建镜像Percona 官方提供了percona/percona-xtradb-cluster镜像,开箱即用,内置了集群启动脚本,能根据环境变量自动判断是 bootstrap 还是 join。我强烈建议直接用官方镜像,别自己从 MySQL 镜像改,因为 PXC 的启动逻辑涉及 Galera 的引导判断,自己写脚本很容易在节点重启时出现脑裂。镜像版本选择上,5.7 和 8.0 都有官方支持。8.0 在性能和 SQL 特性上更好,但如果你现有业务还在 5.7,迁移到 8.0 要注意字符集和认证插件的兼容性。我目前生产用的是 8.0.32 这个版本,跑了半年多没出过集群层面的问题。3. 部署前的环境准备与关键参数计算3.1 集群规模怎么定:三个节点是底线PXC 基于 Galera 的多数派协议,集群要能正常工作,存活节点数必须大于总数的一半。这意味着:1 个节点:没有高可用,挂了就没了2 个节点:坏一个就只剩一个,达不到多数派,集群只读3 个节点:坏一个还剩两个,多数派成立,集群继续可写5 个节点:能容忍两个节点同时故障所以最小生产规模是 3 节点。有人为了省钱用 2 节点,结果一个节点维护重启,整个集群就只读了,业务直接受影响。如果预算实在紧张,可以用 3 个节点但配置低一些,也别用 2 个高配节点。节点数也不是越多越好。Galera 的写入需要所有节点认证,节点越多,认证阶段的网络往返越多,写入延迟越高。3 到 5 个节点是比较平衡的选择,超过 7 个节点写入性能下降会很明显。3.2 资源请求与限制的合理设置数据库 Pod 的资源设置有个原则:requests 要保证,limits 要留余量。PXC 节点在同步大事务时内存和 CPU 会短时飙升,如果 limits 设得太紧,OOMKilled 或者 CPU 被限流,会导致节点被踢出集群。我一般这样配(以 8 核 16G 的节点为例):resources: requests: memory: "8Gi" cpu: "2" limits: memory: "14Gi" cpu: "6"内存 limits 留 2G 给系统和其他进程,别把 16G 全给 MySQL。MySQL 的innodb_buffer_pool_size一般设为容器内存的 50% 到 70%,比如 limits 是 14G,buffer pool 设 8G 到 10G 比较合适。设太大反而会因为内存不足触发 swap,性能断崖式下跌。CPU 的 limits 不要设成和 requests 一样,否则节点突发负载时会被 throttle。我见过有人 requests 和 limits 都设 2 核,结果 SST 全量同步时 CPU 打满被限流,同步超时失败,节点一直加不进集群。3.3 网络与 DNS 配置的坑K8s 集群的 DNS 解析默认走 CoreDNS,StatefulSet 的 Pod 域名解析需要 Headless Service 配合。这里有个常见问题:Pod 的hostname和subdomain必须设置,否则 DNS 记录生成不出来。spec: hostname: pxc subdomain: pxc-service这样 pxc-0 的完整域名就是pxc-0.pxc-service.default.svc.cluster.local。PXC 的集群地址配置里要写这个完整域名,不能只写 Pod 名。另外,如果你的 K8s 集群跨多个可用区,要注意节点间网络延迟。Galera 对延迟很敏感,跨区延迟超过 5ms 就会出现明显的写入性能下降,超过 50ms 基本不可用。尽量把 PXC 节点调度到同一区域,用podAntiAffinity让它们分散在不同物理节点上保证高可用,但别跨太远。4. 完整部署实操:从零到集群可用4.1 创建 Namespace 和 ConfigMap先把命名空间建好,所有资源都放里面,方便管理。kubectl create namespace pxc-demo然后是 ConfigMap,把 PXC 的配置文件挂进去。核心配置项我列一下:apiVersion: v1 kind: ConfigMap metadata: name: pxc-config namespace: pxc-demo data: custom.cnf: | [mysqld] default_storage_engine=InnoDB innodb_buffer_pool_size=8G innodb_log_file_size=1G innodb_flush_log_at_trx_commit=2 wsrep_provider_options="gcache.size=2G;gcs.fc_limit=256" wsrep_slave_threads=8 wsrep_sst_method=xtrabackup-v2 max_connections=1000这里几个参数值得解释。innodb_flush_log_at_trx_commit=2是把刷盘频率从每次事务改成每秒一次,牺牲一点持久性换性能,PXC 本身有副本冗余,单节点掉电丢一秒数据可以接受。gcache.size是 Galera 的写缓存,设大一点能让新加入的节点走 IST 增量同步而不是 SST 全量同步,省时间。wsrep_slave_threads是并行应用事务的线程数,设成 CPU 核数左右比较合适。4.2 部署 Headless ServiceapiVersion: v1 kind: Service metadata: name: pxc-service namespace: pxc-demo spec: clusterIP: None selector: app: pxc ports: - name: mysql port: 3306 - name: galera port: 4567 - name: ist port: 4568 - name: sst port: 4444注意clusterIP: None这行,这是 Headless Service 的标志。端口名要和后面 StatefulSet 里的容器端口名对应上。4.3 StatefulSet 核心配置逐项拆解StatefulSet 是整个部署的核心,我把它拆成几块讲。副本数和 Service 关联:apiVersion: apps/v1 kind: StatefulSet metadata: name: pxc namespace: pxc-demo spec: serviceName: pxc-service replicas: 3 podManagementPolicy: OrderedReadyserviceName必须指向刚才建的 Headless Service,这是 Pod 域名能解析的前提。podManagementPolicy用默认的OrderedReady,保证 pxc-0 先起来。容器配置:containers: - name: pxc image: percona/percona-xtradb-cluster:8.0.32 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: pxc-secret key: root-password - name: CLUSTER_NAME value: pxc-cluster - name: XTRABACKUP_PASSWORD valueFrom: secretKeyRef: name: pxc-secret key: xtrabackup-password - name: DISCOVERY_SERVICE value: pxc-service ports: - containerPort: 3306 - containerPort: 4567 - containerPort: 4568 - containerPort: 4444DISCOVERY_SERVICE指向 Headless Service 名字,官方镜像的启动脚本会去查这个 Service 的 DNS 来发现集群成员。密码用 Secret 管理,别明文写在 YAML 里。存储挂载:volumeMounts: - name: data mountPath: /var/lib/mysql - name: config mountPath: /etc/percona-xtradb-cluster.conf.d/custom.cnf subPath: custom.cnf数据目录挂 PVC,配置文件从 ConfigMap 挂进来。subPath的用法是把 ConfigMap 里的单个 key 挂成文件,而不是覆盖整个目录。volumeClaimTemplates:volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: "fast-ssd" resources: requests: storage: 500Gi每个 Pod 会根据自己的序号生成独立的 PVC,比如># 先部署 replicas: 1 kubectl apply -f pxc-statefulset.yaml # 等 pxc-0 变成 Running 且 Ready kubectl get pods -n pxc-demo -w # 确认集群状态 kubectl exec -n pxc-demo pxc-0 -- mysql -uroot -p -e "show status like 'wsrep_cluster_size'"看到wsrep_cluster_size是 1,说明单节点集群起来了。然后再扩容:kubectl scale statefulset pxc -n pxc-demo --replicas=3pxc-1 和 pxc-2 启动时会自动去查pxc-service的 DNS,发现 pxc-0 已经在跑,就以 join 模式加入集群。加入过程中会做 SST 全量同步,把 pxc-0 的数据全量拷贝过来,数据量大的话这一步会比较久。注意:扩容前一定要确认 pxc-0 的wsrep_cluster_size是 1 且wsrep_ready是 ON,否则新节点可能加入失败或者形成两个独立集群(脑裂)。4.5 验证集群状态的关键命令集群起来之后,别急着上业务,先验证状态。进任意一个节点执行:-- 集群大小,应该是 3 show status like 'wsrep_cluster_size'; -- 节点是否就绪,应该是 ON show status like 'wsrep_ready'; -- 集群状态,应该是 Synced show status like 'wsrep_local_state_comment'; -- 集群中所有节点的状态 show status like 'wsrep_incoming_addresses'; -- 复制队列,正常应该是 0 show status like 'wsrep_local_recv_queue';wsrep_local_state_comment是Synced才说明节点完全同步好了。如果是Joining或者Donor,说明还在同步数据,这时候别切流量过来。wsrep_local_recv_queue持续大于 0 说明节点应用事务跟不上,可能是wsrep_slave_threads设小了。5. 常见故障排查与运维经验5.1 节点一直卡在 Joining 状态这是最常见的故障。现象是 Pod 是 Running,但wsrep_local_state_comment一直是Joining,集群大小也不对。排查思路按这个顺序来:看日志:kubectl logs pxc-1 -n pxc-demo,重点看 SST 相关的报错。常见的是 xtrabackup 版本不匹配、SST 端口不通、磁盘空间不足。检查网络:从 pxc-1 里telnet pxc-0.pxc-service 4444,看 SST 端口通不通。NetworkPolicy 如果配了,要放行 4444、4568、4567 这几个端口。检查磁盘:SST 全量同步需要和目标节点数据量相当的临时空间,PVC 满了会直接失败。检查 gcache:如果 gcache 太小,新节点没法做 IST 增量同步,只能走 SST 全量,数据量大时容易超时。我遇到过一次是 PVC 空间不够,SST 传到一半失败,节点反复重试。扩容 PVC 后重新加入就好了。5.2 集群脑裂:最危险的状态脑裂是指集群分裂成两个独立的部分,各自认为自己是主集群,都接受写入。这种情况数据会不一致,恢复起来非常麻烦。脑裂的根因通常是网络分区或者节点数配置不当。比如 3 节点集群,网络抖动导致一个节点和另外两个失联,失联的那个节点如果还接受写入,就脑裂了。Galera 的多数派机制本来能防止这个,但如果配置了pc.weight或者evs相关参数不当,可能绕过保护。预防措施:节点数用奇数,3 或 5不要随意调整pc.weight,除非你很清楚在做什么网络层面保证节点间通信稳定,别跨高延迟区域监控wsrep_cluster_status,正常是Primary,变成non-Primary说明节点已经脱离集群如果真脑裂了,先停掉所有节点的写入,然后选数据最全的那个节点做 bootstrap 重建集群,其他节点清空数据重新加入。这个过程会丢数据,所以预防远比恢复重要。5.3 写入性能突然下降PXC 的写入性能受几个因素影响,按影响程度排序:因素影响排查方法网络延迟高节点间 ping 延迟,应小于 5ms大事务高查wsrep_local_recv_queue,大事务会阻塞节点数过多中超过 5 节点认证开销明显磁盘 IO中查 iostat,IO util 持续 100% 说明瓶颈参数配置低检查wsrep_slave_threads、innodb_flush_log_at_trx_commit大事务是 PXC 的天敌。一个 10 万行的批量插入,在单机 MySQL 上可能几秒就完了,但在 PXC 里,这个事务要在所有节点上认证和应用,任何一个节点慢都会拖累整个集群。建议把大事务拆成小批次,比如每 1000 行提交一次。5.4 节点重启后无法重新加入节点维护重启后,如果 gcache 里还有它需要的增量数据,会走 IST 快速同步;如果 gcache 已经被覆盖,就只能走 SST 全量同步。SST 期间该节点不可用,而且会从 donor 节点拷贝全量数据,对 donor 节点也有压力。减少 SST 频率的办法:把gcache.size设大,能缓存更多增量节点重启前先手动wsrep_desync,让它优雅退出维护窗口尽量短,别让节点离线太久如果节点重启后一直加不进去,最干脆的办法是清空该节点的数据目录,让它重新做 SST。虽然慢,但比反复重试靠谱。kubectl exec -n pxc-demo pxc-1 -- rm -rf /var/lib/mysql/* kubectl delete pod pxc-1 -n pxc-demoPod 重建后会自动重新加入集群,走 SST 全量同步。5.5 备份策略:别只依赖集群冗余PXC 的副本冗余能防节点故障,但防不了误删库、误更新这种逻辑错误。必须做定期逻辑备份。我用的方案是每天凌晨用mysqldump或xtrabackup做一次全量备份,存到对象存储。备份时要注意:从单个节点备份,别从多个节点同时备份,浪费资源用--single-transaction保证一致性快照备份文件要验证可恢复性,别等到真需要恢复时才发现备份是坏的kubectl exec -n pxc-demo pxc-0 -- mysqldump -uroot -p \ --single-transaction --routines --triggers \ --all-databases > backup-$(date +%Y%m%d).sql恢复的时候,先停掉集群写入,选一个节点导入备份,然后让其他节点重新 SST 同步。这个过程要谨慎,最好在测试环境演练过再上生产。6. 生产环境调优与监控要点6.1 关键监控指标PXC 集群要监控的指标比单机 MySQL 多一层,除了常规的 QPS、连接数、慢查询,还要盯 Galera 的状态:wsrep_cluster_size:集群节点数,掉了说明有节点离线wsrep_cluster_status:必须是Primary,non-Primary说明节点脱离集群wsrep_ready:必须是ON,OFF说明节点不能接受写入wsrep_local_recv_queue:接收队列,持续大于 0 说明应用事务跟不上wsrep_flow_control_paused:流控暂停比例,大于 0.05 说明集群压力大wsrep_local_state_comment:节点状态,Synced才正常这些指标用 Prometheus 的 mysqld_exporter 采集,配 Grafana 面板展示。告警规则我设的是:wsrep_cluster_size小于预期值持续 1 分钟就告警,wsrep_local_recv_queue大于 100 持续 5 分钟告警。6.2 参数调优的取舍PXC 的参数调优本质是在一致性、性能、可用性之间找平衡。几个关键取舍:同步复制 vs 异步复制:PXC 默认是同步复制,写入要等多数节点确认。如果业务能接受异步,可以调wsrep_sync_wait=0,但这样就不保证强一致了。金融类业务别动这个参数。流控阈值:gcs.fc_limit控制流控触发阈值,默认 16。调大能减少流控频率,但节点落后太多时恢复更慢。我一般设 256,配合wsrep_slave_threads=8,在写入压力和同步延迟之间取平衡。SST 方法:wsrep_sst_method有xtrabackup-v2、rsync、mariabackup几个选项。xtrabackup-v2支持增量,是首选。rsync会锁表,生产环境别用。6.3 滚动升级的正确姿势PXC 版本升级或者参数变更需要重启节点,必须逐个滚动操作,不能同时重启多个。流程是:从集群里摘掉一个节点(wsrep_desync=ON)重启该节点,等它重新同步好(wsrep_local_state_comment=Synced)恢复该节点(wsrep_desync=OFF)等集群稳定几分钟,再操作下一个节点整个过程集群保持可用,但性能会有波动,因为少了一个节点分担。建议在业务低峰期做。注意:升级前一定要备份,并且确认新版本和旧版本的 Galera 协议兼容。跨大版本升级(比如 5.7 到 8.0)不能滚动升级,需要逻辑导出导入。7. 我踩过的几个坑和最后的小建议第一个坑是忘了配 PodAntiAffinity,结果三个 PXC Pod 被调度到同一个物理节点上,那个节点一挂,整个集群全没了。后来加了反亲和规则,强制分散到不同节点:affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: pxc topologyKey: kubernetes.io/hostname第二个坑是PVC 用了默认的 StorageClass,那个存储类是基于机械盘的,IOPS 低得可怜,集群跑起来后写入延迟一直在 100ms 以上。换成 SSD 存储类后降到 5ms 以内。数据库对存储的要求真的不能省。第三个坑是没设wsrep_sst_donor,SST 时随机选 donor 节点,有时候选到一个负载已经很高的节点,导致 SST 超时。后来指定了固定的 donor 优先级,让负载低的节点优先做 donor。最后分享一个小技巧:在 PXC 前面挂一个 ProxySQL 或者 HAProxy 做读写分离和故障转移。PXC 虽然多主,但让应用直接连多个节点会有连接管理问题,而且节点故障时应用要自己处理重连。加一层代理,应用只连代理,代理负责健康检查和路由,应用层就简单多了。ProxySQL 还能做读写分离,把读流量分散到多个节点,减轻写入节点的压力。这套方案我在几个生产环境跑了快一年,经历过节点故障、网络抖动、大事务冲击,集群都能自己恢复,没出现过数据丢失。K8s 管有状态服务确实比无状态麻烦,但一旦跑顺了,运维效率比传统部署高很多。希望这些经验能帮你少走点弯路。
1. 为什么要在 Kubernetes 里跑 PXC 集群把 MySQL 跑在 Kubernetes 上,很多人第一反应是“有状态服务上容器,这不是自找麻烦吗”。我一开始也这么想,直到手里几个业务系统的数据库单点故障越来越频繁,主从切换靠脚本加人工,半夜被叫起来处理主库挂掉的事发生了好几次,才下决心把数据库这层也搬到 K8s 里统一编排。选 PXC(Percona XtraDB Cluster)而不是普通 MySQL 主从,核心原因就一个:它自带多主同步复制和自动选主能力,节点挂了集群自己收敛,不需要外部中间件来管故障转移。PXC 的本质是 Galera Cluster 的一个发行版,三个及以上节点组成一个集群,任意节点都能读写,写入时通过认证协议在集群内广播事务,多数节点确认后才提交。这套机制决定了它对网络延迟和节点数量很敏感,也决定了它在 K8s 里的部署方式和普通无状态应用完全不同——你需要 StatefulSet 来保证稳定的网络标识和存储,需要 Headless Service 来做节点发现,需要 PVC 来保证数据持久化,还需要一个初始化引导逻辑来决定“谁是第一个启动的节点”。这篇文章面向的是已经对 Kubernetes 有基本操作能力、手里有真实数据库迁移或高可用需求的运维和开发人员。如果你只是想在本地跑个 MySQL 玩玩,那用单实例 Deployment 就够了,没必要上 PXC。但如果你面对的是生产环境、要求 RPO 接近零、能接受一定程度的写入延迟换取高可用,那 PXC on K8s 这套方案值得认真研究。下面我会从整体设计思路讲起,把每个关键环节拆开,包括我实际部署时踩过的坑和调优参数,尽量让你照着做就能跑起来。2. 整体架构设计与核心组件选型2.1 为什么是 StatefulSet 而不是 Deployment无状态应用用 Deployment,Pod 名字随机、IP 随机、存储共享,挂了重建就行。但 PXC 每个节点都有自己的数据副本,节点之间通过固定的域名互相通信,Pod 重启后必须还能被集群里其他节点认出来。StatefulSet 正好解决这个问题:它给每个 Pod 分配一个稳定的序号(pxc-0、pxc-1、pxc-2),配合 Headless Service 生成稳定的 DNS 记录(pxc-0.pxc-service.namespace.svc.cluster.local),Pod 重建后域名不变,其他节点还能找到它。这里有个细节很多人忽略:StatefulSet 的 Pod 默认是有序启动的,pxc-0 起来之后才起 pxc-1。这个特性对 PXC 初始化特别关键,因为集群第一个节点需要以 bootstrap 模式启动来初始化集群,后面的节点才能加入。如果你用 Deployment,所有 Pod 同时起来,谁都不认识谁,集群根本组不起来。2.2 Headless Service 的作用与配置要点普通 Service 会给一个 ClusterIP,流量通过 kube-proxy 负载均衡到后端 Pod。但 PXC 节点之间需要直接通信,每个节点要知道其他节点的具体地址,不能走负载均衡。Headless Service 就是把clusterIP: None设上,这样 Service 不给虚拟 IP,而是直接返回所有后端 Pod 的 DNS A 记录。PXC 节点通过查询这个 Service 的 DNS 就能拿到集群里所有节点的 IP,实现自动发现。配置上要注意两点:一是selector必须匹配 StatefulSet 的 Pod 标签,二是端口要暴露 PXC 通信需要的几个端口。PXC 用到 3306(MySQL 客户端)、4567(Galera 复制)、4568(IST 增量同步)、4444(SST 全量同步)。这几个端口少一个集群都起不来,我见过有人只开了 3306,结果节点一直卡在 joining 状态。2.3 存储方案选择:Local PV 还是网络存储数据库对磁盘 IO 极其敏感,存储选型直接决定集群性能。K8s 里常见的有几类:存储类型延迟可用性适用场景Local PV(本地 SSD)极低节点故障数据丢失对性能要求极高、有副本冗余网络块存储(如 Ceph RBD)中等跨节点可迁移通用生产环境NFS较高共享方便测试环境、低并发我的建议是:生产环境优先用网络块存储,虽然延迟比本地盘高一些,但 Pod 漂移到其他节点时数据还能挂载回来。Local PV 性能最好,但节点挂了数据就没了,除非你的 PXC 副本数足够多、能接受单节点数据丢失后重新同步。测试环境用 NFS 就够了,别在测试环境纠结性能。存储容量上,PXC 每个节点都存全量数据,所以 PVC 大小要按单节点全量数据量来算,不是总量除以节点数。比如你有 500G 数据,三个节点每个都要 500G 以上的 PVC,再留 30% 余量给 binlog 和临时表。2.4 镜像选择:Percona 官方镜像 vs 自建镜像Percona 官方提供了percona/percona-xtradb-cluster镜像,开箱即用,内置了集群启动脚本,能根据环境变量自动判断是 bootstrap 还是 join。我强烈建议直接用官方镜像,别自己从 MySQL 镜像改,因为 PXC 的启动逻辑涉及 Galera 的引导判断,自己写脚本很容易在节点重启时出现脑裂。镜像版本选择上,5.7 和 8.0 都有官方支持。8.0 在性能和 SQL 特性上更好,但如果你现有业务还在 5.7,迁移到 8.0 要注意字符集和认证插件的兼容性。我目前生产用的是 8.0.32 这个版本,跑了半年多没出过集群层面的问题。3. 部署前的环境准备与关键参数计算3.1 集群规模怎么定:三个节点是底线PXC 基于 Galera 的多数派协议,集群要能正常工作,存活节点数必须大于总数的一半。这意味着:1 个节点:没有高可用,挂了就没了2 个节点:坏一个就只剩一个,达不到多数派,集群只读3 个节点:坏一个还剩两个,多数派成立,集群继续可写5 个节点:能容忍两个节点同时故障所以最小生产规模是 3 节点。有人为了省钱用 2 节点,结果一个节点维护重启,整个集群就只读了,业务直接受影响。如果预算实在紧张,可以用 3 个节点但配置低一些,也别用 2 个高配节点。节点数也不是越多越好。Galera 的写入需要所有节点认证,节点越多,认证阶段的网络往返越多,写入延迟越高。3 到 5 个节点是比较平衡的选择,超过 7 个节点写入性能下降会很明显。3.2 资源请求与限制的合理设置数据库 Pod 的资源设置有个原则:requests 要保证,limits 要留余量。PXC 节点在同步大事务时内存和 CPU 会短时飙升,如果 limits 设得太紧,OOMKilled 或者 CPU 被限流,会导致节点被踢出集群。我一般这样配(以 8 核 16G 的节点为例):resources: requests: memory: "8Gi" cpu: "2" limits: memory: "14Gi" cpu: "6"内存 limits 留 2G 给系统和其他进程,别把 16G 全给 MySQL。MySQL 的innodb_buffer_pool_size一般设为容器内存的 50% 到 70%,比如 limits 是 14G,buffer pool 设 8G 到 10G 比较合适。设太大反而会因为内存不足触发 swap,性能断崖式下跌。CPU 的 limits 不要设成和 requests 一样,否则节点突发负载时会被 throttle。我见过有人 requests 和 limits 都设 2 核,结果 SST 全量同步时 CPU 打满被限流,同步超时失败,节点一直加不进集群。3.3 网络与 DNS 配置的坑K8s 集群的 DNS 解析默认走 CoreDNS,StatefulSet 的 Pod 域名解析需要 Headless Service 配合。这里有个常见问题:Pod 的hostname和subdomain必须设置,否则 DNS 记录生成不出来。spec: hostname: pxc subdomain: pxc-service这样 pxc-0 的完整域名就是pxc-0.pxc-service.default.svc.cluster.local。PXC 的集群地址配置里要写这个完整域名,不能只写 Pod 名。另外,如果你的 K8s 集群跨多个可用区,要注意节点间网络延迟。Galera 对延迟很敏感,跨区延迟超过 5ms 就会出现明显的写入性能下降,超过 50ms 基本不可用。尽量把 PXC 节点调度到同一区域,用podAntiAffinity让它们分散在不同物理节点上保证高可用,但别跨太远。4. 完整部署实操:从零到集群可用4.1 创建 Namespace 和 ConfigMap先把命名空间建好,所有资源都放里面,方便管理。kubectl create namespace pxc-demo然后是 ConfigMap,把 PXC 的配置文件挂进去。核心配置项我列一下:apiVersion: v1 kind: ConfigMap metadata: name: pxc-config namespace: pxc-demo data: custom.cnf: | [mysqld] default_storage_engine=InnoDB innodb_buffer_pool_size=8G innodb_log_file_size=1G innodb_flush_log_at_trx_commit=2 wsrep_provider_options="gcache.size=2G;gcs.fc_limit=256" wsrep_slave_threads=8 wsrep_sst_method=xtrabackup-v2 max_connections=1000这里几个参数值得解释。innodb_flush_log_at_trx_commit=2是把刷盘频率从每次事务改成每秒一次,牺牲一点持久性换性能,PXC 本身有副本冗余,单节点掉电丢一秒数据可以接受。gcache.size是 Galera 的写缓存,设大一点能让新加入的节点走 IST 增量同步而不是 SST 全量同步,省时间。wsrep_slave_threads是并行应用事务的线程数,设成 CPU 核数左右比较合适。4.2 部署 Headless ServiceapiVersion: v1 kind: Service metadata: name: pxc-service namespace: pxc-demo spec: clusterIP: None selector: app: pxc ports: - name: mysql port: 3306 - name: galera port: 4567 - name: ist port: 4568 - name: sst port: 4444注意clusterIP: None这行,这是 Headless Service 的标志。端口名要和后面 StatefulSet 里的容器端口名对应上。4.3 StatefulSet 核心配置逐项拆解StatefulSet 是整个部署的核心,我把它拆成几块讲。副本数和 Service 关联:apiVersion: apps/v1 kind: StatefulSet metadata: name: pxc namespace: pxc-demo spec: serviceName: pxc-service replicas: 3 podManagementPolicy: OrderedReadyserviceName必须指向刚才建的 Headless Service,这是 Pod 域名能解析的前提。podManagementPolicy用默认的OrderedReady,保证 pxc-0 先起来。容器配置:containers: - name: pxc image: percona/percona-xtradb-cluster:8.0.32 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: pxc-secret key: root-password - name: CLUSTER_NAME value: pxc-cluster - name: XTRABACKUP_PASSWORD valueFrom: secretKeyRef: name: pxc-secret key: xtrabackup-password - name: DISCOVERY_SERVICE value: pxc-service ports: - containerPort: 3306 - containerPort: 4567 - containerPort: 4568 - containerPort: 4444DISCOVERY_SERVICE指向 Headless Service 名字,官方镜像的启动脚本会去查这个 Service 的 DNS 来发现集群成员。密码用 Secret 管理,别明文写在 YAML 里。存储挂载:volumeMounts: - name: data mountPath: /var/lib/mysql - name: config mountPath: /etc/percona-xtradb-cluster.conf.d/custom.cnf subPath: custom.cnf数据目录挂 PVC,配置文件从 ConfigMap 挂进来。subPath的用法是把 ConfigMap 里的单个 key 挂成文件,而不是覆盖整个目录。volumeClaimTemplates:volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: "fast-ssd" resources: requests: storage: 500Gi每个 Pod 会根据自己的序号生成独立的 PVC,比如># 先部署 replicas: 1 kubectl apply -f pxc-statefulset.yaml # 等 pxc-0 变成 Running 且 Ready kubectl get pods -n pxc-demo -w # 确认集群状态 kubectl exec -n pxc-demo pxc-0 -- mysql -uroot -p -e "show status like 'wsrep_cluster_size'"看到wsrep_cluster_size是 1,说明单节点集群起来了。然后再扩容:kubectl scale statefulset pxc -n pxc-demo --replicas=3pxc-1 和 pxc-2 启动时会自动去查pxc-service的 DNS,发现 pxc-0 已经在跑,就以 join 模式加入集群。加入过程中会做 SST 全量同步,把 pxc-0 的数据全量拷贝过来,数据量大的话这一步会比较久。注意:扩容前一定要确认 pxc-0 的wsrep_cluster_size是 1 且wsrep_ready是 ON,否则新节点可能加入失败或者形成两个独立集群(脑裂)。4.5 验证集群状态的关键命令集群起来之后,别急着上业务,先验证状态。进任意一个节点执行:-- 集群大小,应该是 3 show status like 'wsrep_cluster_size'; -- 节点是否就绪,应该是 ON show status like 'wsrep_ready'; -- 集群状态,应该是 Synced show status like 'wsrep_local_state_comment'; -- 集群中所有节点的状态 show status like 'wsrep_incoming_addresses'; -- 复制队列,正常应该是 0 show status like 'wsrep_local_recv_queue';wsrep_local_state_comment是Synced才说明节点完全同步好了。如果是Joining或者Donor,说明还在同步数据,这时候别切流量过来。wsrep_local_recv_queue持续大于 0 说明节点应用事务跟不上,可能是wsrep_slave_threads设小了。5. 常见故障排查与运维经验5.1 节点一直卡在 Joining 状态这是最常见的故障。现象是 Pod 是 Running,但wsrep_local_state_comment一直是Joining,集群大小也不对。排查思路按这个顺序来:看日志:kubectl logs pxc-1 -n pxc-demo,重点看 SST 相关的报错。常见的是 xtrabackup 版本不匹配、SST 端口不通、磁盘空间不足。检查网络:从 pxc-1 里telnet pxc-0.pxc-service 4444,看 SST 端口通不通。NetworkPolicy 如果配了,要放行 4444、4568、4567 这几个端口。检查磁盘:SST 全量同步需要和目标节点数据量相当的临时空间,PVC 满了会直接失败。检查 gcache:如果 gcache 太小,新节点没法做 IST 增量同步,只能走 SST 全量,数据量大时容易超时。我遇到过一次是 PVC 空间不够,SST 传到一半失败,节点反复重试。扩容 PVC 后重新加入就好了。5.2 集群脑裂:最危险的状态脑裂是指集群分裂成两个独立的部分,各自认为自己是主集群,都接受写入。这种情况数据会不一致,恢复起来非常麻烦。脑裂的根因通常是网络分区或者节点数配置不当。比如 3 节点集群,网络抖动导致一个节点和另外两个失联,失联的那个节点如果还接受写入,就脑裂了。Galera 的多数派机制本来能防止这个,但如果配置了pc.weight或者evs相关参数不当,可能绕过保护。预防措施:节点数用奇数,3 或 5不要随意调整pc.weight,除非你很清楚在做什么网络层面保证节点间通信稳定,别跨高延迟区域监控wsrep_cluster_status,正常是Primary,变成non-Primary说明节点已经脱离集群如果真脑裂了,先停掉所有节点的写入,然后选数据最全的那个节点做 bootstrap 重建集群,其他节点清空数据重新加入。这个过程会丢数据,所以预防远比恢复重要。5.3 写入性能突然下降PXC 的写入性能受几个因素影响,按影响程度排序:因素影响排查方法网络延迟高节点间 ping 延迟,应小于 5ms大事务高查wsrep_local_recv_queue,大事务会阻塞节点数过多中超过 5 节点认证开销明显磁盘 IO中查 iostat,IO util 持续 100% 说明瓶颈参数配置低检查wsrep_slave_threads、innodb_flush_log_at_trx_commit大事务是 PXC 的天敌。一个 10 万行的批量插入,在单机 MySQL 上可能几秒就完了,但在 PXC 里,这个事务要在所有节点上认证和应用,任何一个节点慢都会拖累整个集群。建议把大事务拆成小批次,比如每 1000 行提交一次。5.4 节点重启后无法重新加入节点维护重启后,如果 gcache 里还有它需要的增量数据,会走 IST 快速同步;如果 gcache 已经被覆盖,就只能走 SST 全量同步。SST 期间该节点不可用,而且会从 donor 节点拷贝全量数据,对 donor 节点也有压力。减少 SST 频率的办法:把gcache.size设大,能缓存更多增量节点重启前先手动wsrep_desync,让它优雅退出维护窗口尽量短,别让节点离线太久如果节点重启后一直加不进去,最干脆的办法是清空该节点的数据目录,让它重新做 SST。虽然慢,但比反复重试靠谱。kubectl exec -n pxc-demo pxc-1 -- rm -rf /var/lib/mysql/* kubectl delete pod pxc-1 -n pxc-demoPod 重建后会自动重新加入集群,走 SST 全量同步。5.5 备份策略:别只依赖集群冗余PXC 的副本冗余能防节点故障,但防不了误删库、误更新这种逻辑错误。必须做定期逻辑备份。我用的方案是每天凌晨用mysqldump或xtrabackup做一次全量备份,存到对象存储。备份时要注意:从单个节点备份,别从多个节点同时备份,浪费资源用--single-transaction保证一致性快照备份文件要验证可恢复性,别等到真需要恢复时才发现备份是坏的kubectl exec -n pxc-demo pxc-0 -- mysqldump -uroot -p \ --single-transaction --routines --triggers \ --all-databases > backup-$(date +%Y%m%d).sql恢复的时候,先停掉集群写入,选一个节点导入备份,然后让其他节点重新 SST 同步。这个过程要谨慎,最好在测试环境演练过再上生产。6. 生产环境调优与监控要点6.1 关键监控指标PXC 集群要监控的指标比单机 MySQL 多一层,除了常规的 QPS、连接数、慢查询,还要盯 Galera 的状态:wsrep_cluster_size:集群节点数,掉了说明有节点离线wsrep_cluster_status:必须是Primary,non-Primary说明节点脱离集群wsrep_ready:必须是ON,OFF说明节点不能接受写入wsrep_local_recv_queue:接收队列,持续大于 0 说明应用事务跟不上wsrep_flow_control_paused:流控暂停比例,大于 0.05 说明集群压力大wsrep_local_state_comment:节点状态,Synced才正常这些指标用 Prometheus 的 mysqld_exporter 采集,配 Grafana 面板展示。告警规则我设的是:wsrep_cluster_size小于预期值持续 1 分钟就告警,wsrep_local_recv_queue大于 100 持续 5 分钟告警。6.2 参数调优的取舍PXC 的参数调优本质是在一致性、性能、可用性之间找平衡。几个关键取舍:同步复制 vs 异步复制:PXC 默认是同步复制,写入要等多数节点确认。如果业务能接受异步,可以调wsrep_sync_wait=0,但这样就不保证强一致了。金融类业务别动这个参数。流控阈值:gcs.fc_limit控制流控触发阈值,默认 16。调大能减少流控频率,但节点落后太多时恢复更慢。我一般设 256,配合wsrep_slave_threads=8,在写入压力和同步延迟之间取平衡。SST 方法:wsrep_sst_method有xtrabackup-v2、rsync、mariabackup几个选项。xtrabackup-v2支持增量,是首选。rsync会锁表,生产环境别用。6.3 滚动升级的正确姿势PXC 版本升级或者参数变更需要重启节点,必须逐个滚动操作,不能同时重启多个。流程是:从集群里摘掉一个节点(wsrep_desync=ON)重启该节点,等它重新同步好(wsrep_local_state_comment=Synced)恢复该节点(wsrep_desync=OFF)等集群稳定几分钟,再操作下一个节点整个过程集群保持可用,但性能会有波动,因为少了一个节点分担。建议在业务低峰期做。注意:升级前一定要备份,并且确认新版本和旧版本的 Galera 协议兼容。跨大版本升级(比如 5.7 到 8.0)不能滚动升级,需要逻辑导出导入。7. 我踩过的几个坑和最后的小建议第一个坑是忘了配 PodAntiAffinity,结果三个 PXC Pod 被调度到同一个物理节点上,那个节点一挂,整个集群全没了。后来加了反亲和规则,强制分散到不同节点:affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: pxc topologyKey: kubernetes.io/hostname第二个坑是PVC 用了默认的 StorageClass,那个存储类是基于机械盘的,IOPS 低得可怜,集群跑起来后写入延迟一直在 100ms 以上。换成 SSD 存储类后降到 5ms 以内。数据库对存储的要求真的不能省。第三个坑是没设wsrep_sst_donor,SST 时随机选 donor 节点,有时候选到一个负载已经很高的节点,导致 SST 超时。后来指定了固定的 donor 优先级,让负载低的节点优先做 donor。最后分享一个小技巧:在 PXC 前面挂一个 ProxySQL 或者 HAProxy 做读写分离和故障转移。PXC 虽然多主,但让应用直接连多个节点会有连接管理问题,而且节点故障时应用要自己处理重连。加一层代理,应用只连代理,代理负责健康检查和路由,应用层就简单多了。ProxySQL 还能做读写分离,把读流量分散到多个节点,减轻写入节点的压力。这套方案我在几个生产环境跑了快一年,经历过节点故障、网络抖动、大事务冲击,集群都能自己恢复,没出现过数据丢失。K8s 管有状态服务确实比无状态麻烦,但一旦跑顺了,运维效率比传统部署高很多。希望这些经验能帮你少走点弯路。