原力注入

云原生应用生命周期管理:主从架构 MySQL 案例解析

云原生应用生命周期管理:主从架构 MySQL 案例解析

前文:云原生应用生命周期管理思考之需求分析

Kubernetes 的工作负载(如 Deployment 和 StatefulSet)能够很好地描述典型的无状态微服务,但在处理复杂的分布式应用时(例如具有主从架构的 MySQL),这些原生资源就显得力不从心,难以满足拓扑管理、故障恢复和动态配置等高级需求。

官方 MySQL Operator 请参考:https://github.com/mysql/mysql-operator,本文的目标是从需求出发,一步一步来分析和设计一个 MySQL Application CRD 和 Controller, 供学习与探讨之用。

1. 背景

1.1 软件系统的一般描述

在计算机领域,软件系统是指由多个相互协作的子系统和组件组成的整体,其目的是实现特定的功能或满足特定的业务需求。一个软件系统的设计和实现通常涉及多个层级,从整体架构到具体实现的细节。例如,一个医院管理系统可能包括患者记录管理、预约排班、账单处理等多个子系统,每个子系统又由更小的组件协作完成特定任务。

  • • 系统: 软件系统是一个完整的功能集合,包含所有子系统和组件,它们共同协作以实现预期目标。例如,操作系统是一个典型的软件系统,它涵盖内核、文件系统、驱动程序等多个部分。

  • • 子系统: 系统中的逻辑单元,通常专注于某一领域或功能。例如,电子商务系统中的支付模块、订单管理模块等。子系统通常可以独立运行或被单独测试。

  • • 组件: 子系统中更小的单元,负责具体的实现逻辑或功能。例如,支付模块中的订单校验组件、支付接口组件等。组件通常是可复用的、独立的逻辑单元。

1.2 微服务领域中一般应用的定义

微服务架构是一种现代化的软件架构风格,将系统划分为多个小型、独立运行的服务。每个服务专注于处理特定的业务功能,并通过轻量级通信协议(如 HTTP 或消息队列)与其他服务交互。这种架构强调服务的独立性、可扩展性和高可用性,使得系统更易于开发、部署和维护。

  • • 微服务系统: 整体架构由一组相互协作的微服务应用组成。它们共同实现系统的完整功能,同时可以独立部署和扩展。例如,一个电商平台可能由用户服务、订单服务、支付服务和库存服务组成。

  • • 微服务应用: 微服务系统中的单个服务,通常聚焦于某一具体的业务功能。每个微服务通常具备独立的数据库和业务逻辑,例如订单服务负责管理订单的创建、更新和查询等操作。

  • • 微服务组件: 微服务应用内部的功能模块,例如业务逻辑层、数据访问层、API 层等。组件通常按照职责划分,形成清晰的层次结构,便于维护和扩展。

  • • 微服务实例: 微服务应用的运行实例,可能是容器、虚拟机或物理服务器上的部署。例如,用户服务的多个实例可能运行在不同的容器中,以应对高并发需求。

1.3 无状态微服务组件与有状态分布式组件在 Kubernetes 中的区别

1.3.1 无状态微服务组件

无状态微服务组件是指服务在处理请求时不依赖于本地存储的状态信息,所有状态信息通常存储在外部共享存储中(如数据库或缓存)。这种设计使得服务可以自由扩展和缩减实例数量,而无需考虑状态一致性问题。

在 Kubernetes 中,无状态微服务组件可以通过 Deployment 控制器进行良好的管理和部署。Deployment 提供以下功能:

  1. 1. 自动扩缩容: Deployment 支持水平扩展(Horizontal Scaling),可根据负载动态调整实例数量。

  2. 2. 自愈能力: 如果某个 Pod 异常终止,Deployment 会自动重新创建以保证期望的实例数量。

  3. 3. 服务访问:

  • • 使用传统的服务注册与发现工具(如 Eureka、Consul),实现服务的动态注册与访问。

  • • 使用 Kubernetes 原生的 Service 对 Pod 的统一访问,屏蔽实例动态变化的复杂性。

例如,无状态的用户服务(User Service)可以通过 Deployment 配置多个实例,并通过 Service 提供负载均衡的统一访问入口。

1.3.2 有状态分布式组件

有状态分布式组件与无状态服务的主要区别在于:这些组件依赖于持久化存储和节点间的状态一致性。例如,MySQL 一主多从架构中,每个节点的角色(主节点或从节点)及其数据存储是组件运行的重要部分。

