在 GKE 上部署有状态 MySQL 集群


本文档适用于想在 Google Kubernetes Engine 上部署高可用性 MySQL 拓扑的数据库管理员、云架构师和专业运营人员。

在本教程中,您将了解如何部署 MySQL InnoDB 集群MySQL InnoDB ClusterSet、如何在 GKE 集群上部署 MySQL 路由器中间件,以及如何执行升级。

目标

在本教程中,您将学习如何完成以下操作:

  • 创建和部署有状态 Kubernetes Service。
  • 部署 MySQL InnoDB 集群以实现高可用性。
  • 部署路由器中间件以进行数据库操作路由。
  • 部署 MySQL InnoDB ClusterSet 以实现灾难容忍。
  • 模拟 MySQL 集群故障切换。
  • 执行 MySQL 版本升级。

以下各部分介绍您将在本教程中构建的解决方案的架构。

MySQL InnoDB 集群

在您的区域 GKE 集群中,使用 StatefulSet 部署具有必要命名和配置的 MySQL 数据库实例,以创建 MySQL InnoDB 集群。如需提供容错和高可用性,您需要部署三个数据库实例 Pod。这样可以确保在任何给定时间不同可用区中的大多数 Pod 可用,从而能够使用共识协议成功选举主实例,并且使 MySQL InnoDB 集群能够容忍单可用区故障。

显示应用、MySQL 路由器和 MySQL 集群之间的关系的架构图
图 1:单个 MySQL InnoDB 集群的示例架构

部署后,您可以将一个 Pod 指定为主实例来响应读取和写入操作。另外两个 Pod 是辅助只读副本。如果主实例遇到基础架构故障,您可以将这两个副本 Pod 中的一个提升为主实例。

在另一个命名空间中,您需要部署三个 MySQL 路由器 Pod,以提供连接路由来提高弹性。您的应用不会连接到数据库服务,而是连接到 MySQL 路由器 Pod。每个路由器 Pod 了解每个 MySQL InnoDB 集群 Pod 的状态和用途,并将应用操作路由到相应的健康 Pod。路由状态会缓存在路由器 Pod 中,并根据存储在 MySQL InnoDB 集群的每个节点上的集群元数据更新。如果某个实例发生故障,路由器会将连接路由调整到活动实例。

MySQL InnoDB ClusterSet

您可以从初始 MySQL InnoDB 集群创建 MySQL InnoDB ClusterSet。这样,如果主集群不再可用,您可以提高灾难容忍度。

展示 MySQL 主集群和副本集群如何通过异步复制保持同步的示意图。
图 2示例多区域 ClusterSet 架构,其中包含一个主集群和一个副本集群

如果 MySQL InnoDB 集群主实例不再可用,您可以将 ClusterSet 中的副本集群提升为主实例。使用 MySQL 路由器中间件时,您的应用不需要跟踪主数据库实例的健康状况。路由器会调整路由,以在选举完成后将连接发送到新的主实例。但是,您需要负责确保连接到 MySQL 路由器中间件的应用遵循弹性最佳实践,以便在集群故障切换期间发生错误时可以重试连接。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

设置项目

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, click Create project to begin creating a new Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the GKE API.

    Enable the API

  5. In the Google Cloud console, on the project selector page, click Create project to begin creating a new Google Cloud project.

    Go to project selector

  6. Make sure that billing is enabled for your Google Cloud project.

  7. Enable the GKE API.

    Enable the API

设置角色

  1. Grant roles to your user account. Run the following command once for each of the following IAM roles: role/storage.objectViewer, role/logging.logWriter, role/artifactregistry.Admin, roles/container.clusterAdmin, role/container.serviceAgent, roles/serviceusage.serviceUsageAdmin, roles/iam.serviceAccountAdmin

    gcloud projects add-iam-policy-binding PROJECT_ID --member="user:USER_IDENTIFIER" --role=ROLE
    • Replace PROJECT_ID with your project ID.
    • Replace USER_IDENTIFIER with the identifier for your user account. For example, user:myemail@example.com.

    • Replace ROLE with each individual role.

设置您的环境

在本教程中,您将使用 Cloud Shell 来管理 Google Cloud 上托管的资源。Cloud Shell 预安装有 Docker、kubectl 和 gcloud CLI。

为了使用 Cloud Shell 设置您的环境,请执行以下操作:

  1. 设置环境变量。

    export PROJECT_ID=PROJECT_ID
    export CLUSTER_NAME=gkemulti-west
    export REGION=COMPUTE_REGION
    

    替换以下值:

    • PROJECT_ID:您的 Google Cloud 项目 ID
    • COMPUTE_REGION:您的 Compute Engine 区域。在本教程中,区域为 us-west1。通常,建议使用您附近的区域。
  2. 设置默认环境变量。

     gcloud config set project PROJECT_ID
     gcloud config set compute/region COMPUTE_REGION
    
  3. 克隆代码库。

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples
    
  4. 切换到工作目录。

    cd kubernetes-engine-samples/databases/gke-stateful-mysql/kubernetes
    

创建 GKE 集群

在本部分中,您将创建一个区域级 GKE 集群。与可用区级集群不同,区域级集群的控制平面会被复制到多个可用区,因此单个可用区中的服务中断不会导致该控制平面不可用。

如需创建 GKE 集群,请按照以下步骤操作:

