ステートフル MySQL クラスタを GKE にデプロイする


このドキュメントは、高可用性 MySQL トポロジを Google Kubernetes Engine にデプロイすることに関心があるデータベース管理者、クラウド アーキテクト、運用の担当者を対象としています。

このチュートリアルに沿って、GKE クラスタに MySQL Router ミドルウェアのみならず、MySQL InnoDB クラスタMySQL InnoDB ClusterSet をデプロイする方法、およびアップグレードの実施方法を学びます。

目標

このチュートリアルの学習内容は次のとおりです。

  • ステートフルな Kubernetes Service を作成してデプロイする。
  • 高可用性対応の MySQL InnoDB クラスタをデプロイする。
  • データベース オペレーション ルーティング用の Router ミドルウェアをデプロイする。
  • 耐障害性のために MySQL InnoDB ClusterSet をデプロイする。
  • MySQL クラスタのフェイルオーバーをシミュレーションする。
  • MySQL のバージョン アップグレードを実行する。

以降のセクションでは、このチュートリアルで構築するソリューションのアーキテクチャについて説明します。

MySQL InnoDB クラスタ

リージョン GKE クラスタで、StatefulSet を使用して、必要な命名と構成の MySQL データベース インスタンスをデプロイし、MySQL InnoDB クラスタを作成します。フォールト トレラントと高可用性を実現するために、3 つのデータベース インスタンス Pod をデプロイします。これにより、コンセンサス プロトコルを使用してプライマリ選出が正常に行われたときに、異なるゾーンの Pod の大半が常に使用可能になり、MySQL InnoDB クラスタが単一ゾーンでの障害に耐えられるようになります。

アプリケーション、MySQL Router、MySQL Cluster の関係を示すアーキテクチャ図
図 1: 単一の MySQL InnoDB クラスタのアーキテクチャの例

デプロイしたら、1 つの Pod をプライマリ インスタンスに指定し、読み取りオペレーションと書き込みオペレーションの両方を処理します。他の 2 つの Pod はセカンダリ読み取り専用レプリカです。プライマリ インスタンスでインフラストラクチャに障害が発生した場合、この 2 つのレプリカ Pod のいずれかをプライマリに昇格できます。

別の Namespace に 3 つの MySQL Router Pod をデプロイして、復元力を高めるために接続ルーティングを提供します。アプリケーションはデータベース サービスに直接接続するのではなく、MySQL Router Pod に接続します。各 Router Pod は、各 MySQL InnoDB Cluster Pod のステータスと目的を認識し、アプリケーション オペレーションを該当する正常な Pod にルーティングします。ルーティング状態は Router Pod でキャッシュに保存され、MySQL InnoDB クラスタの各ノードに保存されているクラスタ メタデータから更新されます。インスタンスに障害が発生した場合、Router はライブ インスタンスへの接続ルーティングを調整します。

MySQL InnoDB ClusterSet

最初の MySQL InnoDB クラスタから、MySQL InnoDB ClusterSet を作成できます。 プライマリ クラスタが使用不能になった場合、耐障害性を向上させることができます。

図は、プライマリとレプリカの MySQL InnoDB クラスタが非同期レプリケーションによって同期が維持される方法を示しています。
図 2: 1 つのプライマリ クラスタと 1 つのレプリカ クラスタを含むマルチリージョン ClusterSet アーキテクチャの例

MySQL InnoDB クラスタのプライマリ インスタンスが使用できなくなった場合は、ClusterSet 内のレプリカ クラスタをプライマリに昇格できます。MySQL Router ミドルウェアを使用する場合、アプリケーションでプライマリ データベース インスタンスの正常性を追跡する必要はありません。選択が行われた後に、新しいプライマリに接続を送信するようにルーティングが調整されます。ただし、MySQL Router ミドルウェアに接続するアプリケーションが復元力を高めるベスト プラクティスに沿って、クラスタ フェイルオーバー中にエラーが発生した場合に接続を再試行するようにするのは、お客様の責任です。

費用