在 Kubernetes 中,StatefulSet 是为有状态组件设计的控制器,但它在一些复杂场景下仍存在不足,例如:

  1. 1. 状态描述: StatefulSet 仅负责为每个 Pod 提供稳定的网络标识和持久化存储,但无法直接表达 MySQL 主从复制的角色状态(主实例与从实例)。

  2. 2. 主备切换: 当主节点出现故障时,StatefulSet 无法自动感知和完成主从角色切换,需要结合额外的管理工具(如 Orchestrator 或自定义控制器)进行角色管理和故障恢复。

  3. 3. 扩展复杂性: 对于分布式数据库等组件,扩展通常涉及数据迁移和一致性校验,StatefulSet 无法直接处理这些需求。

1.3.3 举例:

  • • 无状态微服务组件: 用户服务(User Service)可以通过 Deployment 部署多个无状态实例,每个实例独立处理请求。

  • • 有状态分布式组件: MySQL 一主多从架构可以使用 StatefulSet 创建 Pod,并借助外部管理工具管理主从角色和状态,但这并非完全符合云原生理念。相较之下,通过实现 MySQL Operator,可以更优雅地描述和管理 MySQL 主从架构,实现自动化与高可用性。

2. MySQL Operator 需求分析

2.1 多样化部署支持

提供对多种 MySQL 部署场景的支持,包括:

  • • 单实例部署模式:适用于开发测试场景或轻量级应用。

  • • 一主多从架构:满足生产环境下的高性能读写分离需求,支持扩展性与可靠性。

2.2 建模与抽象描述

基于微服务架构的理念,对 MySQL 的以下内容进行建模与描述:

  • • 应用:代表整体 MySQL 集群实例。

  • • 组件:如主节点、从节点以及管理工具模块。

  • • 实例:具体的运行实例(如 Pod)与其配置,确保逻辑关系清晰且易于管理。

2.3 CRD 定义与集成

  • • 自定义资源定义(CRD):通过 CRD 对 MySQL 集群的逻辑结构进行精细化建模。

  • • Kubernetes 集成:与 Kubernetes 原生工作负载深度结合,实现对 MySQL 集群的声明式管理和全生命周期控制。

  • • 生命周期管理:支持部署、扩容、缩容、备份、恢复等操作的一站式管理。

2.4 主节点地址自动发现

  • • 设计专用的 CRD 和辅助机制,使 MySQL 应用能够通过 Kubernetes Service 自动解析当前主节点(Primary)的访问地址。

  • • 确保主节点地址在主备切换或扩缩容过程中保持稳定,简化应用对数据库的连接管理。

2.5 高可用主备切换机制

  • • 实时监控:通过控制器(Controller)监控主节点运行状态,检测故障或性能瓶颈。

  • • 自动切换:在主节点故障时,快速完成主备切换,并更新相关节点的复制关系。

  • • 一致性保障:在切换过程中确保数据一致性,避免数据丢失或服务中断。

2.6 其他关键需求

  • • 弹性扩展支持:支持动态添加或移除从节点,以满足业务负载变化的需求。

  • • 数据备份与恢复:集成定时备份和按需恢复功能,保障数据的安全性与完整性。

  • • 高性能与低延迟:优化主从复制性能,确保高负载下的稳定运行。

3. MySQL Operator CRD 设计

3.1 基于微服务架构的建模

CRD 应按照微服务架构的思想,对 MySQL 应用、组件和实例进行建模,明确各层级的职责与配置:

  • • MySQL 应用

    • • 代表整个 MySQL 系统的抽象单元,由多个组件组成。

    • • 定义全局配置,例如存储类型(如持久化存储或临时存储)、访问策略(如读写分离策略)、备份设置等。

    • • 应用级别的字段示例:

      spec:
        storageType: "Persistent"
        accessPolicy: "ReadWriteSplit"
        backupPolicy:
          enabled: true
          schedule: "0 2 * * *"
  • • MySQL 组件

    • • 定义应用中的核心功能单元,如主节点(Primary)和从节点(Replica)。

    • • 每个组件可以拥有独立的配置,例如资源限制、启动参数等。

    • • 示例字段:

      components:
        primary:
          resources:
            limits:
              cpu: "2"
              memory: "4Gi"
        replicas:
          replicas: 3
          resources:
            limits:
              cpu: "1"
              memory: "2Gi"
  • • MySQL 实例

    • • 具体的运行单元,代表 Pod,负责执行读写操作。

    • • 实例化后继承组件配置,并支持通过状态字段监控运行情况。

    • • 示例状态字段:

      status:
        instances:
          - name: "mysql-primary-0"
            phase: "Running"
          - name: "mysql-replica-1"
            phase: "Pending"