Autopilot

  1. 在 Cloud Shell 中,在 us-west1 区域中创建 GKE Autopilot 集群。

    gcloud container clusters create-auto $CLUSTER_NAME \
        --region=$REGION
    
  2. 获取 GKE 集群凭据。

    gcloud container clusters get-credentials $CLUSTER_NAME \
      --region=$REGION
    
  3. 跨三个区域部署 Service。

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: prepare-three-zone-ha
      labels:
        app: prepare-three-zone-ha
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: prepare-three-zone-ha
      template:
        metadata:
          labels:
            app: prepare-three-zone-ha
        spec:
          affinity:
            # Tell Kubernetes to avoid scheduling a replica in a zone where there
            # is already a replica with the label "app: prepare-three-zone-ha"
            podAntiAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
              - labelSelector:
                  matchExpressions:
                  - key: app
                    operator: In
                    values:
                    - prepare-three-zone-ha
                topologyKey: "topology.kubernetes.io/zone"
          containers:
          - name: prepare-three-zone-ha
            image: busybox:latest
            command:
                - "/bin/sh"
                - "-c"
                - "while true; do sleep 3600; done"
            resources:
              limits:
                cpu: "500m"
                ephemeral-storage: "10Mi"
                memory: "0.5Gi"
              requests:
                cpu: "500m"
                ephemeral-storage: "10Mi"
                memory: "0.5Gi"
    kubectl apply -f prepare-for-ha.yaml
    

    默认情况下,Autopilot 会在两个可用区中预配资源。prepare-for-ha.yaml 中定义的 Deployment 会设置 replicas:3、具有 requiredDuringSchedulingIgnoredDuringExecutionpodAntiAffinity,以及 topologyKey: "topology.kubernetes.io/zone",以确保 Autopilot 在集群的三个可用区中预配节点。

  4. 检查 Deployment 的状态。

    kubectl get deployment prepare-three-zone-ha --watch
    

    当您看到三个 Pod 处于就绪状态时,请使用 CTRL+C 取消此命令。输出类似于以下内容:

    NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
    prepare-three-zone-ha   0/3     3            0           9s
    prepare-three-zone-ha   1/3     3            1           116s
    prepare-three-zone-ha   2/3     3            2           119s
    prepare-three-zone-ha   3/3     3            3           2m16s
    
  5. 运行此脚本以验证 Pod 已在三个可用区中部署。

    bash ../scripts/inspect_pod_node.sh default
    

    输出的每一行对应一个 Pod,第二列表示 Cloud 可用区。输出类似于以下内容:

    gk3-gkemulti-west1-default-pool-eb354e2d-z6mv us-west1-b prepare-three-zone-ha-7885d77d9c-8f7qb
    gk3-gkemulti-west1-nap-25b73chq-739a9d40-4csr us-west1-c prepare-three-zone-ha-7885d77d9c-98fpn
    gk3-gkemulti-west1-default-pool-160c3578-bmm2 us-west1-a prepare-three-zone-ha-7885d77d9c-phmhj
    

Standard

  1. 在 Cloud Shell 中,在 us-west1 区域中创建 GKE Standard 集群。

    gcloud container clusters create $CLUSTER_NAME \
      --region=$REGION \
      --machine-type="e2-standard-2" \
      --disk-type="pd-standard" \
      --num-nodes="5"
    
  2. 获取 GKE 集群凭据。

    gcloud container clusters get-credentials $CLUSTER_NAME \
      --region=$REGION
    

部署 MySQL StatefulSet

在本部分中,您将部署一个 MySQL StatefulSet。每个 StatefulSet 由三个 MySQL 副本组成。

