1. 项目概述:为什么用StatefulSet部署MySQL主从不是“炫技”,而是生产刚需
StatefulSet这个词在Kubernetes生态里,常被新手误认为是“带状态应用的万能解药”,但真正把它用对、用稳、用出生产价值的团队,往往都踩过至少三轮坑。我第一次在某金融类模拟项目X中接手MySQL主从集群迁移时,就犯了典型错误——直接套用Deployment模板,结果发现:主库Pod重启后IP漂移,从库的CHANGE MASTER TO指令彻底失效;数据目录挂载混乱,两个Pod共用同一块PV导致binlog错乱;甚至健康检查探针一触发,整个集群就进入反复重建的死循环。后来才明白,Deployment设计之初就假设所有副本完全对等、可随意销毁重建,而MySQL主从的本质是有身份、有顺序、有依赖、有持久化关系的拓扑结构。StatefulSet正是为这类场景量身定制的控制器:它保证每个Pod拥有唯一且稳定的网络标识(如mysql-0、mysql-1)、按序创建与销毁(先启主再启从)、独立绑定专属存储(每个Pod挂载独立PV),并支持滚动更新时严格遵循主从依赖链。这不是K8s的“高级功能”,而是把数据库这种核心中间件搬进容器编排体系时,绕不开的底层契约。本文聚焦的“使用StatefulSet部署MySQL主从集群”,解决的不是“能不能跑起来”的问题,而是“能不能在故障恢复、扩缩容、版本升级时不丢数据、不断服务、不破坏主从一致性”的硬性要求。适合正在规划数据库容器化落地的运维工程师、SRE、云平台架构师,以及需要理解有状态应用编排逻辑的后端开发人员。如果你只关心“5分钟跑通Demo”,那本文可能略显冗长;但如果你的目标是让MySQL在K8s里真正扛住日均百万级事务,那每一个配置项背后的取舍,都值得你逐行推敲。
2. 整体架构设计与方案选型逻辑:为什么不用Operator?为什么坚持手动编排?
2.1 主从拓扑的不可妥协性:从单点故障到脑裂风险的全链路审视
在开始写YAML之前,必须先厘清MySQL主从最核心的三个刚性约束:
第一,角色固化不可互换。主库(Master)负责写入和binlog生成,从库(Slave)仅接受读请求并回放relay log。任何自动化的“主从切换”在StatefulSet原生模型下都是危险的——StatefulSet本身不管理Pod角色状态,它只管Pod生命周期。若强行让StatefulSet控制“谁是主谁是从”,一旦发生网络分区,两个Pod可能同时认为自己是主,产生脑裂(Split-Brain),造成数据覆盖或丢失。因此,我们的设计原则是:主从角色由初始化脚本和配置文件静态定义,StatefulSet只负责稳定承载这两个固定角色的Pod。mysql-0永远是主库,mysql-1永远是从库,这个映射关系写死在initContainer的启动逻辑里,不依赖运行时选举。
第二,数据路径必须绝对隔离。主库的binlog、从库的relay log、各自的innodb数据文件,三者路径不能混用。曾有团队用一个共享NFS卷挂载所有Pod,结果主库写入的binlog被从库误删,导致复制中断数小时。我们的方案强制每个Pod绑定独立的PersistentVolumeClaim(PVC),通过volumeClaimTemplates自动生成mysql-data-mysql-0、mysql-data-mysql-1两个PVC,确保物理存储层零交叉。
第三,网络通信必须可预测。从库连接主库时,必须使用稳定DNS名(如mysql-0.mysql-headless.default.svc.cluster.local),而非Service ClusterIP——因为ClusterIP是虚拟IP,无法被MySQL的replication线程识别为有效主机名。我们为此专门创建Headless Service(无ClusterIP),让K8s DNS直接解析出Pod的Endpoint IP,这是StatefulSet实现“稳定网络标识”的关键一环。
提示:Headless Service的serviceName必须与StatefulSet的serviceName完全一致,否则DNS解析会失败。这是实操中最常被忽略的配置点,建议在编写YAML后,先用
kubectl get svc确认Service类型为ClusterIP: None。
2.2 为什么放弃MySQL Operator?手写YAML的三大现实考量
当前社区有多个MySQL Operator(如Oracle官方、Presslabs等),它们封装了高可用切换、备份恢复等高级功能。但在某电商类模拟项目X的落地评审中,我们最终选择纯YAML手写方案,原因有三:
其一,可控性优先于自动化。Operator的“智能切换”本质是监听Pod状态并执行SQL命令(如STOP SLAVE; RESET SLAVE; CHANGE MASTER TO)。但在生产环境中,一次误判可能导致从库跳过关键事务。手写方案将所有数据库操作收敛到initContainer的shell脚本中,每一步都有日志输出和exit code校验,故障时可精准定位是网络超时、权限拒绝还是SQL语法错误。
其二,轻量化降低维护成本。Operator自身是一个运行在集群内的Controller Pod,需额外维护其RBAC权限、镜像更新、监控告警。而纯YAML方案只需管理4个核心资源(ConfigMap、Headless Service、StatefulSet、StorageClass),学习曲线平缓,新成员上手快。某次线上紧急扩容时,运维同事30分钟内就完成了StatefulSet副本数调整和PVC扩容,而Operator方案需同步修改CRD定义并等待Controller reconcile。
其三,规避版本兼容陷阱。MySQL 5.7与8.0在复制协议、密码认证插件上有重大差异。Operator通常针对特定MySQL大版本优化,升级时需同步更新Operator版本。手写方案则可自由选择任意官方MySQL镜像(如mysql:5.7.42或mysql:8.0.33),只需微调initContainer中的SQL语句即可。我们在某教育类模拟项目X中,就成功复用同一套YAML模板,仅替换镜像tag和初始化SQL,就完成了从5.7到8.0的平滑迁移。
注意:手写方案不等于“裸奔”。我们仍会为StatefulSet配置livenessProbe和readinessProbe,但探针逻辑极度精简——主库只检查
mysqladmin ping -u root -p$MYSQL_ROOT_PASSWORD,从库额外增加show slave status\G | grep -q "Slave_IO_Running: Yes"。避免复杂SQL探针在高负载时拖垮数据库。
2.3 存储方案选型:Local PV vs NFS vs 云盘,为什么最终锁定云厂商块存储?
StatefulSet的存储方案选择,直接决定集群的IOPS上限和故障域范围。我们对比了三种主流方案:
- Local PV(本地磁盘):性能最优(直通SSD),但存在严重缺陷——Pod被调度到其他节点时,数据无法跟随迁移。若节点宕机,该Pod的数据即永久丢失。这违背了K8s“声明式编排”的核心理念,仅适用于可容忍数据丢失的测试环境。
- NFS共享存储:部署简单,但MySQL对随机I/O极其敏感。NFSv3/v4的锁机制和缓存策略会导致严重的写放大,某次压测中,相同规格下NFS的TPS仅为云盘的60%,且在并发INSERT时频繁出现
Lock wait timeout exceeded错误。 - 云厂商块存储(如AWS EBS、阿里云云盘、腾讯云CBS):完美平衡性能与可靠性。以阿里云ESSD PL1云盘为例,单盘最大32,000 IOPS,延迟稳定在1ms以内,且支持在线扩容、快照备份、跨可用区复制。最关键的是,它与K8s的StorageClass深度集成,PVC可声明式申请,PV由云平台自动创建绑定,完全符合GitOps工作流。
最终方案采用云盘+StorageClass动态供给:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: mysql-sc provisioner: disk.csi.aliyuncs.com # 阿里云CSI驱动 parameters: type: cloud_essd # ESSD云盘 performanceLevel: PL1 # 性能等级 reclaimPolicy: Retain # 数据保留策略,避免误删 volumeBindingMode: ImmediatereclaimPolicy: Retain是关键配置——当PVC被删除时,PV不会自动销毁,而是进入Released状态,管理员可手动检查数据后再决定是否回收。这为误操作提供了最后一道保险。
3. 核心细节解析与实操要点:从ConfigMap到InitContainer的每一行代码深意
3.1 ConfigMap:不只是配置文件,而是主从角色的“宪法性文件”
ConfigMap看似只是键值对集合,但在本方案中,它承担着定义主从行为边界的重任。我们将其拆分为两部分:全局配置(global.cnf)和角色专用配置(master.cnf / slave.cnf),并通过subPath挂载实现精细化控制。
global.cnf包含所有Pod共用的基础参数:
[mysqld] # 必须关闭bind-address=127.0.0.1,否则Pod间无法通信 bind-address = 0.0.0.0 # 关闭DNS反向解析,避免因Pod DNS解析慢导致连接超时 skip-name-resolve # 设置默认字符集,避免中文乱码 character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci # 启用slow query log便于性能分析 slow_query_log = 1 long_query_time = 2其中bind-address = 0.0.0.0是血泪教训。初期配置为127.0.0.1,导致从库无法连接主库IP,报错Can't connect to MySQL server on 'mysql-0.mysql-headless' (111)。根本原因是MySQL默认只监听本地回环,必须显式放开。
master.cnf专为主库设计:
[mysqld] # 开启binlog,指定格式为ROW(推荐,避免语句级复制的不确定性) log-bin = mysql-bin binlog-format = ROW # 设置server-id,必须全局唯一,StatefulSet中用$(HOSTNAME)动态注入 server-id = $(HOSTNAME##*-) # binlog过期时间,防止磁盘爆满 expire-logs-days = 7 max-binlog-size = 100Mserver-id = $(HOSTNAME##*-)这一行是Shell参数展开技巧。StatefulSet创建的Pod名为mysql-0、mysql-1,$(HOSTNAME##*-)会截取-后的数字部分,使mysql-0的server-id为0,mysql-1为1。这比硬编码更安全,避免手动修改时出错。
slave.cnf为从库定制:
[mysqld] # 从库必须设置read_only=1,防止应用误写入 read_only = 1 # 跳过某些错误(如主库已删除的表),避免复制中断 slave-skip-errors = 1032,1062 # relay log自动清理,避免占满磁盘 relay-log-purge = 1read_only = 1是强安全策略。即使应用连接到从库并执行UPDATE,也会报错ERROR 1290 (HY000): The MySQL server is running with the --read-only option so it cannot execute this statement,从源头杜绝脏写。
实操心得:ConfigMap挂载时务必使用
subPath,而非整个目录。否则MySQL启动时会因/etc/mysql/conf.d/下存在空文件而报错unknown variable。正确写法:volumeMounts: - name: config mountPath: /etc/mysql/conf.d/global.cnf subPath: global.cnf
3.2 InitContainer:主从初始化的“宪法实施者”,不是简单的脚本执行器
InitContainer是本方案成败的关键。它在主容器(MySQL)启动前运行,完成所有前置依赖检查和数据库初始化。我们设计了两个InitContainer:init-mysql(主从差异化初始化)和clone-mysql(从库数据克隆)。
init-mysql的核心逻辑是判断当前Pod角色并执行对应SQL:
#!/bin/bash set -ex # 从HOSTNAME提取序号,mysql-0为主,mysql-1为从 POD_NAME=$(hostname) POD_INDEX=${POD_NAME##*-} if [ "$POD_INDEX" == "0" ]; then echo "Initializing Master..." # 主库:创建复制用户,并授权REPLICATION SLAVE mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e " CREATE USER 'repl'@'%' IDENTIFIED BY 'replpass'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES; " else echo "Initializing Slave..." # 从库:等待主库就绪,再执行CHANGE MASTER TO until mysqladmin ping -h mysql-0.mysql-headless -u root -p"$MYSQL_ROOT_PASSWORD" --silent; do echo "Waiting for Master mysql-0 to be ready..." sleep 5 done # 获取主库当前binlog位置 MASTER_STATUS=$(mysql -h mysql-0.mysql-headless -u root -p"$MYSQL_ROOT_PASSWORD" -e "SHOW MASTER STATUS\G" | grep -E "(File|Position)" | awk '{print $2}') MASTER_FILE=$(echo "$MASTER_STATUS" | head -1) MASTER_POS=$(echo "$MASTER_STATUS" | tail -1) # 配置从库指向主库 mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e " CHANGE MASTER TO MASTER_HOST='mysql-0.mysql-headless', MASTER_USER='repl', MASTER_PASSWORD='replpass', MASTER_LOG_FILE='$MASTER_FILE', MASTER_LOG_POS=$MASTER_POS; " fi这段脚本有三个精妙设计:
- 幂等性保障:
set -ex确保任一命令失败即退出,避免半初始化状态;所有SQL操作都基于当前状态判断,重复执行不会报错。 - 主库就绪等待:
until mysqladmin ping ...循环检测主库连通性,而非简单sleep固定时间。在某次网络抖动测试中,主库启动耗时12秒,固定sleep 10秒会导致从库初始化失败。 - binlog位点动态获取:
SHOW MASTER STATUS返回的是主库当前最新位点,确保从库从“此刻”开始复制,避免遗漏任何事务。
clone-mysql仅在从库首次启动时运行,用于全量数据同步:
#!/bin/bash set -ex # 检查/data/mysql是否为空,为空则从主库dump if [ ! -f "/data/mysql/ibdata1" ]; then echo "Cloning data from Master..." # 使用mysqldump导出主库全部数据库(除系统库) mysqldump -h mysql-0.mysql-headless -u root -p"$MYSQL_ROOT_PASSWORD" \ --all-databases --single-transaction --routines --triggers \ | mysql -u root -p"$MYSQL_ROOT_PASSWORD" fi这里的关键是--single-transaction参数,它在InnoDB引擎下创建一致性快照,确保导出过程中主库可正常写入,且从库获得的数据是某个时间点的完整视图。若省略此参数,在大型库导出时可能遇到Lock wait timeout或数据不一致。
3.3 Headless Service与DNS解析:让“mysql-0”成为真正的网络身份
Headless Service是StatefulSet的“身份证发放机构”。它的YAML看似简单,却暗藏玄机:
apiVersion: v1 kind: Service metadata: name: mysql-headless labels: app: mysql spec: clusterIP: None # 必须为None,否则DNS不解析Pod IP selector: app: mysql ports: - port: 3306 name: mysqlclusterIP: None是核心。当设为None时,K8s DNS会为每个匹配selector的Pod生成独立的A记录:
mysql-0.mysql-headless.default.svc.cluster.local→ 解析为mysql-0 Pod的IPmysql-1.mysql-headless.default.svc.cluster.local→ 解析为mysql-1 Pod的IP
验证方法:进入Pod执行nslookup mysql-0.mysql-headless,应返回具体IP而非ClusterIP。若返回10.96.0.1(默认kube-dns ClusterIP),说明Service配置错误。
常见误区:有人试图用普通Service(有ClusterIP)加
publishNotReadyAddresses: true来模拟Headless效果。这是错误的——ClusterIP本质是iptables规则,无法被MySQL客户端识别为有效主机名,连接时会报Unknown MySQL server host。
4. 实操过程与核心环节实现:从零部署到故障演练的完整流水线
4.1 环境准备与资源清单:一份清单,杜绝“少装一个包”的尴尬
在开始部署前,必须确认以下K8s集群能力已就绪。我们以某高校实验室的K8s 1.24集群为例(非生产环境,但具备完整特性):
- CSI驱动已安装:
kubectl get csidriver应显示云厂商CSI驱动(如disk.csi.aliyuncs.com)。若未安装,需先部署对应CSI插件,否则PVC将一直处于Pending状态。 - StorageClass已创建:
kubectl get sc应看到mysql-sc。注意provisioner字段必须与CSI驱动名称完全一致,大小写敏感。 - RBAC权限已授予:StatefulSet需
get、list、watchendpoints权限,以便initContainer能查询Headless Service的Endpoint。最小化RBAC如下:apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: mysql-role rules: - apiGroups: [""] resources: ["endpoints"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: mysql-rolebinding subjects: - kind: ServiceAccount name: default roleRef: kind: Role name: mysql-role apiGroup: rbac.authorization.k8s.io - MySQL镜像已预加载:为避免拉取超时,建议提前在所有Node执行
docker pull mysql:5.7.42。若使用私有镜像仓库,需配置ImagePullSecret。
实操心得:每次部署前,先运行
kubectl auth can-i --list检查当前账号权限。曾有一次因忘记绑定RoleBinding,initContainer卡在nslookup mysql-0.mysql-headless无限重试,日志里只有;; connection timed out; no servers could be reached,排查3小时才发现是RBAC问题。
4.2 StatefulSet核心YAML详解:237行代码背后的17个关键决策点
以下是StatefulSet的精简版核心配置(已过滤注释和非关键字段),我们逐段解析其设计意图:
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: "mysql-headless" # 必须与Headless Service名称一致 replicas: 2 # 固定2副本,主从各一 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: serviceAccountName: default # 绑定RBAC initContainers: - name: init-mysql image: mysql:5.7.42 command: ["/bin/bash", "/scripts/init-mysql.sh"] env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: password volumeMounts: - name: config mountPath: /scripts - name: mysql-persistent-storage mountPath: /var/lib/mysql - name: clone-mysql image: mysql:5.7.42 command: ["/bin/bash", "/scripts/clone-mysql.sh"] envFrom: - secretRef: name: mysql-secret volumeMounts: - name: config mountPath: /scripts - name: mysql-persistent-storage mountPath: /var/lib/mysql containers: - name: mysql image: mysql:5.7.42 envFrom: - secretRef: name: mysql-secret ports: - containerPort: 3306 name: mysql volumeMounts: - name: config mountPath: /etc/mysql/conf.d/global.cnf subPath: global.cnf - name: config mountPath: /etc/mysql/conf.d/master.cnf subPath: master.cnf - name: config mountPath: /etc/mysql/conf.d/slave.cnf subPath: slave.cnf - name: mysql-persistent-storage mountPath: /var/lib/mysql livenessProbe: exec: command: ["mysqladmin", "ping", "-u", "root", "-p$MYSQL_ROOT_PASSWORD"] initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: exec: command: ["sh", "-c", "mysql -u root -p$MYSQL_ROOT_PASSWORD -e 'SELECT 1'"] initialDelaySeconds: 15 periodSeconds: 5 volumes: - name: config configMap: name: mysql-config volumeClaimTemplates: - metadata: name: mysql-persistent-storage spec: accessModes: ["ReadWriteOnce"] storageClassName: "mysql-sc" resources: requests: storage: 20Gi关键决策点解析:
serviceName: "mysql-headless":StatefulSet与Headless Service的绑定纽带,缺失则Pod无法获得稳定DNS名。replicas: 2:明确限定主从数量。若设为3,则mysql-2会尝试连接mysql-0,但我们的initContainer未定义mysql-2的逻辑,将导致启动失败。initContainers顺序:init-mysql必须在clone-mysql之前。因为clone-mysql依赖init-mysql创建的复制用户repl。envFrom与secretRef:密码等敏感信息必须通过Secret注入,禁止明文写在YAML中。mysql-secret需提前创建:kubectl create secret generic mysql-secret --from-literal=password=yourpassword。subPath挂载:如前所述,避免MySQL加载空配置文件报错。livenessProbe与readinessProbe分离:livenessProbe只检查进程存活(mysqladmin ping),readinessProbe检查SQL可执行性(SELECT 1)。这样设计可避免主库因慢查询导致readiness失败,进而被Service剔除流量,但liveness仍健康,Pod不会重启。volumeClaimTemplates:accessModes: ["ReadWriteOnce"]表示单节点读写,符合MySQL数据目录需求;storageClassName: "mysql-sc"关联前述StorageClass;storage: 20Gi是根据业务预估的最小容量,实际应结合日均增长量计算(如日增1GB,则需预留30天空间)。
参数计算示例:某电商类模拟项目X日均订单10万,平均每单产生5KB binlog,日增binlog约500MB。加上数据文件、relay log,日均总增长约2GB。按30天保留周期,最小PVC应为60Gi。我们取整为100Gi,并启用云盘自动扩容(ESSD支持在线扩容至32TiB)。
4.3 部署执行与状态验证:四步确认法,确保每一步都稳如磐石
部署不是kubectl apply -f一条命令结束,而是分阶段验证的过程。我们采用“四步确认法”:
第一步:资源创建与Pod调度
kubectl apply -f mysql-configmap.yaml kubectl apply -f mysql-service.yaml kubectl apply -f mysql-statefulset.yaml # 观察Pod状态 kubectl get pods -w # 正常应看到: # mysql-0 1/1 Running 0 2m # mysql-1 1/1 Running 0 1m45s若长时间处于Pending,用kubectl describe pod mysql-0检查Events,常见原因是PVC未绑定(StorageClass不存在)或节点资源不足。
第二步:InitContainer日志审计
# 查看主库初始化日志 kubectl logs mysql-0 -c init-mysql # 应输出:Initializing Master... 并无报错 # 查看从库初始化日志 kubectl logs mysql-1 -c init-mysql # 应输出:Waiting for Master... Initializing Slave... 并无报错 # 特别关注是否有"ERROR 1045 (28000)"(密码错误)或"ERROR 2003 (HY000)"(网络不通)第三步:主从状态深度验证
进入主库Pod,检查binlog是否开启及当前位点:
kubectl exec -it mysql-0 -- mysql -uroot -p$MYSQL_ROOT_PASSWORD mysql> SHOW MASTER STATUS\G # 输出应类似: # File: mysql-bin.000001 # Position: 154 # Binlog_Do_DB: # Binlog_Ignore_DB:进入从库Pod,检查复制状态:
kubectl exec -it mysql-1 -- mysql -uroot -p$MYSQL_ROOT_PASSWORD mysql> SHOW SLAVE STATUS\G # 关键字段必须为: # Slave_IO_Running: Yes # Slave_SQL_Running: Yes # Seconds_Behind_Master: 0 # 如果Seconds_Behind_Master > 0,说明从库有延迟,需检查网络或主库负载第四步:业务连通性测试
在集群内起一个临时busybox Pod,测试主从读写分离:
kubectl run test-client --image=busybox:1.35 --rm -it --restart=Never -- sh # 在busybox中执行: / # nslookup mysql-0.mysql-headless / # mysql -h mysql-0.mysql-headless -uroot -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE testdb;" / # mysql -h mysql-1.mysql-headless -uroot -p$MYSQL_ROOT_PASSWORD -e "SHOW DATABASES LIKE 'testdb';" # 若返回testdb,证明主从复制生效实操心得:
kubectl exec进入Pod时,若提示command terminated with exit code 1,大概率是密码错误。此时不要反复尝试,应先kubectl get secret mysql-secret -o yaml解码密码,确认无误后再执行。曾有同事因复制粘贴时多了一个空格,浪费2小时排查网络问题。
4.4 故障模拟与恢复演练:主动“搞破坏”,才是对系统最大的尊重
生产环境没有“永远正常”,只有“快速恢复”。我们定期进行三类故障演练:
故障一:主库Pod意外终止
kubectl delete pod mysql-0 # StatefulSet会自动重建mysql-0,但新Pod的server-id仍为0(因HOSTNAME不变) # 检查从库SHOW SLAVE STATUS,Seconds_Behind_Master应短暂上升后归零 # 验证:在新主库执行INSERT,从库SELECT应实时可见故障二:从库网络隔离
# 在mysql-1节点上,用iptables模拟网络中断 kubectl exec mysql-1 -- iptables -A OUTPUT -d $(kubectl get pod mysql-0 -o jsonpath='{.status.podIP}') -j DROP # 观察从库SHOW SLAVE STATUS,Slave_IO_Running变为No # 恢复网络:kubectl exec mysql-1 -- iptables -D OUTPUT 1 # 从库会自动重连并追赶binlog,Seconds_Behind_Master逐步下降故障三:PVC磁盘满导致写入失败
# 手动填满mysql-0的PVC(仅测试环境!) kubectl exec mysql-0 -- dd if=/dev/zero of=/var/lib/mysql/fillfile bs=1M count=10000 # 主库日志会出现"Disk full"错误,binlog停止写入 # 清理文件后,主库自动恢复,从库通过重连获取新binlog每次演练后,必须记录RTO(恢复时间目标)和RPO(恢复点目标)。在某次压测中,我们测得RTO<30秒,RPO=0(因binlog实时同步),完全满足模拟项目X的SLA要求。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪经验”
5.1 从库复制中断的十大高频原因与速查表
| 现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Slave_IO_Running: No | 主库网络不通 | kubectl exec mysql-1 -- ping mysql-0.mysql-headless | 检查Headless Service、NetworkPolicy、节点防火墙 |
Slave_IO_Running: No | 复制用户密码错误 | kubectl logs mysql-1 -c init-mysql | grep "Access denied" | 更新mysql-secret并滚动重启StatefulSet |
Slave_SQL_Running: No | 主库已删除的表,从库执行DROP失败 | SHOW SLAVE STATUS\G | grep Last_SQL_Error | 执行SET GLOBAL sql_slave_skip_counter=1; START SLAVE;跳过错误(谨慎!) |
Seconds_Behind_Master > 0 | 从库IOPS不足 | kubectl exec mysql-1 -- iostat -x 1 3 | 升级云盘性能等级(如PL1→PL2) |
Seconds_Behind_Master > 0 | 主库大事务阻塞 | kubectl exec mysql-0 -- mysql -e "SHOW PROCESSLIST\G" | grep "Query" | 优化应用,拆分大事务 |
Relay log read failure | relay log文件损坏 | SHOW SLAVE STATUS\G | grep Relay_Log_File | STOP SLAVE; RESET SLAVE; START SLAVE;重建relay log |
Duplicate entry | 主从数据不一致 | SELECT * FROM table WHERE id=xxx对比主从数据 | 手动修复数据,或重新全量同步 |
Master_Host is not set | initContainer未执行CHANGE MASTER TO | kubectl logs mysql-1 -c init-mysql | 检查init-mysql脚本逻辑,确认MASTER_STATUS获取成功 |
Can't find record in 'table' | 从库缺少主库的外键约束 | SHOW CREATE TABLE table\G对比主从 | 在从库执行SET FOREIGN_KEY_CHECKS=0;后重建表 |
Lost connection to MySQL server | MySQL连接超时 | kubectl exec mysql-1 -- mysql -h mysql-0.mysql-headless -e "SHOW VARIABLES LIKE 'wait_timeout';" | 在master.cnf中增加wait_timeout=28800 |
独家技巧:当
SHOW SLAVE STATUS输出过长难以阅读时,用awk快速提取关键字段:kubectl exec mysql-1 -- mysql -e "SHOW SLAVE STATUS\G" \| awk '/Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master|Last_IO_Error|Last_SQL_Error/ {print}'
5.2 StatefulSet滚动更新的“静默陷阱”:为什么不能直接改镜像?
StatefulSet支持kubectl set image进行滚动更新,但直接更新MySQL镜像存在巨大风险:
- 主库升级时,从库可能因协议不兼容拒绝连接。例如MySQL 5.7主库与8.0从库,
CHANGE MASTER TO会报错The slave I/O thread stops because master and slave have equal MySQL server ids。 - 滚动更新顺序是mysql-1→mysql-0,即先更新从库,再更新主库。但主库未升级前,从库8.0无法解析5.7的binlog格式。
安全升级流程:
- 先停写入流量(切走应用连接)
- 手动
kubectl scale statefulset mysql --replicas=1,只保留主库 - 更新StatefulSet镜像,
kubectl rollout restart statefulset mysql,等待mysql-0升级完成 - 恢复从库:
kubectl scale statefulset mysql --replicas=2,观察mysql-1初始化日志 - 验证主从状态,再恢复流量
血泪教训:某次为赶工期,直接
kubectl set image,导致从库启动后卡在Waiting for Master,因新镜像8.0的mysqladmin命令路径变更,initContainer脚本里的mysqladmin ping命令失效。最终手动进入Pod修改脚本才恢复。
5.3 PVC数据保留与迁移:如何安全地把数据从旧集群迁移到新集群?
reclaimPolicy: Retain虽能防误删,但也带来数据迁移难题。当需要将mysql-0的数据迁移到新集群时,标准流程如下:
- 备份源集群数据:
kubectl exec mysql-0 -- mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --all-databases > backup.sql kubectl cp backup.sql ./backup.sql # 下载到本地 - 在新集群创建同名PVC:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-data-mysql-0 spec: accessModes: ["ReadWriteOnce"]