0. 一句话看清整个架构

           kubectl(在 master 上执行)
                    │
     无头服务 mysql(clusterIP: None,DNS 直连 Pod)
       mysql-0.mysql.mysql.svc.cluster.local  →  主库 master
       mysql-1.mysql.mysql.svc.cluster.local  →  从库 slave
​
  StatefulSet mysql(副本=2)
       mysql-0 ──挂载──>  PVC(data-mysql-0) ──绑定──>  PV(mysql-pv-0) ──NFS──>  /data/nfs/mysql-0
       mysql-1 ──挂载──>  PVC(data-mysql-1) ──绑定──>  PV(mysql-pv-1) ──NFS──>  /data/nfs/mysql-1

核心一句话有状态应用靠 StatefulSet 拿「稳定名字 + 稳定存储」,主从之间靠无头服务的 DNS 互相点名。


1. 为什么要"这么多组件"?(先建立心智模型)

回答"为什么要这么搭",就是回答简历里的知识点:

需求用到的 K8s 概念解决什么
主从要有固定身份(谁是主、谁是从)StatefulSet(稳定 Pod 名 mysql-0/1)普通 Deployment 的 Pod 名是随机哈希,无法区分角色
从库要"精确"连主库那一个 Pod无头服务 Headless(clusterIP: None)普通 Service 会轮询负载均衡,破坏主从关系
数据不能因 Pod 重启/删除而丢PV/PVC + NFS数据落到共享盘,Pod 没了数据还在
一 Pod 一磁盘、自动建 PVCvolumeClaimTemplates每起一个 Pod 自动生成一个 PVC
主/从用不同 my.cnfConfigMap + initContainer启动前按序号(0/1)把对应配置放进去

记忆口诀:Deployment 管无状态,StatefulSet 管「有名、有盘、有序」。


2. 完整搭建流程(含全部 yaml + 命令)

Step 1. NFS 共享存储(地基)

# 三台都装客户端
dnf install -y nfs-utils
​
# master(NFS 服务器)导出两个目录,分别给主从
mkdir -p /data/nfs/mysql-0 /data/nfs/mysql-1
chmod -R 777 /data/nfs              # ⚠️ 必须含 x!见坑 2
cat >> /etc/exports <<'EOF'
/data/nfs/mysql-0 *(rw,no_root_squash,no_all_squash,sync)
/data/nfs/mysql-1 *(rw,no_root_squash,no_all_squash,sync)
EOF
systemctl enable --now rpcbind nfs-server
exportfs -ra
showmount -e <master_ip>     # 验证两个导出

Step 2. 命名空间 + ConfigMap(主从配置按角色区分)

apiVersion: v1
kind: ConfigMap
metadata: { name: mysql-config, namespace: mysql }
data:
  primary.cnf: |                # 主库:开 binlog
    [mysqld]
    server-id=1
    log-bin=mysql-bin
    binlog-format=ROW
    gtid_mode=ON                # ⚠️ MySQL 5.7 必须手动开!
    enforce-gtid-consistency=ON # ⚠️ 见坑 3
  replica.cnf: |                # 从库:只读
    [mysqld]
    server-id=2
    read-only=ON
    gtid_mode=ON
    enforce-gtid-consistency=ON
kubectl create namespace mysql
kubectl apply -f mysql-config.yml

Step 3. 两个 PV(静态供给,NFS)

apiVersion: v1
kind: PersistentVolume
metadata: { name: mysql-pv-0 }
spec:
  capacity: { storage: 2Gi }
  accessModes: [ReadWriteOnce]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""          # ⚠️ 空字符串="只要静态 PV",避免被动态供给抢
  nfs:
    server: <master_ip>
    path: /data/nfs/mysql-0
---
# mysql-pv-1 同结构,path 换成 /data/nfs/mysql-1
kubectl apply -f mysql-pv.yml
kubectl get pv      # 两个都应为 Available

Step 4. 无头服务(Headless)