如需部署 MySQL StatefulSet,请按照以下步骤操作:

  1. 为 StatefulSet 创建命名空间。

    kubectl create namespace mysql1
    
  2. 创建 MySQL 密钥。

    apiVersion: v1
    kind: Secret
    metadata:
      name: mysql-secret
    type: Opaque
    data:
      password: UGFzc3dvcmQkMTIzNDU2 # Password$123456
      admin-password: UGFzc3dvcmQkMTIzNDU2 # Password$123456
    kubectl apply -n mysql1 -f secret.yaml
    

    密码随每个 Pod 一起部署,并供本教程中的 MySQL InnoDB 集群和 ClusterSet 部署的管理脚本和命令使用。

  3. 创建 StorageClass。

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: fast-storageclass
    provisioner: pd.csi.storage.gke.io
    volumeBindingMode: WaitForFirstConsumer
    reclaimPolicy: Retain
    allowVolumeExpansion: true
    parameters:
      type: pd-balanced
    kubectl apply -n mysql1 -f storageclass.yaml
    

    此存储类别使用 pd-balanced 永久性磁盘类型,该类型可平衡性能和费用。volumeBindingMode 字段设置为 WaitForFirstConsumer,表示 GKE 将推迟预配 PersistentVolume,直到 Pod 创建完成为止。此设置可确保在安排 Pod 的可用区中预配磁盘。

  4. 部署 MySQL 实例 Pod 的 StatefulSet。

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: dbc1
      labels:
        app: mysql
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: mysql
      serviceName: mysql
      template:
        metadata:
          labels:
            app: mysql
        spec:
          topologySpreadConstraints:
          - maxSkew: 1
            topologyKey: "topology.kubernetes.io/zone"
            whenUnsatisfiable: DoNotSchedule
            labelSelector:
              matchLabels:
                app: mysql
          affinity:
            podAntiAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
              - labelSelector:
                  matchExpressions:
                  - key: app
                    operator: In
                    values:
                    - mysql
                topologyKey: "kubernetes.io/hostname"
          containers:
          - name: mysql
            image: mysql/mysql-server:8.0.28
            command:
            - /bin/bash
            args:
            - -c
            - >-
              /entrypoint.sh
              --server-id=$((20 +  $(echo $HOSTNAME | grep -o '[^-]*$') + 1))
              --report-host=${HOSTNAME}.mysql.mysql1.svc.cluster.local
              --binlog-checksum=NONE
              --enforce-gtid-consistency=ON
              --gtid-mode=ON
              --default-authentication-plugin=mysql_native_password
            env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: password
            - name: MYSQL_ADMIN_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: admin-password
            - name: MYSQL_ROOT_HOST
              value: '%'
            ports:
            - name: mysql
              containerPort: 3306
            - name: mysqlx
              containerPort: 33060
            - name: xcom
              containerPort: 33061
            resources:
              limits:
                cpu: "500m"
                ephemeral-storage: "1Gi"
                memory: "1Gi"
              requests:
                cpu: "500m"
                ephemeral-storage: "1Gi"
                memory: "1Gi"
            volumeMounts:
            - name: mysql
              mountPath: /var/lib/mysql
              subPath: mysql
            readinessProbe:
              exec:
                command:
                - bash
                - "-c"
                - |
                  mysql -h127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD -e'SELECT 1'
              initialDelaySeconds: 30
              periodSeconds: 2
              timeoutSeconds: 1
            livenessProbe:
              exec:
                command:
                - bash
                - "-c"
                - |
                  mysqladmin -uroot -p$MYSQL_ROOT_PASSWORD ping
              initialDelaySeconds: 30
              periodSeconds: 10
              timeoutSeconds: 5
      updateStrategy:
        rollingUpdate:
          partition: 0
        type: RollingUpdate
      volumeClaimTemplates:
      - metadata:
          name: mysql
          labels:
            app: mysql
        spec:
          storageClassName: fast-storageclass
          volumeMode: Filesystem
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 10Gi
    kubectl apply -n mysql1 -f c1-mysql.yaml
    

    此命令会部署由三个副本组成的 StatefulSet。在本教程中,MySQL 主集群部署在 us-west1 中的三个可用区中。输出类似于以下内容:

    service/mysql created
    statefulset.apps/dbc1 created
    

    在本教程中,资源限制和请求设置为最小值以节省费用。在规划生产工作负载时,请务必根据贵组织的需求适当设置这些值。

  5. 验证 StatefulSet 已成功创建。

    kubectl get statefulset -n mysql1 --watch
    

    StatefulSet 可能需要大约 10 分钟才能准备就绪。

  6. 当所有三个 Pod 都处于就绪状态时,使用 Ctrl+C 退出该命令。如果您看到 CPU 或内存不足导致的 PodUnscheduleable 错误,请等待几分钟,以便控制平面调整大小以适应大型工作负载。

    输出类似于以下内容:

    NAME   READY   AGE
    dbc1   1/3     39s
    dbc1   2/3     50s
    dbc1   3/3     73s
    
  7. 如需检查 Pod 在 GKE 集群节点上的放置位置,请运行此脚本:

    bash ../scripts/inspect_pod_node.sh mysql1 mysql
    

    输出结果显示 Pod 名称、GKE 节点名称和预配该节点的可用区,如下所示:

    gke-gkemulti-west-5-default-pool-4bcaca65-jch0 us-west1-b dbc1-0
    gke-gkemulti-west-5-default-pool-1ac6e8b5-ddjx us-west1-c dbc1-1
    gke-gkemulti-west-5-default-pool-1f5baa66-bf8t us-west1-a dbc1-2
    

    输出中的列分别表示主机名、Cloud 可用区和 Pod 名称。

    StatefulSet 规范 (c1-mysql.yaml) 中的 topologySpreadConstraints 政策指示调度器将 Pod 均匀地分布在故障域 (topology.kubernetes.io/zone) 中。

    podAntiAffinity 政策强制执行限制条件,规定 Pod 不能位于同一 GKE 集群节点 (kubernetes.io/hostname) 中。对于 MySQL 实例 Pod,此政策会使 Pod 均匀分布在 Google Cloud 区域的三个可用区中。此放置策略将每个数据库实例放置在单独的故障域中,从而实现 MySQL InnoDB 集群的高可用性。

准备 MySQL InnoDB 主集群