このドキュメントでは、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. Google Cloud Console のプロジェクト セレクタのページで、[プロジェクトを作成] をクリックして新しい Google Cloud プロジェクトの作成を開始します。

    プロジェクト セレクタに移動

  3. Google Cloud プロジェクトで課金が有効になっていることを確認します

  4. GKE API を有効にします。

    API を有効にする

  5. Google Cloud Console のプロジェクト セレクタのページで、[プロジェクトを作成] をクリックして新しい Google Cloud プロジェクトの作成を開始します。

    プロジェクト セレクタに移動

  6. Google Cloud プロジェクトで課金が有効になっていることを確認します

  7. GKE API を有効にします。

    API を有効にする

ロールを設定する

  1. Google アカウントにロールを付与します。次の IAM ロールごとに次のコマンドを 1 回実行します。 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:EMAIL_ADDRESS" --role=ROLE
    • PROJECT_ID は、実際のプロジェクト ID に置き換えます。
    • EMAIL_ADDRESS は実際のメールアドレスに置き換えます。
    • 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
    

    次の値を置き換えます。

  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 のリージョン クラスタを作成します。ゾーンクラスタとは異なり、リージョン クラスタのコントロール プレーンは複数のゾーンにレプリケートされるので、1 つのゾーンでサービスが停止してもコントロール プレーンが使用できなくなることはありません。

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. 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 は 2 つのゾーンにリソースをプロビジョニングします。prepare-for-ha.yaml で定義された Deployment により、Autopilot は、replicas:3requiredDuringSchedulingIgnoredDuringExecutionpodAntiAffinitytopologyKey: "topology.kubernetes.io/zone" を設定して、クラスタ内の 3 つのゾーンにノードをプロビジョニングします。

  4. Deployment のステータスを確認します。

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

    準備完了状態の Pod が 3 つ表示されたら、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 が 3 つのゾーンにデプロイされていることを確認します。

    bash ../scripts/inspect_pod_node.sh default
    

    出力の各行は Pod に対応し、2 番目の列はクラウドゾーンを示します。出力は次のようになります。

    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 をデプロイする

このセクションでは、1 つの MySQL StatefulSet をデプロイします。各 StatefulSet は、3 つの 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 Persistent Disk タイプを使用します。volumeBindingMode フィールドは WaitForFirstConsumer に設定されています。つまり、GKE は Pod が作成されるまで PersistentVolume のプロビジョニングを遅延します。これにより、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
    

    このコマンドは、3 つのレプリカで構成される StatefulSet をデプロイします。このチュートリアルでは、プライマリ MySQL クラスタは us-west1 の 3 つのゾーンにデプロイされます。出力は次のようになります。

    service/mysql created
    statefulset.apps/dbc1 created
    

    このチュートリアルでは、費用を節約するために、リソースの上限とリクエストを最小値に設定します。本番環境のワークロードを計画する場合は、これらの値を組織のニーズに合わせて 3 つの値を適切に設定してください。

  5. StatefulSet が正常に作成されたことを確認します。

    kubectl get statefulset -n mysql1 --watch
    

    StatefulSet の準備が整うまでに 10 分ほどかかることがあります。

  6. 3 つの Pod がすべて準備完了の状態になったら、Ctrl+C を使用してコマンドを終了します。CPU またはメモリ不足が原因で PodUnscheduleable エラーが発生した場合は、大きなワークロードに対応できるようにコントロール プレーンのサイズが変更されるまで数分待ちます。

    出力は次のようになります。

    NAME   READY   AGE
    dbc1   1/3     39s
    dbc1   2/3     50s
    dbc1   3/3     73s
    
  7. GKE クラスタノード上の Pod の配置を検査するには、次のスクリプトを実行します。

    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
    

    出力の列は、ホスト名、クラウドゾーン、Pod 名をそれぞれ表します。

    StatefulSet 仕様の topologySpreadConstraints ポリシー(c1-mysql.yaml)は、障害発生ドメイン(topology.kubernetes.io/zone)全体で Pod を均等に配置するようスケジューラに指示します。

    podAntiAffinity ポリシーでは、同じ GKE クラスタノード(kubernetes.io/hostname)に Pod を配置しないように制約が適用されます。MySQL インスタンス Pod の場合、このポリシーにより、Google Cloud リージョンの 3 つのゾーンに Pod が均等にデプロイされます。この配置により、各データベース インスタンスを別々の障害発生ドメインに配置することで、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

    このスクリプトは、3 つの MySQL インスタンスにそれぞれリモート接続し、次の環境変数を設定して保持します。

    • group_replication_ip_allowlist: クラスタ内のインスタンスがグループ内の任意のインスタンスに接続できるようにします。
    • binlog_transaction_dependency_tracking='WRITESET': 競合しない並列トランザクションを許可します。

    8.0.22 より前の MySQL バージョンでは、group_replication_ip_allowlist ではなく group_replication_ip_whitelist を使用します。

  2. Pod ごとにシェルを作成する必要がないように、2 つ目のターミナルを開きます。

  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 管理の createCluster コマンドを使用して MySQL InnoDB クラスタを作成します。dbc1-0 インスタンス(クラスタのプライマリ インスタンス)から始めて、2 つのレプリカをクラスタに追加します。