apiVersion: v1
kind: Service
metadata: { name: mysql, namespace: mysql }
spec:
  clusterIP: None              # ← 无头服务:不分配 VIP
  selector: { app: mysql }
  ports:
    - port: 3306
      name: mysql

Step 5. StatefulSet(核心,含 initContainer 按角色选配置)

apiVersion: apps/v1
kind: StatefulSet
metadata: { name: mysql, namespace: mysql }
spec:
  serviceName: mysql           # 必须 = 无头服务名
  replicas: 2
  selector: { matchLabels: { app: mysql } }
  template:
    metadata: { labels: { app: mysql } }
    spec:
      initContainers:
        - name: init-mysql
          image: hb.reg.com/k8s/mysql:5.7
          command:              # ⚠️ 多行脚本用这种分行写法,见坑 1
            - bash
            - -c
            - |
              if [[ "$HOSTNAME" =~ -([0-9]+)$ ]]; then ORD=${BASH_REMATCH[1]}; fi
              if [ "$ORD" = "0" ]; then
                cp /mnt/config-map/primary.cnf /etc/mysql/conf.d/mysql.cnf
              else
                cp /mnt/config-map/replica.cnf /etc/mysql/conf.d/mysql.cnf
              fi
          volumeMounts:
            - { name: config-map, mountPath: /mnt/config-map }
            - { name: conf, mountPath: /etc/mysql/conf.d }
      containers:
        - name: mysql
          image: hb.reg.com/k8s/mysql:5.7
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "1"
          ports: [ { containerPort: 3306 } ]
          volumeMounts:
            - { name: data, mountPath: /var/lib/mysql }    # 数据落盘
            - { name: conf, mountPath: /etc/mysql/conf.d }
      volumes:
        - { name: conf, emptyDir: {} }
        - { name: config-map, configMap: { name: mysql-config } }
  volumeClaimTemplates:          # 每 Pod 自动建一个 PVC
    - metadata: { name: data }
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: ""
        resources: { requests: { storage: 2Gi } }
kubectl apply -f mysql-statefulset.yml
kubectl -n mysql get pod -w    # 观察有序创建:先 mysql-0 Running,才起 mysql-1
kubectl -n mysql get sts,pvc   # 期望 data-mysql-0/1 Bound

Step 6. 建立主从复制(GTID 自动追平)

# ① 主库建复制账号(⚠️ 已存在会报 ERROR 1396,属正常)
kubectl -n mysql exec -it mysql-0 -- mysql -uroot -p1 -e "
  CREATE USER IF NOT EXISTS 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'Repl@123';
  GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
  FLUSH PRIVILEGES;"

# ② 从库指向主库并开启复制
kubectl -n mysql exec -it mysql-1 -- mysql -uroot -p1 -e "
  CHANGE MASTER TO
    MASTER_HOST='mysql-0.mysql',   # 无头服务 DNS 直指主库
    MASTER_USER='repl',
    MASTER_PASSWORD='Repl@123',
    MASTER_AUTO_POSITION=1;        # GTID 自动定位
  START SLAVE;"

# ③ 验证复制状态(两个都是 Yes 即成功)
kubectl -n mysql exec -it mysql-1 -- mysql -uroot -p1 -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running|Slave_SQL_Running"

MySQL 8.0.23+ 官方写法:CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION=1CHANGE MASTER TO 也可用,仅 deprecated)。

Step 7. 终极验证:主写从读

# 主库建库建表插数据
kubectl -n mysql exec -it mysql-0 -- mysql -uroot -p1 -e "CREATE DATABASE testdb; USE testdb; CREATE TABLE t(id INT); INSERT INTO t VALUES(1);"
# 从库查(能看到 1 = 复制打通)
kubectl -n mysql exec -it mysql-1 -- mysql -uroot -p1 -e "SELECT * FROM testdb.t;"

3. 🕳️ 四个坑