如需配置 MySQL InnoDB 集群,请遵循以下步骤:

  1. Cloud Shell 终端中,为要添加到集群的 MySQL 实例设置群组复制配置。

    bash ../scripts/c1-clustersetup.sh
    
    POD_ORDINAL_START=${1:-0}
    POD_ORDINAL_END=${2:-2}
    for i in $(seq ${POD_ORDINAL_START} ${POD_ORDINAL_END}); do
      echo "Configuring pod mysql1/dbc1-${i}"
      cat <<'  EOF' | kubectl -n mysql1 exec -i dbc1-${i} -- bash -c 'mysql -uroot -proot --password=${MYSQL_ROOT_PASSWORD}'
    INSTALL PLUGIN group_replication SONAME 'group_replication.so';
    RESET PERSIST IF EXISTS group_replication_ip_allowlist;
    RESET PERSIST IF EXISTS binlog_transaction_dependency_tracking;
    SET @@PERSIST.group_replication_ip_allowlist = 'mysql.mysql1.svc.cluster.local';
    SET @@PERSIST.binlog_transaction_dependency_tracking = 'WRITESET';
      EOF
    done

    该脚本将远程连接到三个 MySQL 实例中的每一个,以设置和保留以下环境变量:

    • group_replication_ip_allowlist:允许集群中的实例连接到组中的任意实例。
    • binlog_transaction_dependency_tracking='WRITESET':允许不发生冲突的并行事务。

    在 8.0.22 之前的 MySQL 版本中,使用 group_replication_ip_whitelist 而不是 group_replication_ip_allowlist

  2. 打开第二个终端,这样您就不需要为每个 Pod 创建一个 shell。

  3. 在 Pod dbc1-0 上连接到 MySQL Shell。

    kubectl -n mysql1 exec -it dbc1-0 -- \
        /bin/bash \
        -c 'mysqlsh --uri="root:$MYSQL_ROOT_PASSWORD@dbc1-0.mysql.mysql1.svc.cluster.local"'
    
  4. 验证 MySQL 组复制许可名单以连接到其他实例。

    \sql SELECT @@group_replication_ip_allowlist;
    

    输出类似于以下内容:

    +----------------------------------+
    | @@group_replication_ip_allowlist |
    +----------------------------------+
    | mysql.mysql1.svc.cluster.local   |
    +----------------------------------+
    
  5. 验证各个实例上的 server-id 都是唯一的。

    \sql SELECT @@server_id;
    

    输出类似于以下内容:

    +-------------+
    | @@server_id |
    +-------------+
    |          21 |
    +-------------+
    
  6. 配置每个实例以在 MySQL InnoDB 集群中使用,并在每个实例上创建管理员账号。

    \js
    dba.configureInstance('root@dbc1-0.mysql.mysql1.svc.cluster.local', {password: os.getenv("MYSQL_ROOT_PASSWORD"),clusterAdmin: 'icadmin', clusterAdminPassword: os.getenv("MYSQL_ADMIN_PASSWORD")});
    dba.configureInstance('root@dbc1-1.mysql.mysql1.svc.cluster.local', {password: os.getenv("MYSQL_ROOT_PASSWORD"),clusterAdmin: 'icadmin', clusterAdminPassword: os.getenv("MYSQL_ADMIN_PASSWORD")});
    dba.configureInstance('root@dbc1-2.mysql.mysql1.svc.cluster.local', {password: os.getenv("MYSQL_ROOT_PASSWORD"),clusterAdmin: 'icadmin', clusterAdminPassword: os.getenv("MYSQL_ADMIN_PASSWORD")});
    

    所有实例必须具有相同的用户名和密码,才能使 MySQL InnoDB 集群正常运行。每个命令都会生成类似以下内容的输出:

    ...
    
    The instance 'dbc1-2.mysql:3306' is valid to be used in an InnoDB cluster.
    
    Cluster admin user 'icadmin'@'%' created.
    The instance 'dbc1-2.mysql.mysql1.svc.cluster.local:3306' is already
    ready to be used in an InnoDB cluster.
    
    Successfully enabled parallel appliers.
    
  7. 验证实例已准备好在 MySQL InnoDB 集群中使用。

    dba.checkInstanceConfiguration()
    

    输出类似于以下内容:

    ...
    
    The instance 'dbc1-0.mysql.mysql1.svc.cluster.local:3306' is valid to be used in an InnoDB cluster.
    
    {
        "status": "ok"
    }
    

    (可选)您可以连接到每个 MySQL 实例并重复此命令。例如,运行以下命令以检查 dbc1-1 实例上的状态:

    kubectl -n mysql1 exec -it dbc1-0 -- \
        /bin/bash \
        -c 'mysqlsh --uri="root:$MYSQL_ROOT_PASSWORD@dbc1-1.mysql.mysql1.svc.cluster.local" \
        --js --execute "dba.checkInstanceConfiguration()"'
    

创建 MySQL InnoDB 主集群

接下来,使用 MySQL Admin createCluster 命令创建 MySQL InnoDB 集群。从 dbc1-0 实例(它将是集群的主实例)开始,然后向集群添加两个副本实例。