3.2 主从架构及一主多从数量表示

CRD 应灵活支持不同的部署模式和节点配置:

  • • 部署模式字段

    • • 使用 mode 字段区分不同的部署模式,如单实例模式(single)和主从架构模式(primary-replica)。

    • • 示例:

      spec:
        mode: "primary-replica"
  • • 副本数量配置

    • • 使用 replicas 字段指定主从节点的数量。

    • • 在主从架构中,primary 节点的数量通常固定为 1,而 replica 节点的数量可以动态调整。

    • • 示例:

      replicas:
        primary: 1
        replica: 3

3.3 主节点访问地址

为 MySQL 主节点设计稳定的访问机制:

  • • Service 统一访问入口

    • • 在 CRD 中定义一个字段用于生成 Kubernetes Service,自动指向主节点。

    • • 示例字段:

      status:
        primaryService: "mysql-primary-svc"
  • • 动态更新

    • • 在主备切换时,Operator 自动更新主节点的 Service 指向,确保应用访问时无需感知主节点的变化。

    • • 示例逻辑:

      status:
        primaryService: "mysql-primary-svc"
        currentPrimary: "mysql-primary-0"

3.4 设计目标总结

  • • 提供基于微服务架构的清晰建模,支持灵活的部署配置与高可用性管理。

  • • 实现主从架构及动态扩缩容的灵活配置。

  • • 确保主节点的访问地址稳定,简化应用与数据库的集成与管理。

4. MySQL Application CRD 例子

以下是一个完整的 MySQL Application CRD 示例定义,结合了微服务架构的建模思想,并对其关键部分进行了解释。

4.1 MySQL Application CRD 示例定义

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: mysqlapplications.mysql.operator.io
spec:
  group: mysql.operator.io
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                storageType:
                  type: string
                  enum: ["Persistent", "Temporary"]
                accessPolicy:
                  type: string
                  enum: ["ReadWriteSplit", "ReadOnly"]
                backupPolicy:
                  type: object
                  properties:
                    enabled:
                      type: boolean
                    schedule:
                      type: string
                mode:
                  type: string
                  enum: ["single", "primary-replica"]
                replicas:
                  type: object
                  properties:
                    primary:
                      type: integer
                      minimum: 1
                      maximum: 1
                    replica:
                      type: integer
                      minimum: 0
                components:
                  type: object
                  properties:
                    primary:
                      type: object
                      properties:
                        resources:
                          type: object
                          properties:
                            limits:
                              type: object
                              properties:
                                cpu:
                                  type: string
                                memory:
                                  type: string
                    replicas:
                      type: object
                      properties:
                        replicas:
                          type: integer
                        resources:
                          type: object
                          properties:
                            limits:
                              type: object
                              properties:
                                cpu:
                                  type: string
                                memory:
                                  type: string
            status:
              type: object
              properties:
                primaryService:
                  type: string
                currentPrimary:
                  type: string
                instances:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                      phase:
                        type: string
  scope: Namespaced
  names:
    plural: mysqlapplications
    singular: mysqlapplication
    kind: MySQLApplication
    shortNames:
      - mysqlapp

4.2 CRD 关键部分解释

4.2.1 MySQL 应用层级

  • • storageType: 定义存储类型,支持 Persistent(持久化存储)和 Temporary(临时存储)。

  • • accessPolicy: 定义访问策略,支持 ReadWriteSplit(读写分离)和 ReadOnly(只读)。

  • • backupPolicy: 定义备份策略,包括是否启用备份 (enabled) 和备份计划 (schedule)。

spec:
  storageType: "Persistent"
  accessPolicy: "ReadWriteSplit"
  backupPolicy:
    enabled: true
    schedule: "0 2 * * *"

4.2.2 MySQL 组件层级

  • • primary: 定义主节点的资源配置,如 CPU 和内存限制。

  • • replicas: 定义从节点的数量和资源配置。

components:
  primary:
    resources:
      limits:
        cpu: "2"
        memory: "4Gi"
  replicas:
    replicas: 3
    resources:
      limits:
        cpu: "1"
        memory: "2Gi"