坑 1:YAML command: ["bash","-c",|] 直接解析失败

症状kubectl applyerror converting YAML to JSON: line 20: found character '|' that cannot start any token

原因:多行字面量块标记 | 不能放在 flow 序列 [...] 里面,YAML 解析器不认识。

修复:把 command 写成分行列表

command:
  - bash
  - -c
  - |
    这里写多行脚本

同批的小坑

  • Kind: StatefulSet → 必须小写 kind

  • { name:MYSQL_ROOT_PASSWORD, vlue: "1" }name: 后要空格、vluevalue 拼错。

校验命令(改完必跑,秒查)

kubectl apply -f xxx.yml --dry-run=client -o yaml

坑 2(最阴险):NFS 目录缺 x,容器内 Permission denied

症状:StatefulSet 建出来了、PVC 也 Bound,但 Pod 主容器 CrashLoopBackOff,日志:

mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (Errcode: 13 - Permission denied)
[ERROR] --initialize specified but the data directory exists and is not writable. Aborting.

现象很诡异:root 能手动写、UID 999 写不了,明明目录是 666。

根因:在 Linux/NFS 里,要在目录里创建文件,除了该目录要有 write,整条路径还都要有 execute(x)——因为要"进入/遍历"这个目录。问题出在上层目录 /data/nfs 是 666(没有 x),容器里的受限用户(UID 999,MySQL 的 mysql 用户)进不去;root 能无视权限进去,所以只有 root 正常。

复现定位技巧(用对照法,判断是哪一级缺 x):

setpriv --reuid 999 --regid 0 --clear-groups sh -c "touch /path/.t && echo OK || echo FAIL"

修复(给路径各级补上 x,最省事直接 777):

chmod 777 /data/nfs /data/nfs/mysql-0 /data/nfs/mysql-1

坑 3:MySQL 5.7 默认 GTID 关闭,MASTER_AUTO_POSITION 用不了

症状

ERROR 1777 (HY000): CHANGE MASTER TO MASTER_AUTO_POSITION = 1 cannot be
executed because @@GLOBAL.GTID_MODE = OFF.

原因:课件假设 MySQL 8.0(默认开 GTID),但练习环境是 5.7,默认 gtid_mode=OFF。GTID 没开,MASTER_AUTO_POSITION=1 自然不可用。

修复:ConfigMap 主/从 my.cnf 都加,然后重启 Pod

[mysqld]
gtid_mode=ON
enforce-gtid-consistency=ON

⚠️ 关键gtid_mode 必须在 MySQL 数据目录初始化之前就得开着。如果 PVC 里已经是 GTID OFF 初始化的数据,改配置后 MySQL 不允许直接切 ON(有事务数据时更严格)。练习环境删 PVC + 清 NFS 数据目录重建最干净。

坑 4:删 PVC 后 PV 停在 Released,新 PVC 一直 Pending

症状:重建 StatefulSet 后 get pvc 显示 data-mysql-0 Pending,PV 是 Released

原因:PV 用了 persistentVolumeReclaimPolicy: Retain。删掉 PVC 后,PV 变为 Released(数据保留),但不会自动回到 Available 去接新的 PVC(Retain 的设计就是"留给管理员手动处理")。

修复(手动摘除 claimRef,让 PV 重新 Available):

kubectl patch pv mysql-pv-0 --type json -p='[{"op":"remove","path":"/spec/claimRef"}]'
kubectl patch pv mysql-pv-1 --type json -p='[{"op":"remove","path":"/spec/claimRef"}]'

小知识:重建后 PVC↔PV 可能"交叉绑定"(0 绑到 pv1、1 绑到 pv0)——不影响功能,两个都是独立 2Gi NFS 目录。


4. 面试讲知识点(从里到外)

4.1 StatefulSet —— 有状态应用的"三个稳定"