如需初始化 MySQL InnoDB 集群,请遵循以下步骤:

  1. 创建 MySQL InnoDB 集群。

    var cluster=dba.createCluster('mycluster');
    

    运行 createCluster 命令会触发以下操作:

    • 部署元数据架构。
    • 验证组复制的配置正确无误。
    • 将其注册为新集群的种子实例。
    • 创建必要的内部账号,例如复制用户账号。
    • 开始组复制。

    此命令会初始化 MySQL InnoDB 集群,并使用主机 dbc1-0 作为主实例。集群引用存储在集群变量中。

    输出类似于以下内容:

    A new InnoDB cluster will be created on instance 'dbc1-0.mysql:3306'.
    
    Validating instance configuration at dbc1-0.mysql:3306...
    
    This instance reports its own address as dbc1-0.mysql.mysql1.svc.cluster.local:3306
    
    Instance configuration is suitable.
    NOTE: Group Replication will communicate with other instances using
    'dbc1-0.mysql:33061'. Use the localAddress
    option to override.
    
    Creating InnoDB cluster 'mycluster' on
    'dbc1-0.mysql.mysql1.svc.cluster.local:3306'...
    
    Adding Seed Instance...
    Cluster successfully created. Use Cluster.addInstance() to add MySQL
    instances.
    At least 3 instances are needed for the cluster to be able to withstand
    up to one server failure.
    
  2. 将第二个实例添加到集群。

    cluster.addInstance('icadmin@dbc1-1.mysql', {password: os.getenv("MYSQL_ROOT_PASSWORD"), recoveryMethod: 'clone'});
    
  3. 将剩余的实例添加到集群。

    cluster.addInstance('icadmin@dbc1-2.mysql', {password: os.getenv("MYSQL_ROOT_PASSWORD"), recoveryMethod: 'clone'});
    

    输出类似于以下内容:

    ...
    The instance 'dbc1-2.mysql:3306' was successfully added to the cluster.
    
  4. 验证集群的状态。

    cluster.status()
    

    此命令会显示集群的状态。该拓扑由三个主机组成,即一个主实例和两个辅助实例。或者,您也可以调用 cluster.status({extended:1})

    输出类似于以下内容:

    {
        "clusterName": "mysql1",
        "defaultReplicaSet": {
            "name": "default",
            "primary": "dbc1-0.mysql:3306",
            "ssl": "REQUIRED",
            "status": "OK",
            "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
            "topology": {
                "dbc1-0.mysql:3306": {
                    "address": "dbc1-0.mysql:3306",
                    "memberRole": "PRIMARY",
                    "mode": "R/W",
                    "readReplicas": {},
                    "replicationLag": null,
                    "role": "HA",
                    "status": "ONLINE",
                    "version": "8.0.28"
                },
                "dbc1-1.mysql:3306": {
                    "address": "dbc1-1.mysql:3306",
                    "memberRole": "SECONDARY",
                    "mode": "R/O",
                    "readReplicas": {},
                    "replicationLag": null,
                    "role": "HA",
                    "status": "ONLINE",
                    "version": "8.0.28"
                },
                "dbc1-2.mysql:3306": {
                    "address": "dbc1-2.mysql:3306",
                    "memberRole": "SECONDARY",
                    "mode": "R/O",
                    "readReplicas": {},
                    "replicationLag": null,
                    "role": "HA",
                    "status": "ONLINE",
                    "version": "8.0.28"
                }
            },
            "topologyMode": "Single-Primary"
        },
        "groupInformationSourceMember": "dbc1-0.mysql:3306"
    }
    

    您还可以调用 cluster.status({extended:1}) 来获取其他状态详细信息。

创建示例数据库

如需创建示例数据库,请按照以下步骤操作:

  1. 创建数据库并将数据加载到数据库中。

    \sql
    create database loanapplication;
    use loanapplication
    CREATE TABLE loan (loan_id INT unsigned AUTO_INCREMENT PRIMARY KEY, firstname VARCHAR(30) NOT NULL, lastname VARCHAR(30) NOT NULL , status VARCHAR(30) );
    
  2. 将示例数据插入数据库。如要插入数据,您必须连接到集群的主实例。

    INSERT INTO loan (firstname, lastname, status) VALUES ( 'Fred','Flintstone','pending');
    INSERT INTO loan (firstname, lastname, status) VALUES ( 'Betty','Rubble','approved');
    
  3. 验证该表包含上一步中插入的三行。

    SELECT * FROM loan;
    

    输出类似于以下内容:

    +---------+-----------+------------+----------+
    | loan_id | firstname | lastname   | status   |
    +---------+-----------+------------+----------+
    |       1 | Fred      | Flintstone | pending  |
    |       2 | Betty     | Rubble     | approved |
    +---------+-----------+------------+----------+
    2 rows in set (0.0010 sec)
    

创建 MySQL InnoDB ClusterSet

您可以使用专用的 ClusterSet 复制渠道创建 MySQL InnoDB ClusterSet,以管理从主集群到副本集群的复制。

MySQL InnoDB ClusterSet 可将 MySQL InnoDB 主集群与它自己在其他位置(例如多个可用区和多个区域)的一个或多个副本相连,从而为 MySQL InnoDB 集群部署提供灾难容忍。

如果您关闭了 MySQL Shell,请在新的 Cloud Shell 终端中运行以下命令来创建新的 shell:

  kubectl -n mysql1 exec -it dbc1-0 -- \
      /bin/bash -c 'mysqlsh \
      --uri="root:$MYSQL_ROOT_PASSWORD@dbc1-0.mysql.mysql1.svc.cluster.local"'

