Skip to content

Kubernetes 部署 MySQL 主从集群(ZooKeeper 选举)

本文档介绍在 Kubernetes 环境中使用 ZooKeeper 进行主从选举的 MySQL 高可用部署方案。

架构介绍

架构概述

本方案采用 MySQL 主从复制架构,配合 ZooKeeper 实现自动主节点选举。当主节点故障时,ZooKeeper 可以协调从节点进行选举,确保服务的高可用性。

┌─────────────────────────────────────────────────────────────┐
│                  Kubernetes Cluster                        │
│                                                        │
│   ┌────────────────────┐   ┌────────────────────┐    │
│   │  MySQL Master    │◄──►│  MySQL Replica  │    │
│   │   (Pod/StatefulSet) │    │  (Pod/StatefulSet)  │    │
│   └────────┬───────┘   └────────┬───────┘    │
│            │                  │                  │
│   ┌────────▼────────┐   ┌───▼────────────┐  │
│   │  PVC (数据)  │   │  PVC (数据)    │  │
│   └─────────────┘   └──────────────┘  │
└────────────────────────────────────┘

          ┌──────────┴──────────┐
          │                     │
┌─────────▼─────────┐   ┌────▼─────────┐
│  ZooKeeper-1    │◄─►│ ZooKeeper-3 │
│      Pod       │   │    Pod    │
└────────────────┘   └───────────┘
         │   ◄─────────►   │
        Master Election Leader

技术选型

  • MySQL 模式:主从复制 + 半同步复制
  • 选举协调:ZooKeeper(EPHEMERAL_SEQUENTIAL 节点)
  • 健康检查:MySQL 健康检查 + ZooKeeper 心跳
  • 故障转移:ZooKeeper 触发选举,应用层感知新主节点

为什么使用 ZooKeeper

ZooKeeper 在分布式系统中提供可靠的协调服务:

  • 原子广播:ZAB 协议保证一致性
  • 临时节点:自动检测节点失效
  • 顺序节点:支持领导者选举
  • 观察者模式:实时感知配置变化

部署资源清单

1. 命名空间

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: mysql-mgr
  labels:
    name: mysql-mgr
    environment: production

2. ZooKeeper 集群配置

2.1 ConfigMap

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: zk-config
  namespace: mysql-mgr
data:
  zoo.cfg: |
    tickTime=2000
    initLimit=10
    syncLimit=5
    dataDir=/data
    clientPort=2181
    admin.enableServer=false
    4lw.commands.whitelist=*
    skipACL=yes
    metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
    metricsProvider.httpPort=7000
    metricsProvider.exportJvmInfo=true
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: zk-log4j
  namespace: mysql-mgr
data:
  log4j.properties: |
    zookeeper.root.logger=CONSOLE
    zookeeper.console.threshold=INFO

2.2 ZooKeeper Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: zookeeper-headless
  namespace: mysql-mgr
spec:
  clusterIP: None
  ports:
  - name: client
    port: 2181
  - name: quorum
    port: 2888
  - name: leader-election
    port: 3888
  selector:
    app: zookeeper
---
apiVersion: v1
kind: Service
metadata:
  name: zookeeper
  namespace: mysql-mgr
spec:
  type: ClusterIP
  ports:
  - name: client
    port: 2181
    targetPort: 2181
  selector:
    app: zookeeper

2.3 ZooKeeper StatefulSet

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: zookeeper
  namespace: mysql-mgr
spec:
  serviceName: zookeeper-headless
  replicas: 3
  selector:
    matchLabels:
      app: zookeeper
  template:
    metadata:
      labels:
        app: zookeeper
    spec:
      terminationGracePeriodSeconds: 30
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: zookeeper
            topologyKey: kubernetes.io/hostname
      containers:
      - name: zookeeper
        image: zookeeper:3.9.2
        imagePullPolicy: IfNotPresent
        ports:
        - name: client
          containerPort: 2181
        - name: quorum
          containerPort: 2888
        - name: leader-election
          containerPort: 3888
        env:
        - name: ZOO_MY_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: ZOO_SERVERS
          value: "0=zookeeper-0.zookeeper-headless.mysql-mgr.svc.cluster.local:2888:3888,1=zookeeper-1.zookeeper-headless.mysql-mgr.svc.cluster.local:2888:3888,2=zookeeper-2.zookeeper-headless.mysql-mgr.svc.cluster.local:2888:3888"
        - name: JVMFLAGS
          value: "-Xmx512m -Xms512m"
        resources:
          requests:
            cpu: 100m
            memory: 512Mi
          limits:
            cpu: 500m
            memory: 1Gi
        volumeMounts:
        - name: data
          mountPath: /data
        - name: config
          mountPath: /conf
        livenessProbe:
          exec:
            command:
            - zookeeper-shell
            - localhost:2181
            - ruok
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - zookeeper-shell
            - localhost:2181
            - ruok
          initialDelaySeconds: 10
          periodSeconds: 5
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: zookeeper-data
      - name: config
        configMap:
          name: zk-config
  volumeClaimTemplates:
  - metadata:
      name: zookeeper-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast
      resources:
        requests:
          storage: 5Gi