稳定项含义对 MySQL 的意义
稳定网络标识Pod 名固定 mysql-0/mysql-1(不是随机哈希)主从靠固定名字区分角色
稳定存储每个 Pod 绑定自己的 PVC,删 Pod 数据不丢数据库数据必须持久
有序部署/伸缩按 0→1→2 顺序启动、反向缩容先起主库,从库才能连

4.2 无头服务 —— 让 Pod 能"点名"

  • 普通 Service:分配 ClusterIP(VIP),做负载均衡,客户端不知道连的是哪个 Pod;

  • 无头服务:clusterIP: None不做负载均衡,DNS 直接解析到每个 Pod 的真实 IP;

  • DNS 规则:<Pod名>.<服务名>.<命名空间>.svc.cluster.localmysql-0.mysql.mysql... 直指主库;

  • 为什么主从必须用它:从库要"精确"连主库那一个 Pod,普通 Service 会轮询分给两个,主从就乱了。

4.3 PV / PVC —— 存储的"供给侧"和"需求侧"

  • PV:集群级存储资源,管理员准备,声明"有哪些存储";

  • PVC:用户申请单,声明"我要多大、什么访问模式";

  • 绑定:PVC 找容量、访问模式、storageClass 都匹配的 PV 绑定;

  • volumeClaimTemplates:StatefulSet 专属,为每个 Pod 自动生成一个 PVCdata-mysql-0/1),实现"一 Pod 一磁盘"。

PV 关键字段速记(别人问你 PV 怎么配):

  • capacity: 容量多大;

  • accessModes: 谁能用(RWO 单节点/ROX 多读/RWX 多写/RWOP 单 Pod)——数据库用 RWO;

  • persistentVolumeReclaimPolicy: PVC 删了怎么办(Retain 保留/Delete 删除/Recycle 废弃);

  • storageClassName: 和 PVC 对暗号(""=只要静态 PV);

  • nfs.server/path: 数据实际存在哪。

PV 四种状态Available(可用)→Bound(已绑定)→Released(绑定解除数据在)→Failed(出错)。

4.4 NFS —— 学习环境首选共享存储

  • NFS Server(装在 master)导出目录 → 各节点装 nfs-utils 作客户端 → K8s 的 PV 里写 nfs.server+path。

  • 选它:简单、多节点能挂同一份数据,够支撑主从演示。生产常用 Ceph、云盘等。

4.5 MySQL 主从复制原理(面试重点)

主库写 binlog → 从库 IO 线程拉 binlog 存 relay log → 从库 SQL 线程重放 relay log

三个关键配置

  • server-id:每实例唯一(主 1 从 2),复制靠它识别身份;

  • log-bin:主库必须开(记录变更);

  • read-only:从库只读,防误写。

两种定位方式

  • 老方式:binlog 文件名 + 位置(要手动记,易错);

  • GTID(推荐):全局事务 ID,MASTER_AUTO_POSITION=1 自动追平,无需记位置。

验证SHOW SLAVE STATUS\GSlave_IO_RunningSlave_SQL_Running 都 = Yes


5. 两种常见报错速查(实验时你会遇到)

报错含义处理
ERROR 1396 Operation CREATE USER failed for 'repl'@'%'账号已存在(之前建过)不是错,GRANT + FLUSH 成功即可用;可改 CREATE USER IF NOT EXISTS
ERROR 3081 This operation cannot be performed with running replication threads复制已经在跑了,你重复 CHANGE MASTERSTOP SLAVE; 再 CHANGE MASTER;或直接查 SHOW SLAVE STATUS 确认已正常

6. 复盘清单(做完自检)

  • kubectl get nodes 三节点 Ready
  • kubectl -n mysql get pods → mysql-0、mysql-1 都 1/1 Running
  • kubectl -n mysql get pvc → data-mysql-0/1 都 Bound
  • 从库 SHOW SLAVE STATUS\G → Slave_IO/SQL_Running 都 Yes,Seconds_Behind=0
  • 主写 INSERT → 从库 SELECT 能查到 ✅

更多推荐