如需创建 MySQL InnoDB ClusterSet,请按照以下步骤操作:

  1. 在 MySQL Shell 终端中,获取集群对象。

    \js
    cluster=dba.getCluster()
    

    输出类似于以下内容:

    <Cluster:mycluster>
    
  2. 使用存储在集群对象中的现有 MySQL InnoDB 主集群初始化 MySQL InnoDB ClusterSet。

    clusterset=cluster.createClusterSet('clusterset')
    

    输出类似于以下内容:

    A new ClusterSet will be created based on the Cluster 'mycluster'.
    
    * Validating Cluster 'mycluster' for ClusterSet compliance.
    
    * Creating InnoDB ClusterSet 'clusterset' on 'mycluster'...
    
    * Updating metadata...
    
    ClusterSet successfully created. Use ClusterSet.createReplicaCluster() to add Replica Clusters to it.
    
    <ClusterSet:clusterset>
    
  3. 检查 MySQL InnoDB ClusterSet 的状态。

    clusterset.status()
    

    输出类似于以下内容:

    {
        "clusters": {
            "mycluster": {
                "clusterRole": "PRIMARY",
                "globalStatus": "OK",
                "primary": "dbc1-0.mysql:3306"
            }
        },
        "domainName": "clusterset",
        "globalPrimaryInstance": "dbc1-0.mysql:3306",
        "primaryCluster": "mycluster",
        "status": "HEALTHY",
        "statusText": "All Clusters available."
    }
    

    (可选)您可以调用 clusterset.status({extended:1}) 来获取其他状态详细信息,包括有关集群的信息。

  4. 退出 MySQL Shell。

    \q
    

部署 MySQL 路由器

您可以部署 MySQL 路由器,以将客户端应用流量定向到正确的集群。路由基于发出数据库操作的应用的连接端口:

  • 写入操作会路由到主 ClusterSet 中的主集群实例。
  • 读取操作可以路由到主集群中的任何实例。

当您启动 MySQL 路由器时,它会针对 MySQL InnoDB ClusterSet 部署引导。与 MySQL InnoDB ClusterSet 连接的 MySQL 路由器实例可识别任何受控制的切换或紧急故障切换,并将流量定向到新的主集群。

如需部署 MySQL 路由器,请按照以下步骤操作:

  1. 在 Cloud Shell 终端中,部署 MySQL 路由器。

    kubectl apply -n mysql1 -f c1-router.yaml
    

    输出类似于以下内容:

    configmap/mysql-router-config created
    service/mysql-router created
    deployment.apps/mysql-router created
    
  2. 检查 MySQL 路由器部署的就绪情况。

    kubectl -n mysql1 get deployment mysql-router --watch
    

    三个 Pod 都准备就绪后,输出类似于以下内容:

    NAME           READY   UP-TO-DATE   AVAILABLE   AGE
    mysql-router   3/3     3            0           3m36s
    

    如果您在控制台中看到 PodUnschedulable 错误,请等待一两分钟,此时 GKE 会预配更多节点。刷新后,您应该会看到 3/3 OK

  3. 在现有集群的任何成员上启动 MySQL Shell。

    kubectl -n mysql1 exec -it dbc1-0 -- \
        /bin/bash -c 'mysqlsh --uri="root:$MYSQL_ROOT_PASSWORD@dbc1-0.mysql"'
    

    此命令会连接到 dbc1-0 Pod,然后启动连接到 dbc1-0 MySQL 实例的 shell。

  4. 验证路由器配置。

    clusterset=dba.getClusterSet()
    clusterset.listRouters()
    

    输出类似于以下内容:

    {
      "domainName": "clusterset",
      "routers": {
        "mysql-router-7cd8585fbc-74pkm::": {
            "hostname": "mysql-router-7cd8585fbc-74pkm",
            "lastCheckIn": "2022-09-22 23:26:26",
            "roPort": 6447,
            "roXPort": 6449,
            "rwPort": 6446,
            "rwXPort": 6448,
            "targetCluster": null,
            "version": "8.0.27"
        },
        "mysql-router-7cd8585fbc-824d4::": {
          ...
        },
        "mysql-router-7cd8585fbc-v2qxz::": {
          ...
        }
      }
    }
    
  5. 退出 MySQL Shell。

    \q
    
  6. 运行此脚本以检查 MySQL 路由器 Pod 的放置位置。

    bash ../scripts/inspect_pod_node.sh mysql1 | sort
    

    该脚本会显示 mysql1 命名空间中所有 Pod 的节点和 Cloud 可用区位置,输出类似于以下内容:

    gke-gkemulti-west-5-default-pool-1ac6e8b5-0h9v us-west1-c mysql-router-6654f985f5-df97q
    gke-gkemulti-west-5-default-pool-1ac6e8b5-ddjx us-west1-c dbc1-1
    gke-gkemulti-west-5-default-pool-1f5baa66-bf8t us-west1-a dbc1-2
    gke-gkemulti-west-5-default-pool-1f5baa66-kt03 us-west1-a mysql-router-6654f985f5-qlfj9
    gke-gkemulti-west-5-default-pool-4bcaca65-2l6s us-west1-b mysql-router-6654f985f5-5967d
    gke-gkemulti-west-5-default-pool-4bcaca65-jch0 us-west1-b dbc1-0
    

    您可以观察到 MySQL 路由器 Pod 在各个可用区中均匀分布;也就是说,它不与 MySQL Pod 位于同一节点上,也不与另一个 MySQL 路由器 Pod 位于同一节点上。

管理 GKE 和 MySQL InnoDB 集群升级

MySQL 和 Kubernetes 的更新会定期发布。请遵循运营最佳实践定期更新您的软件环境。默认情况下,GKE 会为您管理集群和节点池升级。Kubernetes 和 GKE 还提供了其他功能来辅助 MySQL 软件升级。

规划 GKE 升级