MySQL InnoDB クラスタを初期化する手順は次のとおりです。

  1. MySQL InnoDB クラスタを作成します。

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

    createCluster コマンドを実行すると、次のオペレーションがトリガーされます。

    • メタデータ スキーマをデプロイします。
    • グループ レプリケーションの構成が正しいことを確認します。
    • これを新しいクラスタのシード インスタンスとして登録します。
    • レプリケーション ユーザー アカウントなど、必要な内部アカウントを作成します。
    • グループのレプリケーションを開始します。

    このコマンドは、ホスト dbc1-0 をプライマリとして MySQL InnoDB クラスタを初期化します。クラスタ参照はクラスタ変数に格納されます。

    出力は次のようになります。

    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. 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()
    

    このコマンドにより、クラスタのステータスが表示されます。このトポロジは、3 つのホスト(1 つのプライマリ インスタンスと 2 つのセカンダリ インスタンス)で構成されています。必要に応じて、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. テーブルに、前の手順で挿入した 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 を作成する

MySQL InnoDB ClusterSet を作成すると、専用の ClusterSet レプリケーション チャネルを使用して、プライマリ クラスタからレプリカ クラスタへのレプリケーションを管理できます。

MySQL InnoDB ClusterSet は、複数のゾーンや複数のリージョンなどの代替のロケーションにある複数のレプリカを使用してプライマリ MySQL InnoDB クラスタをリンクさせて、MySQL InnoDB クラスタ デプロイの耐障害性を提供します。

MySQL Shell を閉じた場合は、新しい Cloud 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 Router をデプロイする

MySQL Router をデプロイして、クライアント アプリケーションのトラフィックを適切なクラスタに転送できます。ルーティングは、データベース オペレーションを発行するアプリケーションの接続ポートに基づきます。

  • 書き込みは、プライマリ ClusterSet のプライマリ クラスタ インスタンスにルーティングされます。
  • 読み取りは、プライマリ クラスタ内の任意のインスタンスにルーティングできます。

MySQL Router を起動すると、MySQL InnoDB ClusterSet デプロイに対してブートストラップされます。MySQL InnoDB ClusterSet に接続されている MySQL Router インスタンスは、制御されたスイッチオーバーまたは緊急フェイルオーバーを認識し、トラフィックを新しいプライマリ クラスタに転送します。

MySQL Router をデプロイする手順は次のとおりです。

  1. Cloud Shell ターミナルで、MySQL Router をデプロイします。

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

    出力は次のようになります。

    configmap/mysql-router-config created
    service/mysql-router created
    deployment.apps/mysql-router created
    
  2. MySQL Router のデプロイの準備状況を確認します。

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

    3 つの Pod がすべて準備完了になると、出力は次のようになります。

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

    コンソールに PodUnschedulable エラーが表示された場合は、GKE が追加のノードをプロビジョニングするまで 1~2 分待ちます。更新すると、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 インスタンスに接続されたシェルを起動します。

  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 Router Pod の配置を調べます。

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

    このスクリプトは、mysql1 Namespace 内のすべての Pod のノードとクラウドゾーンの配置を表示します。出力は次のようになります。

    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 Router の Pod がゾーン間で均等に分散されていることがわかります。つまり、MySQL Pod と同じノードに配置されていないか、別の MySQL Router Pod と同じノードに配置されていません。