4.2.3 MySQL 实例层级

  • • instances: 表示具体的运行单元(Pod),并监控其运行状态(如 Running 或 Pending)。

status:
  instances:
    - name: "mysql-primary-0"
      phase: "Running"
    - name: "mysql-replica-1"
      phase: "Pending"

4.2.4 主从架构及副本数量

  • • mode: 定义部署模式,支持 single(单实例模式)和 primary-replica(主从架构模式)。

  • • replicas: 定义主节点和从节点的数量,主节点数量固定为 1,从节点数量可动态调整。

spec:
  mode: "primary-replica"
  replicas:
    primary: 1
    replica: 3

4.2.5 主节点访问地址

  • • primaryService: 定义主节点的 Kubernetes Service 名称,作为统一的访问入口。

  • • currentPrimary: 动态更新当前主节点的名称,确保在主备切换时自动更新 Service 指向。

status:
  primaryService: "mysql-primary-svc"
  currentPrimary: "mysql-primary-0"

4.3 MySQL Application CR 示例

以下是一个基于上述 CRD 的 MySQLApplication 自定义资源(CR)示例:

apiVersion: mysql.operator.io/v1
kind: MySQLApplication
metadata:
  name: my-mysql-app
  namespace: default
spec:
  storageType: "Persistent"
  accessPolicy: "ReadWriteSplit"
  backupPolicy:
    enabled: true
    schedule: "0 2 * * *"
  mode: "primary-replica"
  replicas:
    primary: 1
    replica: 3
  components:
    primary:
      resources:
        limits:
          cpu: "2"
          memory: "4Gi"
    replicas:
      replicas: 3
      resources:
        limits:
          cpu: "1"
          memory: "2Gi"
status:
  primaryService: "mysql-primary-svc"
  currentPrimary: "mysql-primary-0"
  instances:
    - name: "mysql-primary-0"
      phase: "Running"
    - name: "mysql-replica-1"
      phase: "Running"
    - name: "mysql-replica-2"
      phase: "Running"
    - name: "mysql-replica-3"
      phase: "Pending"

4.4 CR 与具体 Kubernetes 资源的对应关系

当用户创建上述 MySQLApplication CR 后,MySQL Operator 会根据 CR 的定义自动创建和管理以下 Kubernetes 资源:

4.4.1 StatefulSet

  • • 用于管理 MySQL 主节点和从节点的 Pod。

  • • 示例:

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: mysql-primary
      namespace: default
    spec:
      replicas: 1
      serviceName: mysql-primary-svc
      template:
        spec:
          containers:
            - name: mysql
              image: mysql:5.7
              resources:
                limits:
                  cpu: "2"
                  memory: "4Gi"

4.4.2 Service

  • • 用于暴露 MySQL 主节点的访问入口。

  • • 示例:

    apiVersion: v1
    kind: Service
    metadata:
      name: mysql-primary-svc
      namespace: default
    spec:
      selector:
        app: mysql-primary
      ports:
        - protocol: TCP
          port: 3306
          targetPort: 3306

4.2.3 PersistentVolumeClaim (PVC)

  • • 用于为 MySQL 主节点和从节点申请持久化存储。

  • • 示例:

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: mysql-pvc
      namespace: default
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi

4.2.4 CronJob

  • • 用于定期执行 MySQL 备份任务。

  • • 示例:

    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: mysql-backup
      namespace: default
    spec:
      schedule: "0 2 * * *"
      jobTemplate:
        spec:
          template:
            spec:
              containers:
                - name: backup
                  image: mysql-backup-tool:latest
                  args: ["--backup"]
              restartPolicy: OnFailure

4.2.5 Pod

  • • 具体的 MySQL 实例运行单元。

  • • 示例:

    apiVersion: v1
    kind: Pod
    metadata:
      name: mysql-primary-0
      namespace: default
    spec:
      containers:
        - name: mysql
          image: mysql:5.7
          resources:
            limits:
              cpu: "2"
              memory: "4Gi"

通过这种设计,MySQL Operator 能够以声明式的方式管理 MySQL 应用的生命周期,简化了复杂分布式系统的部署和维护。

5. MySQL 主备切换设计思路