您可以执行主动步骤并设置配置,以在运行有状态服务时缓解风险并使集群升级更加顺畅,包括:

  • Standard 集群:遵循 GKE 升级集群的最佳实践。选择适合的升级策略,以确保在维护窗口内进行升级:

    • 如果费用优化非常重要,并且工作负载可以容忍 60 分钟以内的正常关停,请选择超额配置升级
    • 如果工作负载对中断的容忍度较低,并且能够接受因资源用量增加而导致的临时费用增加,请选择蓝绿升级

    如需了解详情,请参阅升级运行有状态工作负载的集群。根据您选择的发布渠道,Autopilot 集群会自动升级

  • 请使用维护窗口来确保升级在预期时间进行。在维护窗口之前,请确保数据库备份成功。

  • 在允许流量进入升级后的 MySQL 节点之前,请使用就绪性探测和活跃性探测来确保节点已准备好接受流量。

  • 创建探测,以在接受流量之前评估复制是否同步。此任务可以通过自定义脚本完成,具体取决于数据库的复杂程度和规模。

设置 Pod 中断预算 (PDB) 政策

当 MySQL InnoDB 集群在 GKE 上运行时,任意时刻都必须有足够的实例在运行,以满足仲裁要求。

在本教程中,MySQL 集群包含三个实例,则必须有两个实例可用才能形成仲裁。PodDisruptionBudget 政策使您可以限制在任何给定时刻可终止的 Pod 数量。这对于有状态服务的稳定状态操作和集群升级非常有用。

为确保仅有限数量的 Pod 可同时中断,请将工作负载的 PDB 设置为 maxUnavailable: 1。这样可以确保在服务运行过程中的任何时刻,最多只有一个 Pod 没有在运行。

以下 PodDisruptionBudget 政策清单将 MySQL 应用的最大不可用 Pod 数设置为一。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: mysql-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: mysql

如需将 PDB 政策应用于集群,请按照以下步骤操作:

  1. 使用 kubectl 应用 PDB 政策。

    kubectl apply -n mysql1 -f mysql-pdb-maxunavailable.yaml
    
  2. 查看 PDB 的状态。

    kubectl get poddisruptionbudgets -n mysql1 mysql-pdb -o yaml
    

    在输出的 status 部分中,查看 currentHealthydesiredHealthy Pod 计数。输出类似于以下内容:

    status:
    ...
      currentHealthy: 3
      desiredHealthy: 2
      disruptionsAllowed: 1
      expectedPods: 3
    ...
    

规划 MySQL 二进制文件升级

Kubernetes 和 GKE 提供可协助进行 MySQL 二进制文件升级的功能。但是,您需要执行一些操作来为升级做准备。

在开始升级过程之前,请牢记以下注意事项:

  • 升级应先在测试环境中执行。对于生产系统,您应该在测试环境中执行进一步测试。
  • 对于某些二进制文件版本,执行升级后便无法降级版本。请花些时间了解升级的影响。
  • 复制来源可以复制到较新版本。但是,通常不支持从新版本复制到旧版本。
  • 在部署升级后的版本之前,请确保您拥有完整的数据库备份。
  • 请记住,Kubernetes Pod 具有临时性。重新部署 Pod 时,Pod 存储在非永久性卷中的所有配置状态都将丢失。
  • 对于 MySQL 二进制文件升级,请使用上文介绍的相同 PDB、节点池更新策略和探测。

在生产环境中,您应遵循以下最佳实践:

  • 使用新版 MySQL 创建容器映像。
  • 在源代码控制代码库中保留映像构建说明。
  • 使用自动映像构建和测试流水线(如 Cloud Build),并将映像二进制文件存储在 Artifact Registry 等映像存储库中。

为简化本教程,您将不会构建和保留容器映像,而将使用公共 MySQL 映像。

部署升级后的 MySQL 二进制文件

如需执行 MySQL 二进制文件升级,请发出一个修改 StatefulSet 资源映像版本的声明式命令。GKE 会执行必要的步骤来停止当前 Pod,使用升级后的二进制文件部署新的 Pod,并将永久性磁盘挂接到新的 Pod。

  1. 验证 PDB 已创建。

    kubectl get poddisruptionbudgets -n mysql1
    
  2. 获取有状态集的列表。

    kubectl get statefulsets -n mysql1
    
  3. 使用 app 标签获取正在运行的 Pod 列表。

    kubectl get pods --selector=app=mysql -n mysql1
    
  4. 更新有状态集中的 MySQL 映像。

    kubectl  -n mysql1 \
        set image statefulset/dbc1 \
        mysql=mysql/mysql-server:8.0.30
    

    输出类似于以下内容:

    statefulset.apps/mysql image updated
    
  5. 检查终止 Pod 和新 Pod 的状态。

    kubectl get pods --selector=app=mysql -n mysql1
    

验证 MySQL 二进制文件升级