3. MySQL 配置

3.1 ConfigMap

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
  namespace: mysql-mgr
data:
  my.cnf: |
    [mysqld]
    # 基础配置
    default-storage-engine=InnoDB
    character-set-server=utf8mb4
    collation-server=utf8mb4_unicode_ci
    max_connections=2000
    
    # 网络配置
    bind-address=0.0.0.0
    port=3306
    
    # GTID 配置
    gtid_mode=ON
    enforce_gtid_consistency=ON
    
    # 二进制日志
    log-bin=mysql-bin
    binlog_format=ROW
    log_replica_updates=ON
    expire_logs_days=7
    max_binlog_size=256M
    
    # 中继日志
    relay_log=relay-bin
    relay_log_recovery=ON
    
    # 复制配置
    rpl_semi_sync_source_enabled=ON
    rpl_semi_sync_replica_enabled=ON
    replica_skip_errors=1062
    
    # 只读配置
    read_only=1
    super_read_only=1
    
    # 优化配置
    innodb_buffer_pool_size=1G
    innodb_log_file_size=256M
    innodb_flush_log_at_trx_commit=1
    table_open_cache=2000
    
    # 复制同步超时
    rpl_semi_sync_source_timeout=1000
    rpl_semi_sync_replica_timeout=1000
    
    # 日志
    log_error=/var/log/mysql/error.log
    slow_query_log=1
    slow_query_log_file=/var/log/mysql/slow.log
    long_query_time=2
    
    [client]
    default-character-set=utf8mb4
    
    [mysql]
    default-character-set=utf8mb4

3.2 MySQL 主节点 Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-master-headless
  namespace: mysql-mgr
spec:
  clusterIP: None
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql
    role: master
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-master
  namespace: mysql-mgr
spec:
  type: ClusterIP
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
  selector:
    app: mysql
    role: master

3.3 MySQL 从节点 Service

yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-replica-headless
  namespace: mysql-mgr
spec:
  clusterIP: None
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql
    role: replica
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-replica
  namespace: mysql-mgr
spec:
  type: ClusterIP
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
  selector:
    app: mysql
    role: replica

4. MySQL StatefulSet

4.1 主节点 StatefulSet

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-master
  namespace: mysql-mgr
spec:
  serviceName: mysql-master-headless
  replicas: 1
  selector:
    matchLabels:
      app: mysql
      role: master
  template:
    metadata:
      labels:
        app: mysql
        role: master
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: mysql
        image: mysql:8.0.36
        imagePullPolicy: IfNotPresent
        ports:
        - name: mysql
          containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: root-password
        - name: MYSQL_REPLICATION_USER
          value: "repl"
        - name: MYSQL_REPLICATION_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: repl-password
        - name: ZOOKEEPER_SERVERS
          value: "zookeeper.mysql-mgr.svc.cluster.local:2181"
        - name: SERVER_ID
          value: "1"
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 2000m
            memory: 2Gi
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-config
          mountPath: /etc/mysql/conf.d
        - name: mysql-logs
          mountPath: /var/log/mysql
        livenessProbe:
          exec:
            command:
            - mysqladmin
            - ping
            - -h
            - localhost
            - -uroot
            - -pRoot@2024
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - mysql
            - -h
            - localhost
            - -uroot
            - -pRoot@2024
            - -e
            - SELECT 1
          initialDelaySeconds: 30
          periodSeconds: 5
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql-master-data
      - name: mysql-config
        configMap:
          name: mysql-config
      - name: mysql-logs
        emptyDir: {}
  volumeClaimTemplates:
  - metadata:
      name: mysql-master-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast
      resources:
        requests:
          storage: 50Gi

4.2 从节点 StatefulSet

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-replica
  namespace: mysql-mgr