GKE と MySQL InnoDB クラスタのアップグレードを管理する

MySQL と Kubernetes の更新は定期的にリリースされます。運用のベスト プラクティスに従って、ソフトウェア環境を定期的に更新します。デフォルトでは、GKE はクラスタとノードプールを管理して、アップグレードします。Kubernetes と GKE には、MySQL ソフトウェアのアップグレードを容易にする追加機能も用意されています。

GKE のアップグレードを計画する

ステートフル サービスの実行時に、次のように予防的な措置を講じて、リスクを軽減してクラスタのアップグレードをよりスムーズに行うための構成を設定できます。

  • 標準クラスタ: クラスタのアップグレードに関する GKE のベスト プラクティスに沿って実施します。適切なアップグレード戦略を選択して、メンテナンスの時間枠の期間内にアップグレードが確実に行われるようにします。

    • コストの最適化が重要であり、ワークロードが 60 分未満で正常なシャットダウンを許容できる場合は、サージ アップグレードを選択します。
    • ワークロードの中断を許容できず、リソース使用量の増加によって一時的な費用の増加が許容される場合は、Blue/Green アップグレードを選択します。

    詳細については、ステートフル ワークロードを実行するクラスタをアップグレードするをご覧ください。Autopilot クラスタは、選択したリリース チャンネルに基づいて自動的にアップグレードされます。

  • メンテナンスの時間枠を使用すると、アップグレードが意図したときに行われるようになります。メンテナンスの時間枠の前に、データベースのバックアップが成功したことを確認します。

  • アップグレードされた MySQL ノードへのトラフィックを許可する前に、Readiness プローブと Liveness プローブを使用して、トラフィックの準備ができていることを確認します。

  • トラフィックの受信前に、レプリケーションが同期しているかどうかを評価するプローブを作成します。これは、データベースの複雑さと規模に応じて、カスタム スクリプトを通じて実行できます。

Pod 停止予算(PDB)ポリシーを設定する

MySQL InnoDB クラスタが GKE で実行されている場合、クォーラムの要件を満たすために十分な数のインスタンスが常に実行されている必要があります。

このチュートリアルでは、3 つのインスタンスから成る MySQL クラスタを想定し、クォーラムを形成するために 2 つのインスタンスを使用できる必要があります。PodDisruptionBudget ポリシーによって、任意の時点で終了できる Pod の数の制限が可能になります。これは、ステートフル サービスの安定状態でのオペレーションと、クラスタのアップグレードの両方に役立ちます。

同時に中断される Pod の数を限定するには、ワークロードの PDB を maxUnavailable: 1 に設定します。これにより、サービス オペレーションのどの時点でも、複数の Pod が実行されないことが保証されます。

次の PodDisruptionBudget ポリシー マニフェストは、MySQL アプリケーションで使用できない Pod の最大数を 1 に設定します。

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 リソースのイメージ バージョンを変更する宣言型コマンドを実行します。現在の Pod を停止して、アップグレードされたバイナリで新しい Pod をデプロイし、新しい Pod に永続ディスクを接続するために必要な手順が GKE により実施されます。

  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"
    ...
    

最後のアプリのデプロイのロールアウトをロールバックする

アップグレードされたバイナリ バージョンの Deployment を元に戻すと、ロールアウト プロセスが逆になり、前のイメージ バージョンで Pod の新しいセットがデプロイされます。

Deployment を以前の作業バージョンに戻すには、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 Deployment のレプリカ数を 3 から 5 に更新します。

    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. 2 つの新しい 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. プライマリ クラスタに 2 つ目の新しいインスタンスを追加します。

    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

次のステップ