在升级期间,您可以验证发布、新 Pod 和现有 Service 的状态。

  1. 运行 rollout status 命令来确认升级。

    kubectl rollout status statefulset/dbc1 -n mysql1
    

    输出类似于以下内容:

    partitioned roll out complete: 3 new pods have been updated...
    
  2. 检查有状态集以确认映像版本。

    kubectl get statefulsets -o wide -n mysql1
    

    输出类似于以下内容:

    NAME   READY   AGE   CONTAINERS   IMAGES
    dbc1   3/3     37m   mysql        mysql/mysql-server:8.0.30
    
  3. 检查集群状态。

    kubectl -n mysql1 \
         exec -it dbc1-0 -- \
           /bin/bash \
             -c 'mysqlsh \
             --uri="root:$MYSQL_ROOT_PASSWORD@dbc1-1.mysql.mysql1.svc.cluster.local" \
             --js \
             --execute "print(dba.getClusterSet().status({extended:1})); print(\"\\n\")"'
    

    对于每个集群实例,查找输出中的状态和版本值。输出类似于以下内容:

    ...
      "status": "ONLINE",
      "version": "8.0.30"
    ...
    

回滚上一次的应用部署发布

还原已升级的二进制文件版本的部署时,发布过程会逆转,并会使用先前的映像版本部署一组新的 Pod。

如需将部署还原到先前的工作版本,请使用 rollout undo 命令:

kubectl rollout undo statefulset/dbc1 -n mysql1

输出类似于以下内容:

statefulset.apps/dbc1 rolled back

横向扩容数据库集群

如需横向扩容 MySQL InnoDB 集群,请向 GKE 集群节点池添加更多节点(仅在使用 Standard 集群时才需要此操作),部署更多 MySQL 实例,然后将每个实例添加到现有 MySQL InnoDB 集群。

向 Standard 集群添加节点

如果您使用的是 Autopilot 集群,则无需执行此操作。

如需将节点添加到 Standard 集群,请遵循适用于 Cloud Shell 或 Google Cloud 控制台的以下说明。如需了解详细步骤,请参阅调整节点池的大小

gcloud

在 Cloud Shell 中,将每个代管式实例组中的默认节点池调整为 8 个实例。

gcloud container clusters resize ${CLUSTER_NAME} \
     --node-pool default-pool \
     --num-nodes=8

控制台

如需向 Standard 集群添加节点,请执行以下操作:

  1. 在 Google Cloud 控制台中打开 gkemulti-west1 集群页面。
  2. 选择节点,然后点击默认池
  3. 向下滚动到实例组
  4. 对于每个实例组,将 Number of nodes 值从 5 个节点调整为 8 个节点。

向主集群添加 MySQL Pod

如需部署更多 MySQL Pod 以横向扩容集群,请按照以下步骤操作:

  1. 在 Cloud Shell 中,将 MySQL 部署中的副本数量从三个副本更新为五个副本。

    kubectl scale  -n mysql1 --replicas=5 -f c1-mysql.yaml
    
  2. 验证部署的进度。

    kubectl -n mysql1 get pods --selector=app=mysql -o wide
    

    如需确定 Pod 是否已准备就绪,请使用 --watch 标志来监视部署。如果您使用的是 Autopilot 集群并看到 Pod Unschedulable 错误,则可能表示 GKE 正在预配节点以容纳额外的 Pod。

  3. 为要添加到集群的新 MySQL 实例配置组复制设置

    bash ../scripts/c1-clustersetup.sh 3 4
    

    该脚本会将命令提交到在序数为 3 到 4 的 Pod 上运行的实例。

  4. 打开 MySQL Shell。

    kubectl -n mysql1 \
      exec -it dbc1-0 -- \
          /bin/bash \
            -c 'mysqlsh \
            --uri="root:$MYSQL_ROOT_PASSWORD@dbc1-0.mysql"'
    
  5. 配置两个新的 MySQL 实例。

    dba.configureInstance('root:$MYSQL_ROOT_PASSWORD@dbc1-3.mysql', {password: os.getenv("MYSQL_ROOT_PASSWORD"),clusterAdmin: 'icadmin', clusterAdminPassword: os.getenv("MYSQL_ADMIN_PASSWORD")});
    dba.configureInstance('root:$MYSQL_ROOT_PASSWORD@dbc1-4.mysql', {password: os.getenv("MYSQL_ROOT_PASSWORD"),clusterAdmin: 'icadmin', clusterAdminPassword: os.getenv("MYSQL_ADMIN_PASSWORD")});
    

    这些命令会检查实例是否已正确配置以在 MySQL InnoDB 集群中使用,并执行必要的配置更改。

  6. 将其中一个新实例添加到主集群。

    cluster = dba.getCluster()
    cluster.addInstance('icadmin@dbc1-3.mysql', {password: os.getenv("MYSQL_ROOT_PASSWORD"), recoveryMethod: 'clone'});
    
  7. 将第二个新实例添加到主集群。

    cluster.addInstance('icadmin@dbc1-4.mysql', {password: os.getenv("MYSQL_ROOT_PASSWORD"), recoveryMethod: 'clone'});
    
  8. 获取 ClusterSet 状态,其中也包括集群状态。

    clusterset = dba.getClusterSet()
    clusterset.status({extended: 1})
    

    输出类似于以下内容:

    "domainName": "clusterset",
    "globalPrimaryInstance": "dbc1-0.mysql:3306",
    "metadataServer": "dbc1-0.mysql:3306",
    "primaryCluster": "mycluster",
    "status": "HEALTHY",
    "statusText": "All Clusters available."
    
  9. 退出 MySQL Shell。

    \q
    

清理

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

删除项目

为避免支付费用,最简单的方法是删除您为本教程创建的项目。

Delete a Google Cloud project:

gcloud projects delete PROJECT_ID

后续步骤