spec:
  serviceName: mysql-replica-headless
  replicas: 2
  selector:
    matchLabels:
      app: mysql
      role: replica
  template:
    metadata:
      labels:
        app: mysql
        role: replica
    spec:
      terminationGracePeriodSeconds: 30
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: mysql
                role: replica
            topologyKey: kubernetes.io/hostname
      containers:
      - name: mysql
        image: mysql:8.0.36
        imagePullPolicy: IfNotPresent
        ports:
        - name: mysql
          containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: root-password
        - name: MYSQL_REPLICATION_USER
          value: "repl"
        - name: MYSQL_REPLICATION_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: repl-password
        - name: ZOOKEEPER_SERVERS
          value: "zookeeper.mysql-mgr.svc.cluster.local:2181"
        - name: MASTER_HOST
          value: "mysql-master.mysql-mgr.svc.cluster.local"
        - name: SERVER_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 2000m
            memory: 2Gi
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-config
          mountPath: /etc/mysql/conf.d
        - name: mysql-logs
          mountPath: /var/log/mysql
        livenessProbe:
          exec:
            command:
            - mysqladmin
            - ping
            - -h
            - localhost
            - -uroot
            - -pRoot@2024
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - mysql
            - -h
            - localhost
            - -uroot
            - -pRoot@2024
            - -e
            - SELECT 1
          initialDelaySeconds: 30
          periodSeconds: 5
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql-replica-data
      - name: mysql-config
        configMap:
          name: mysql-config
      - name: mysql-logs
        emptyDir: {}
  volumeClaimTemplates:
  - metadata:
      name: mysql-replica-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast
      resources:
        requests:
          storage: 50Gi

5. 选举控制器(Election Controller)

选举控制器负责协调主从选举,监听 ZooKeeper:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-election-controller
  namespace: mysql-mgr
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql-election
  template:
    metadata:
      labels:
        app: mysql-election
    spec:
      containers:
      - name: controller
        image: your-registry/mysql-election-controller:v1.0.0
        imagePullPolicy: IfNotPresent
        env:
        - name: ZOOKEEPER_HOSTS
          value: "zookeeper.mysql-mgr.svc.cluster.local:2181"
        - name: MYSQL_MASTER_HOST
          value: "mysql-master.mysql-mgr.svc.cluster.local"
        - name: MYSQL_REPLICA_HOSTS
          value: "mysql-replica-0.mysql-replica-headless.mysql-mgr.svc.cluster.local,mysql-replica-1.mysql-replica-headless.mysql-mgr.svc.cluster.local"
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: root-password
        - name: ELECTION_PATH
          value: /mysql-election
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi

选举控制器代码示例(Python):

python
import kazoo
from kazoo.client import KazooClient
from kazoo.exceptions import NodeExistsException
import time

class MySQLElectionController:
    def __init__(self, zk_hosts, election_path, mysql_master_host):
        self.zk = KazooClient(hosts=zk_hosts)
        self.election_path = election_path
        self.mysql_master_host = mysql_master_host
        self.is_master = False
        
    def start(self):
        self.zk.start()
        self.zk.ensure_path(self.election_path)
        
        # 创建临时顺序节点参与选举
        try:
            self.my_node = self.zk.create(
                f"{self.election_path}/candidate_",
                ephemeral=True,
                sequence=True
            )
        except NodeExistsException:
            pass
            
        # 监听选举结果
        @self.zk.ChildrenWatch(f"{self.election_path}/")
        def watch_election(children):
            if not children:
                return
            children.sort()
            leader_node = f"{self.election_path}/{children[0]}"
            
            if leader_node == self.my_node:
                if not self.is_master:
                    self.become_master()
            else:
                if self.is_master:
                    self.become_replica()
                    
    def become_master(self):
        print("Elected as master!")
        self.is_master = True
        # 执行变为 master 的操作
        # 需要将 slave 重新配置
        
    def become_replica(self):
        print("Became replica")
        self.is_master = False
        
    def stop(self):
        self.zk.stop()
        
# 使用示例
if __name__ == "__main__":
    controller = MySQLElectionController(
        zk_hosts="zookeeper.mysql-mgr.svc.cluster.local:2181",
        election_path="/mysql-election",
        mysql_master_host="mysql-master.mysql-mgr.svc.cluster.local"
    )
    try:
        controller.start()
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        controller.stop()

6. Secret

yaml
apiVersion: v1
kind: Secret
metadata:
  name: mysql-secrets
  namespace: mysql-mgr
type: Opaque
stringData:
  root-password: Root@2024
  repl-password: Repl@2024

7. StorageClass

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd
allowVolumeExpansion: true
reclaimPolicy: Retain

8. Pod 中断预算

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: mysql-pdb
  namespace: mysql-mgr
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: mysql
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zookeeper-pdb
  namespace: mysql-mgr
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

部署步骤

1. 基础配置

bash
# 创建命名空间
kubectl apply -f 00-namespace.yaml

# 创建 StorageClass
kubectl apply -f 01-storageclass.yaml