5.1 故障检测

  • • Kubernetes 探针:

    • • 使用 livenessProbe 和 readinessProbe 监控主节点的健康状态是一个很好的实践。

    • • 建议:

      • • livenessProbe 用于检测主节点是否存活,如果失败,Kubernetes 会重启 Pod。

      • • readinessProbe 用于检测主节点是否准备好接受流量,如果失败,Kubernetes 会从 Service 中移除该 Pod。

      • • 可以结合 MySQL 的 SELECT 1 或 SHOW STATUS 命令来实现探针。

    • • 示例:

      livenessProbe:
        exec:
          command:
            - mysql
            - -e
            - "SELECT 1"
        initialDelaySeconds: 30
        periodSeconds: 10
      readinessProbe:
        exec:
          command:
            - mysql
            - -e
            - "SELECT 1"
        initialDelaySeconds: 5
        periodSeconds: 5
  • • 故障检测逻辑:

    • • 如果 livenessProbe 或 readinessProbe 连续失败,Controller 可以标记主节点为故障状态。

    • • 建议:

      • • 除了探针,还可以通过 MySQL 的日志或性能指标(如复制延迟、连接数等)来增强故障检测的准确性。

5.2 备选主节点选择

  • • 选择策略:

    • • 从从节点中选择数据延迟最小、Pod 创建时间最早的节点作为新的主节点是一个合理的策略。

    • • 建议:

      • • 可以通过查询 MySQL 的 SHOW SLAVE STATUS 获取从节点的复制延迟(Seconds_Behind_Master)。

      • • 如果多个从节点的延迟相同,可以优先选择创建时间最早的 Pod。

    • • 示例逻辑:

  1. 1. 遍历所有从节点,获取其复制延迟和 Pod 创建时间。

  2. 2. 选择延迟最小的节点,如果延迟相同,选择创建时间最早的节点。

  • • 优先级策略扩展:

    • • 可以引入更复杂的策略,例如:

      • • 基于节点的资源利用率(如 CPU、内存)选择负载较低的节点。

      • • 基于地理位置选择最近的节点(适用于跨区域部署)。

    5.3 切换步骤

    • • 升级从节点为主节点:

      • • 修改从节点的 MySQL 配置(如 my.cnf 中的 read_only=0)以允许写入。

      • • 建议:

        • • 通过 Kubernetes ConfigMap 动态更新 MySQL 配置文件,然后重启 Pod 或发送 SIGHUP 信号使配置生效。

        • • 确保在切换过程中,新的主节点已经完成数据同步。

    • • 重新绑定 Service:

      • • 更新主节点 Service 的 labelSelector,将流量切换到新的主节点。

      • • 建议:

        • • 使用 Kubernetes 的 Service 资源,通过更新 selector 字段指向新的主节点 Pod。

        • • 示例:

          apiVersion: v1
          kind: Service
          metadata:
            name: mysql-primary-svc
          spec:
            selector:
              app: mysql
              role: primary
            ports:
              - protocol: TCP
                port: 3306
                targetPort: 3306
        • • 在切换时,Controller 需要更新 role: primary 的标签到新的主节点 Pod。

    • • 通知从节点:

      • • 让其他从节点重新同步到新的主节点。

      • • 建议:

        • • 通过 MySQL 的 CHANGE MASTER TO 命令,将从节点的复制源指向新的主节点。

        • • 示例:

          CHANGE MASTER TO MASTER_HOST='new-primary-svc', MASTER_USER='repl', MASTER_PASSWORD='password', MASTER_AUTO_POSITION=1;
          START SLAVE;
        • • 确保在切换过程中,从节点不会丢失数据。

    5.4 其他注意事项

    • • 数据一致性:

      • • 在主备切换过程中,确保数据一致性是关键。

      • • 可以通过 MySQL 的 GTID(全局事务标识)或半同步复制来减少数据丢失的风险。

    • • 切换日志和监控:

      • • 记录主备切换的详细日志,包括切换时间、新主节点的选择依据等。

      • • 监控切换后的集群状态,确保新主节点和从节点正常运行。

    • • 回滚机制:

      • • 如果切换失败或新主节点出现问题,设计回滚机制以恢复原主节点。

    • • 测试和验证:

      • • 在开发完成后,通过模拟主节点故障的场景,测试 Controller 的切换逻辑是否可靠。

    6. MySQL Application 主备切换 Controller 示例实现

    以下是基于上述设计思路和 MySQL Application CRD 定义的 MySQL Application Controller 实现,包括主备切换功能的代码实现和详细说明。

    6.1 Controller 架构设计

    Controller 的核心逻辑包括:

    • • 监听 MySQLApplication CR 的变化:通过 Kubernetes Informer 监听 CR 的创建、更新和删除事件。

    • • 故障检测:通过 Kubernetes 探针和 MySQL 状态监控主节点的健康状态。

    • • 主备切换:当主节点故障时,选择新的主节点并完成切换。

    • • 状态同步:更新 CR 的状态字段,记录当前主节点和实例状态。

    6.2 Controller 实现代码

    以下是 Controller 的核心代码实现,使用 Go 语言和 Kubernetes Client-go 库。

    6.2.1 Controller 初始化

    package main

    import (
        "context"
        "fmt"
        "time"

        corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/labels"
        "k8s.io/client-go/kubernetes"
        "k8s.io/client-go/tools/cache"
        "k8s.io/client-go/util/workqueue"
        "k8s.io/klog/v2"

        mysqlv1 "github.com/example/mysql-operator/pkg/apis/mysql/v1"
        clientset "github.com/example/mysql-operator/pkg/generated/clientset/versioned"
        informers "github.com/example/mysql-operator/pkg/generated/informers/externalversions/mysql/v1"
    )

    type Controller struct {
        kubeClient    kubernetes.Interface
        mysqlClient   clientset.Interface
        queue         workqueue.RateLimitingInterface
        mysqlInformer cache.SharedIndexInformer
    }

    func NewController(
        kubeClient kubernetes.Interface,
        mysqlClient clientset.Interface,
        mysqlInformer informers.MySQLApplicationInformer,
    )
     *Controller {
        controller := &Controller{
            kubeClient:    kubeClient,
            mysqlClient:   mysqlClient,
            queue:         workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
            mysqlInformer: mysqlInformer.Informer(),
        }

        mysqlInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
            AddFunc:    controller.enqueueMySQLApp,
            UpdateFunc: controller.updateMySQLApp,
            DeleteFunc: controller.deleteMySQLApp,
        })

        return controller
    }

    6.2.2 事件处理

    func (c *Controller) enqueueMySQLApp(obj interface{}) {
        key, err := cache.MetaNamespaceKeyFunc(obj)
        if err != nil {
            klog.Errorf("Failed to get key for object: %v", err)
            return
        }
        c.queue.Add(key)
    }

    func (c *Controller) updateMySQLApp(oldObj, newObj interface{}) {
        oldApp := oldObj.(*mysqlv1.MySQLApplication)
        newApp := newObj.(*mysqlv1.MySQLApplication)

        if oldApp.ResourceVersion == newApp.ResourceVersion {
            return
        }
        c.enqueueMySQLApp(newObj)
    }

    func (c *Controller) deleteMySQLApp(obj interface{}) {
        key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
        if err != nil {
            klog.Errorf("Failed to get key for object: %v", err)
            return
        }
        c.queue.Add(key)
    }

    6.2.3 主备切换逻辑

    func (c *Controller) syncHandler(key string) error {
        namespace, name, err := cache.SplitMetaNamespaceKey(key)
        if err != nil {
            return err
        }

        // 获取 MySQLApplication CR
        app, err := c.mysqlClient.MysqlV1().MySQLApplications(namespace).Get(context.TODO(), name, metav1.GetOptions{})
        if err != nil {
            return err
        }

        // 检查主节点状态
        primaryPod, err := c.getPrimaryPod(app)
        if err != nil {
            return err
        }

        if !c.isPodHealthy(primaryPod) {
            klog.Infof("Primary pod %s is unhealthy, triggering failover", primaryPod.Name)

            // 选择新的主节点
            newPrimaryPod, err := c.selectNewPrimary(app)
            if err != nil {
                return err
            }

            // 升级从节点为主节点
            if err := c.promoteToPrimary(newPrimaryPod); err != nil {
                return err
            }

            // 更新 Service 指向新的主节点
            if err := c.updatePrimaryService(app, newPrimaryPod); err != nil {
                return err
            }

            // 更新 CR 状态
            app.Status.CurrentPrimary = newPrimaryPod.Name
            app.Status.PrimaryService = fmt.Sprintf("%s-primary-svc", app.Name)
            if _, err := c.mysqlClient.MysqlV1().MySQLApplications(namespace).UpdateStatus(context.TODO(), app, metav1.UpdateOptions{}); err != nil {
                return err
            }

            klog.Infof("Failover completed: new primary is %s", newPrimaryPod.Name)
        }

        return nil
    }

    6.2.4 辅助函数

    ####$ 6.2.4.1 获取主节点 Pod

    func (c *Controller) getPrimaryPod(app *mysqlv1.MySQLApplication) (*corev1.Pod, error) {
        pods, err := c.kubeClient.CoreV1().Pods(app.Namespace).List(context.TODO(), metav1.ListOptions{
            LabelSelector: labels.Set{"app": "mysql", "role": "primary"}.String(),
        })
        if err != nil {
            return nil, err
        }

        if len(pods.Items) == 0 {
            return nil, fmt.Errorf("no primary pod found")
        }

        return &pods.Items[0], nil
    }

    6.2.4.2 检查 Pod 健康状态
    func (c *Controller) isPodHealthy(pod *corev1.Pod) bool {
        for _, condition := range pod.Status.Conditions {
            if condition.Type == corev1.PodReady && condition.Status != corev1.ConditionTrue {
                return false
            }
        }
        return true
    }
    6.2.4.3 选择新的主节点
    func (c *Controller) selectNewPrimary(app *mysqlv1.MySQLApplication) (*corev1.Pod, error) {
        pods, err := c.kubeClient.CoreV1().Pods(app.Namespace).List(context.TODO(), metav1.ListOptions{
            LabelSelector: labels.Set{"app": "mysql", "role": "replica"}.String(),
        })
        if err != nil {
            return nil, err
        }

        if len(pods.Items) == 0 {
            return nil, fmt.Errorf("no replica pods available for failover")
        }

        // 选择延迟最小的 Pod
        var newPrimaryPod *corev1.Pod
        minDelay := int64(1<<63 - 1)
        for _, pod := range pods.Items {
            delay := c.getReplicationDelay(&pod)
            if delay < minDelay {
                minDelay = delay
                newPrimaryPod = &pod
            }
        }

        return newPrimaryPod, nil
    }

    6.2.4.4 升级从节点为主节点
    func (c *Controller) promoteToPrimary(pod *corev1.Pod) error {
        // 修改 MySQL 配置,设置 read_only=0
        cmd := []string{"mysql", "-e", "SET GLOBAL read_only=0;"}
        _, err := c.kubeClient.CoreV1().Pods(pod.Namespace).Exec(context.TODO(), pod.Name, &corev1.PodExecOptions{
            Command: cmd,
            Stdout:  true,
            Stderr:  true,
        })
        return err
    }
    6.2.4.5 更新 Service 指向新的主节点
    func (c *Controller) updatePrimaryService(app *mysqlv1.MySQLApplication, newPrimaryPod *corev1.Pod) error {
        service, err := c.kubeClient.CoreV1().Services(app.Namespace).Get(context.TODO(), app.Status.PrimaryService, metav1.GetOptions{})
        if err != nil {
            return err
        }

        service.Spec.Selector["role"] = "primary"
        _, err = c.kubeClient.CoreV1().Services(app.Namespace).Update(context.TODO(), service, metav1.UpdateOptions{})
        return err
    }

    6.3 运行 Controller

    func main() {
        klog.InitFlags(nil)
        defer klog.Flush()

        config, err := rest.InClusterConfig()
        if err != nil {
            klog.Fatalf("Failed to get in-cluster config: %v", err)
        }

        kubeClient, err := kubernetes.NewForConfig(config)
        if err != nil {
            klog.Fatalf("Failed to create Kubernetes client: %v", err)
        }

        mysqlClient, err := clientset.NewForConfig(config)
        if err != nil {
            klog.Fatalf("Failed to create MySQL client: %v", err)
        }

        informerFactory := informers.NewSharedInformerFactory(mysqlClient, time.Minute*10)
        mysqlInformer := informerFactory.Mysql().V1().MySQLApplications()

        controller := NewController(kubeClient, mysqlClient, mysqlInformer)

        stopCh := make(chan struct{})
        defer close(stopCh)

        informerFactory.Start(stopCh)

        if err := controller.Run(2, stopCh); err != nil {
            klog.Fatalf("Error running controller: %v", err)
        }
    }

    6.4 总结

    以上代码实现了一个完整的 MySQL Application Controller,具备以下功能:

    1. 1. 监听 MySQLApplication CR 的变化。

    2. 2. 检测主节点故障。

    3. 3. 选择新的主节点并完成切换。

    4. 4. 更新 Service 和 CR 状态。

    通过该 Controller,可以实现 MySQL 主备切换的自动化。