2. ZooKeeper 集群

bash
# 创建配置
kubectl apply -f 02-zk-configmap.yaml

# 创建 Service
kubectl apply -f 03-zk-services.yaml

# 创建 StatefulSet
kubectl apply -f 04-zookeeper.yaml

3. MySQL 集群

bash
# 创建配置
kubectl apply -f 05-mysql-configmap.yaml

# 创建 Secret
kubectl apply -f 06-mysql-secrets.yaml

# 创建 MySQL Services
kubectl apply -f 07-mysql-services.yaml

# 创建 MySQL StatefulSets
kubectl apply -f 08-mysql-master.yaml
kubectl apply -f 09-mysql-replica.yamls

4. 选举控制器

bash
kubectl apply -f 10-election-controller.yaml

5. 验证

bash
# 查看 ZooKeeper 状态
kubectl exec -it zookeeper-0 -n mysql-mgr -- zookeeper-shell localhost:2181 ruok

# 查看 MySQL 主从状态
kubectl exec -it mysql-master-0 -n mysql-mgr -- mysql -uroot -pRoot@2024 -e "SHOW MASTER STATUS\G"
kubectl exec -it mysql-replica-0 -n mysql-mgr -- mysql -uroot -pRoot@2024 -e "SHOW REPLICA STATUS\G"

# 查看选举信息
kubectl logs mysql-election-controller -n mysql-mgr

应用层配置示例

应用层需要配置主从感知的连接:

python
import mysql.connector
from mysql.connector import pooling

class MySQLProxy:
    def __init__(self, master_host, replica_hosts, password):
        self.master_host = master_host
        self.replica_hosts = replica_hosts
        self.password = password
        
        # 创建连接池
        self.master_pool = pooling.MySQLConnectionPool(
            pool_name="master",
            pool_size=5,
            host=master_host,
            user="appuser",
            password=password,
            database="appdb"
        )
        
        self.replica_pool = pooling.MySQLConnectionPool(
            pool_name="replica",
            pool_size=10,
            host=replica_hosts[0],
            user="appuser",
            password=password,
            database="appdb"
        )
        
    def get_write_connection(self):
        """获取写连接(到主节点)"""
        return self.master_pool.get_connection()
        
    def get_read_connection(self):
        """获取读连接(到从节点)"""
        return self.replica_pool.get_connection()
        
    def execute_on_master(self, query, params=None):
        """在主节点上执行写操作"""
        conn = self.get_write_connection()
        cursor = conn.cursor()
        cursor.execute(query, params or ())
        conn.commit()
        return cursor.fetchall()
        
    def execute_on_replica(self, query, params=None):
        """在从节点上执行读操作"""
        conn = self.get_read_connection()
        cursor = conn.cursor()
        cursor.execute(query, params or ())
        return cursor.fetchall()

Spring Boot 配置:

yaml
spring:
  datasource:
    url: jdbc:mysql:loadbalance://mysql-replica.mysql-mgr.svc.cluster.local:3306/appdb
    username: appuser
    password: App@2024
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

故障转移测试

1. 模拟主节点故障

bash
# 删除主节点 Pod
kubectl delete pod mysql-master-0 -n mysql-mgr

2. 观察选举变化

bash
# 查看选举控制器日志
kubectl logs mysql-election-controller -n mysql-mgr -f

# 查看主节点变化
kubectl get pods -n mysql-mgr -l app=mysql -o wide

3. 验证服务恢复

bash
# 测试写入
kubectl exec -it mysql-replica-0 -n mysql-mgr -- \
  mysql -uroot -pRoot@2024 -e "INSERT INTO test.t VALUES(1, NOW())"

# 验证数据同步
kubectl exec -it mysql-replica-1 -n mysql-mgr -- \
  mysql -uroot -pRoot@2024 -e "SELECT * FROM test.t"

常见问题排查

ZooKeeper 无法选举

检查 ZooKeeper 节点数是否达到多数(>= 3);检查网络连通性。

MySQL 复制中断

检查 SHOW REPLICA STATUS 错误信息;验证 binlog 格式是否一致。

选举控制器异常

检查 ZooKeeper 连接配置;验证选举路径权限。

总结

本文档提供了使用 ZooKeeper 进行主从选举的 MySQL 高可用部署方案。关键点包括:

  1. ZooKeeper 集群:3 节点保证高可用
  2. MySQL 主从:1 主 2 从架构
  3. 选举控制器:监听 ZooKeeper,协调主从切换
  4. 应用层感知:需要配置读写分离

真正的故障自动转移还需要:

  • 配合服务注册发现(如 Consul)
  • 应用层连接池支持故障转移
  • 完整的监控告警系统