MOCO - Kubernetes 用 MySQL クラスタ運用ソフトウェア

サイボウズの Kubernetes 基盤を開発している Neco プロジェクトの ymmt です。 サイボウズ製品のほとんどはデータベースとして MySQL を採用しています。 現在 400 を越える MySQL のインスタンスを運用しており、これら全てを新しい Kubernetes 基盤に移行していく予定です。

Kubernetes 上でアプリケーションやミドルウェアの運用を自動化するソフトウェアのことをオペレーターと言います。 大量の MySQL インスタンスを Kubernetes 基盤に移行するにはオペレーターが必須であると考え、技術顧問の @yoku0825 さんの監修の下で MOCO というソフトウェアを開発しオープンソースライセンスで公開しました。

本記事では Kubernetes 上の MySQL オペレーターの状況と、開発した MOCO の機能を詳細に解説いたします。

f:id:cybozuinsideout:20210531121803p:plain:h240

MySQL オペレーターの状況

私たちは以下の MySQL オペレーターソフトウェアについて採用できるか調査しました。

こちらに示す状況は 2021 年 6 月時点の情報です。

開発状況

Oracle のオペレーターは 2019 年から 2 年間開発が停滞している状況でしたが、つい先日新規に作り直すアナウンスがありました。まだプレビュー段階のようです。

Presslabs のオペレーターは活発に開発されているようですが、MySQL 8 対応版がまだリリースされておらず、またプロダクション利用はしないように明記されています。

Percona のオペレーターは活発に開発され、プロダクション利用可能なようです。

対応製品の違い

Oracle のオペレーターは MySQL (InnoDB Cluster) 専用となっています。

Presslabs と Percona のオペレーターは MySQL 互換製品である Percona Server for MySQL 専用となっています。

レプリケーション方式の違い

MySQL および互換製品の Percona Server for MySQL は複数のレプリケーション方式をサポートしています。

  1. Asynchronous Replication
  2. Semi-synchronous Replication
  3. Group Replication (InnoDB Cluster)
  4. Galera Cluster / XtraDB Cluster

詳しい解説はしませんが、レプリケーション方式により様々な制限事項や障害耐性の違いが生じます。

Asynchronous Replication ではデータベースサーバーの故障時に、過去に成功したトランザクションが消失する可能性があります。

Semi-synchronous Replication では適切に設定すると一台のサーバーが故障してもトランザクションが消失しないようにすることができます。 一方で、故障サーバーが復帰すると errant transaction という不整合データが発生する可能性があります。

Group Replication は Paxos をベースにした分散合意方式により errant transaction の発生を防止します。

Galera Cluster およびそれを元にした XtraDB Cluster は詳細に調査していません。 早々にサイボウズ製品が要求する仕様を満たせないことが判明したためです。

Oracle のオペレーターは Group Replication を採用しています。

Presslabs のオペレーターは現在 Asynchronous Replication のみのようです。Semi-synchronous にも対応を進めている様子は伺えます。

Percona のオペレーターは Galera Cluster を元にした XtraDB Cluster を採用しています。

なぜ MOCO を開発したのか

サイボウズの製品は長年レプリケーションをしない単独の MySQL サーバーを前提として開発してきました。 そのため制限事項が多いレプリケーション方式には移行することが困難な状況です。

具体的には以下の条件を満たす方式が製品開発サイドより求められました。

  1. MySQL 8 に対応していること
  2. フェイルオーバー時にトランザクションを失わないこと
  3. 2 GiB を越える大きさのトランザクションを実行できること
  4. innodb_autoinc_lock_mode に 1 を設定できること
  5. SERIALIZABLE トランザクション分離レベルをサポートしていること

結論から言いますと、これらを満たすオペレーターは存在しませんでした。

Oracle のオペレーターが採用する InnoDB Cluster は制限事項が多く、なかでも 2 GiB 以上のトランザクションを扱えない点が問題になりました。

Presslabs のオペレーターは MySQL 8 と semi-synchronous レプリケーションに未対応です。

Percona のオペレーターは 2 GiB を越えるトランザクションが扱えず、innodb_autoinc_lock_mode を 1 にできず、また SERIALIZABLE の対応は experimental でした。

上記要件を満たすものは、自社で開発を行う以外に方法がありませんでした。

MOCO について

MOCO は 1, 3, もしくは 5 台のインスタンスからなるクラスタを幾つでも作成・管理することができます。 クラスタの中の 1 台だけは書き込みが可能で、プライマリと呼びます。 他の読み込みしかできないインスタンスはレプリカと呼びます。

インスタンス間では GTID を用いた loss-less semi-synchronous replication という方式でデータをリアルタイムで複製しています。

この方式は適切に設定することで、書き込んでいるインスタンスが落ちても他のレプリカにこれまでに成功したトランザクションが全てあることを保証できます。一方で落ちたインスタンスが復帰してくると、他のレプリカに存在しないトランザクション(errant transaction)を持ってしまう可能性があります。

特徴

MOCO は多彩かつ高度な機能を提供しています。ここではまず全機能を箇条書きで説明します。

  • MySQL 8 に対応
  • 1 台のみに書き込みが可能なプライマリ + レプリカ方式を採用
  • CLONEプラグインを利用した高速なレプリカの(再)作成
  • プライマリ用、レプリカ用の Service (Kubernetes のロードバランサ)を提供
  • データの反映が遅れているレプリカを Service から自動的に排除
  • Loss-less semi-synchronization でプライマリ障害時にデータ損失が発生しない
  • Errant transaction を持つインスタンスを自動検出しクラスタから隔離
  • 既存の MySQL から非同期レプリケーションするクラスタを作成可能
  • SERIALIZABLE トランザクション分離レベルをサポート
  • 2 GiB 以上の大きさのトランザクションも扱える
  • innodb_autoinc_lock_mode を 1 に設定できる
  • プライマリの故障を自動検出し、他のレプリカにプライマリを切り替えるフェイルオーバー機能
  • プライマリを手動で切り替えるスイッチオーバー機能
  • プライマリの Pod が削除される際に自動でスイッチオーバーする機能
  • MySQL shell を利用した高速なバックアップ
  • 定期的な自動バックアップおよび手動の臨時バックアップが可能
  • 任意の時点のデータをリストアできる Point-in-Time Recovery (PiTR)
  • クラスタ毎に異なる MySQL のバージョンを利用可能
  • 自動的で安全な MySQL のバージョンアップが可能
  • クラスタ内のインスタンスを作成後に増設可能
  • mysqld_exporter が組み込まれており、多彩なメトリクスを取得可能
  • my.cnf の設定をクラスタ毎に変更可能
  • Pod 定義をカスタマイズ可能
  • Service 定義をカスタマイズ可能
  • Slow query log をサイドカーコンテナの標準出力に転送
  • PodDisruptionBudget を自動設定

あと、1,000 以上のクラスタを管理できるようなスケーラブルな内部アーキテクチャになっています。

使い方

MOCO のインストールは簡単です。ユーザーマニュアルに書いてあるのが正式な手順ですが、端的には cert-manager を入れて MOCO のマニフェストを apply するだけです。

$ curl -fsL https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml | kubectl apply -f -
$ curl -fsL https://github.com/cybozu-go/moco/releases/latest/download/moco.yaml | kubectl apply -f -

MySQL クラスタを作るには以下のように MySQLCluster リソースを作成します。 各フィールドの詳しい説明はリンク先のドキュメントを参照してください。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  # クラスタ内の mysqld インスタンスの数。1, 3, 5 から選択できます。後から増やすこともできます。
  replicas: 3
  podTemplate:
    spec:
      containers:
      # mysqld という名前のコンテナを定義してください。必要に応じて他にコンテナや Pod の設定を入れることもできます。
      - name: mysqld
        # MOCO 用に補助ツールが入ったコンテナイメージを使います
        # 利用できるタグは https://quay.io/repository/cybozu/mysql?tag=latest&tab=tags を見てください。
        # 自分でビルドする方法は https://cybozu-go.github.io/moco/custom-mysqld.html を見てください。
        image: quay.io/cybozu/mysql:8.0.25

      # 上記イメージは UID:GID 10000 で実行されます。
      # もし下記 volumeClaimTemplates が作るファイルシステムが root 以外書けない場合、以下の設定が必要です。
      securityContext:
        fsGroup: 10000
  volumeClaimTemplates:
  # mysql-data という名前の volume claim template を定義してください。
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 100Gi

クラスタが構築される様子は kubectl get mysqlcluster で確認できます。 以下のように HEALTHY が True になれば準備完了です。

$ kubectl get -w mysqlcluster test
NAME   AVAILABLE   HEALTHY   PRIMARY   SYNCED REPLICAS   ERRANT REPLICAS   LAST BACKUP
test                         0                                             <no value>
test   False       False     0                                             <no value>
test   False       False     0                                             <no value>
test   False       False     0         1                                   <no value>
test   True        False     0         2                                   <no value>
test   True        True      0         3                                   <no value>

できた MySQL にアクセスするには kubectl-moco という kubectl のプラグインを使います。 プラグインは https://github.com/cybozu-go/moco/releases/ から Windows 用、Linux 用、Mac 用がダウンロードできます。

以下のようにダウンロードして PATH の通っているディレクトリに kubectl-moco (Windows は kubectl-moco.exe) という名前で配置してください。

$ mkdir -p $HOME/go/bin
$ PATH=$HOME/go/bin:$PATH
$ curl -fsL -o $HOME/go/bin/kubectl-moco https://github.com/cybozu-go/moco/releases/latest/download/kubectl-moco-linux-amd64
$ chmod a+x $HOME/go/bin/kubectl-moco

kubectl-moco には mysql, credential, switchover というサブコマンドがあります。 詳しい仕様はマニュアルを参照してください。

以下は MySQL データベースに書き込み可能ユーザーでプライマリインスタンスにアクセスしてデータベース・テーブルを作成し、データを入れる例です。 mysql コマンドを対話的に実行できます。autocommit=0 に初期設定されているので COMMIT を明示的にしています。

$ kubectl moco -n default mysql -it -u moco-writable test

mysql> CREATE DATABASE foo;
Query OK, 1 row affected (0.00 sec)

mysql> USE foo;
Database changed
mysql> CREATE TABLE t (i INT PRIMARY KEY AUTO_INCREMENT, data TEXT NOT NULL);
Query OK, 0 rows affected (0.03 sec)

mysql> INSERT INTO t (data) VALUES ('aaa'), ('bbb');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

mysql> quit
Bye

以下は読み込み専用ユーザーでレプリカ 1 番にアクセスして先ほどのテーブルの内容を確認する例です。

$ kubectl moco -n default mysql --index 1 test -- -D foo -t -e 'SELECT * FROM t'
+---+------+
| i | data |
+---+------+
| 1 | aaa  |
| 2 | bbb  |
+---+------+

しっかりレプリケーションされていますね。

MOCO は初期設定で moco-readonly, moco-writable, moco-admin というユーザーを作っています。 アプリケーションから利用する場合、これらのユーザーではなく別途ユーザーを作成したほうが良いでしょう。

$ kubectl moco -n default mysql -it -u moco-writable test

mysql> CREATE USER 'foo'@'%' IDENTIFIED BY 'xxx';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT ALL ON foo.* TO 'foo'@'%';
Query OK, 0 rows affected (0.00 sec)

Kubernetes クラスタ内の他の Pod から MySQL を利用するには Service を使います。 以下のように、MOCO は MySQLCluster 一つにつき 3 種類の Service を作成します。

$ kubectl get svc -lapp.kubernetes.io/created-by=moco
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)              AGE
moco-test           ClusterIP   None            <none>        3306/TCP,33060/TCP   40m
moco-test-primary   ClusterIP   10.96.102.109   <none>        3306/TCP,33060/TCP   40m
moco-test-replica   ClusterIP   10.96.59.78     <none>        3306/TCP,33060/TCP   40m

プライマリへの書き込みには *-primary の Service を使います。もちろん読み込みも可能です。 ポート番号 3306 はいわゆる旧来の MySQL プロトコル、33060 は X プロトコル用です。

レプリカからの読み込みには *-replica の Service を使います。 こちらは更新が遅延していないレプリカインスタンスに負荷分散されるようになっています。

内部構成

以下の図は MySQLCluster にたいして MOCO のコントローラー(moco-controller) が行う処理を示しています。

MySQL クラスタの内部構成
MySQL クラスタの内部構成

クラスタを作る中心となるのは StatefulSet です。 spec.replicas で指定した数の Pod が StatefulSet から作成されます。

Pod には mysqld コンテナ以外に以下のサイドカーコンテナが付随します。

  • moco-agent: CLONE 操作やレプリケーション遅延のチェックをする
  • fluent-bit: Slow query log を吸い出して標準出力に転送する
  • mysqld_exporter: mysqld の各種メトリクスを Prometheus 形式で出力する

moco-controllermoco-agent は gRPC で通信します。 この gRPC 通信の保護には mTLS (mutual TLS authentication) が用いられており、そのために moco-controllercert-manager を利用して証明書を発行し、MySQLCluster の namespace に転記しています。

また、moco-admin 等のユーザーのパスワードは moco-controller がランダムに生成して Secret に保存されています。

機能紹介

特徴の項で示した機能について解説していきます。

MySQL 8 に対応 (5.7 以前はサポートしない)

サイボウズは全ての MySQL インスタンスを 8 に更新済みであるため、MOCO は MySQL 8 を前提に設計しました。 MySQL 5.7 以前への対応を切ることで、MySQL 8 から導入された以下のメリットを享受できます。

  • Atomic DDL

    MySQL 8 からは CREATE TABLE 等の DDL が InnoDB に保存されるようになり、不意の障害で中途半端な状態になることがなくなりました。

  • mysqld アップグレード処理の簡素化

    正確には MySQL 8.0.16 からですが、従前 mysql_upgrade というコマンドを実行しないといけなかった処理が不要になりました。

  • LOCK INSTANCE FOR BACKUP

    読んで字のごとくですが、バックアップ中に DDL をブロックしつつ DML は許可する新しいタイプのロックです。 プライマリからバックアップを取得しないといけないような場合にアプリケーションへの影響を最小に留めることができます。

接続の自動切換えと負荷分散

プライマリ・レプリカ方式を採用しているため、書き込めるインスタンスはクラスタ中 1 台だけとなります。 もしプライマリが変わるたびに利用しているアプリケーションが接続先を変更しなければいけないとなると、大変面倒です。

この問題を解決するため MySQL Router という専用のソフトウェアがあるのですが、Kubernetes には宛先 Pod を動的に切り替えることができる Service という仕組みがあります。 Service を使うほうが構成コンポーネントを減らして簡略にできるため、MOCO は Service 方式を採用しています。

  • moco-<クラスタ名>-primary: 現在のプライマリインスタンスに繋がる Service
  • moco-<クラスタ名>-replica: データの反映が遅れてないレプリカインスタンスに負荷分散する Service

データの遅延をどの程度許容するかは MySQLCluster の spec.maxDelaySeconds で調整可能です。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  maxDelaySeconds: 180
  ...

自動フェイルオーバー

フェイルオーバーとは、プライマリインスタンスに障害が発生したときにレプリカの一台を新たなプライマリとして設定変更することです。

moco-controller はプライマリの以下の障害を検出し、自動的にフェイルオーバー操作を実行します。

  • 接続不能
  • データ消失

特徴としては、接続不能時に Node を fencing しなくてもフェイルオーバーを実行できることです。

一般的に、観測者のネットワークが分断されている状況であるサーバーに接続不能となっていてもアプリケーションからは当該サーバーが継続的に利用可能である可能性があります。

この状況で不用意に他のサーバーをプライマリに昇格してしまうと、アプリケーションからは二つ書き込み可能なサーバーが出現してしまい、それぞれに異なるデータを書き込んでしまう恐れ(スプリットブレイン)が発生します。 スプリットブレインを防止するために良く使われる技法が fencing で、当該サーバーの電源を別の手段で落とすといったことが行われます。

MOCO は MySQL の semi-synchronous replication の仕様をうまく利用することで fencing しなくても安全にプライマリを切り替えています。 詳細は省きますが、詳しく知りたい方は設計文書や実装を調べてみてください。

手動および自動のスイッチオーバー

スイッチオーバーとは、正常に稼働しているプライマリインスタンスを速やかにレプリカの一台と役割を入れ替えることです。

MOCO は kubectl moco switchover という kubectl のプラグインコマンドを提供しているので、スイッチオーバーを手動で行うことが可能です。 以下の例では PRIMARY が 0 から 1 に変わっています。

$ kubectl get mysqlclusters test
NAME   AVAILABLE   HEALTHY   PRIMARY   SYNCED REPLICAS   ERRANT REPLICAS   LAST BACKUP
test   True        True      0         3                                   2021-05-31T19:37:58Z

$ kubectl moco switchover test

$ kubectl get mysqlclusters test
NAME   AVAILABLE   HEALTHY   PRIMARY   SYNCED REPLICAS   ERRANT REPLICAS   LAST BACKUP
test   True        True      1         3                                   2021-05-31T19:37:58Z

さらに、MOCO はプライマリインスタンスの Pod の metadata.deletionTimestamp がセットされると自動でスイッチオーバーを開始します。

どのような状況で発生するかというと、例えば MySQLCluster の Pod テンプレートをちょっと修正するといった場面です。 この場合全ての mysqld Pod が順番に削除されて作り直されるので、自動でスイッチオーバー処理を行うことでダウンタイムを極小化するわけです。

実際のダウンタイムがどの程度になるかは、ぜひ検証いただければ嬉しいです!

Errant transaction を持つインスタンスを自動検出しクラスタから隔離

Errant transaction は簡単に言えばレプリカに存在するがプライマリインスタンスには存在しないトランザクションのことです。 本来読み込み専用であるレプリカにこのようなトランザクションが入り込むはずはないのですが、不具合や人為的な操作で紛れ込む可能性はあります。

それ以外に、MySQL の semi-synchronous replication ではプライマリインスタンスが障害を起こし、フェイルオーバーしたあとにレプリカとして復帰すると、新しいプライマリが持っていないトランザクションをクラッシュリカバリー操作の結果持つ可能性があります。

このため MySQL のマニュアルでは以下のように障害を起こしたインスタンスは破棄するように注意しています。

MySQL 公式マニュアルの注意書き
MySQL 公式マニュアルの注意書き

MOCO は安全性を考慮してインスタンスは破棄しません。別のいいかたをするとデータの削除やその後の初期化はしません。 その代わりにプライマリインスタンスにないトランザクションを持つインスタンスを自動的に検出し、クラスタから排除します。 インスタンスの破棄はユーザーが手動で行う必要があります。

既存の MySQL から非同期レプリケーションするクラスタを作成可能

すでに稼働している MySQL からデータをレプリケーションするクラスタを作成できます。 レプリケーション元の MySQL は MOCO で構築したものでも、そうでないものでも大丈夫です。

この機能を利用すると、例えば地理的に離れたデータセンター間で災害対策のためにレプリケーションするといったことが可能です。

ただし Clone Plugin を使って開始時にデータを複製する関係で、レプリケーション元と MOCO のクラスタの MySQL は原則として同じバージョンである必要があります。 また、GTID を有効にしておく必要があります。

詳しい手順はユーザーマニュアルを参照してください。

単体構成の MySQL との高い互換性

Group Replication / InnoDB Cluster や Galera Cluster では満たせない以下の要件を充足できます。

  • SERIALIZABLE トランザクション分離レベルをサポート
  • 2 GiB 以上の大きさのトランザクションを扱える
  • innodb_autoinc_lock_mode を 1 に設定できる

バックアップとリストア

MOCO のクラスタは定期的にバックアップするように設定が可能です。

以下のように BackupPolicy というリソースを作成します。 バックアップデータは Amazon S3 互換のオブジェクトストレージに保存するため、権限を持つ ServiceAccount を指定したり環境変数で必要なクレデンシャルを渡せるようになっています。

apiVersion: moco.cybozu.com/v1beta1
kind: BackupPolicy
metadata:
  namespace: default
  name: daily
spec:
  # CRON 形式でスケジュールが指定可能です
  schedule: "@daily"

  jobConfig:
    # S3 にアクセスするための権限を持つ ServiceAccount を指定できます
    # お使いのオブジェクトストレージがそういう形では認証できない場合、"default" で良いです
    serviceAccountName: default

    # 例えば MinIO なら環境変数でクレデンシャルを渡せます
    env:
    - name: AWS_ACCESS_KEY_ID
      value: minioadmin
    - name: AWS_SECRET_ACCESS_KEY
      value: minioadmin

    # バックアップを保存する bucket の情報です。bucketName 以外はオプションです。
    bucketConfig:
      bucketName: moco
      region: us-east-1
      endpointURL: http://minio.default.svc:9000
      usePathStyle: true

    # 作業ディレクトリ用の volume を指定してください。データが大きいと見込まれる場合
    # emptyDir より generic ephemeral volume 等が適切です
    workVolume:
      emptyDir: {}

作成した BackupPolicy を MySQLCluster から以下のように参照します。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  # 同じ Namespace に存在する BackupPolicy の名前
  backupPolicyName: daily
  ...

そうすると、以下のような CronJob が自動的に作成されます。

$ kubectl get cronjobs
NAME               SCHEDULE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
moco-backup-test   @daily     False     0        <none>          11s

臨時にバックアップを取るには kubectl create job を使います。 空のインスタンスだとバックアップがエラーになるので、何かデータを入れてから実施してください。

$ kubectl create job --from=cronjob/moco-backup-test backup-now
job.batch/backup-now created

$ kubectl get jobs backup-now
NAME         COMPLETIONS   DURATION   AGE
backup-now   1/1           2s         5s

成功したバックアップの情報は MySQLCluster の status.backup に記録されます。 Prometheus のメトリクスとしても出力されます。

$ kubectl get mysqlclusters test
NAME   AVAILABLE   HEALTHY   PRIMARY   SYNCED REPLICAS   ERRANT REPLICAS   LAST BACKUP
test   True        True      0         3                                   2021-05-31T19:20:24Z

$ kubectl get mysqlclusters test -o json | jq .status.backup
{
  "binlogFilename": "binlog.000001",
  "binlogSize": 0,
  "dumpSize": 20480,
  "elapsed": "144.786401ms",
  "gtidSet": "d7f0a656-c243-11eb-8f30-a2a44bcdb3f8:1-3",
  "sourceIndex": 1,
  "sourceUUID": "d6b23081-c243-11eb-8609-aecb58bb22b1",
  "time": "2021-05-31T19:20:24Z",
  "warnings": null,
  "workDirUsage": 7886
}

バックアップは MySQL ShellInstance Dump Utility を使用しています。 この方式は LOCK INSTANCE FOR BACKUP を利用しているためバックアップ中でも DML が実行できます。 また mysqldump, mysqlpump と比較して非常に高速です。

cf. MySQL Shell Dump & Load part 2: Benchmarks

初回バックアップではフルダンプを取るだけで binary log はまだ保存しません。 二回目以降のバックアップで、Point-in-Time Recovery のために binary log を差分保存します。

$ kubectl create job --from=cronjob/moco-backup-test backup-now2
job.batch/backup-now2 created

$ kubectl get jobs backup-now2
NAME          COMPLETIONS   DURATION   AGE
backup-now2   1/1           2s         2s

$ kubectl get mysqlclusters test -o json | jq .status.backup
{
  "binlogFilename": "binlog.000001",
  "binlogSize": 662,
  "dumpSize": 20480,
  "elapsed": "197.541044ms",
  "gtidSet": "d7f0a656-c243-11eb-8f30-a2a44bcdb3f8:1-5",
  "sourceIndex": 1,
  "sourceUUID": "d6b23081-c243-11eb-8609-aecb58bb22b1",
  "time": "2021-05-31T19:37:58Z",
  "warnings": null,
  "workDirUsage": 7879
}

リストアするには以下のように spec.restore を指定した MySQLCluster を作成します。 restorePoint にはリストアしたいデータの日時を RFC3339 形式で指定します。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: restore
spec:
  restore:
    # リストアしたい MySQLCluster の namespace と name を指定してください
    # MySQLCluster リソース自体は失われていても問題ありません
    sourceNamespace: default
    sourceName: test

    # リストアしたいデータの日時を RFC3339 形式で指定してください
    restorePoint: "2021-05-31T19:37:20Z"

    # BackupPolicy の jobConfig と同様
    jobConfig:
      serviceAccountName: default
      env:
      - name: AWS_ACCESS_KEY_ID
        value: minioadmin
      - name: AWS_SECRET_ACCESS_KEY
        value: minioadmin
      bucketConfig:
        bucketName: moco
        region: us-east-1
        endpointURL: http://minio.default.svc:9000
        usePathStyle: true
      workVolume:
        emptyDir: {}
  ...

作成すると、moco-restore-<クラスタ名> という Job が自動的に作成されてリストアが開始されます。 この Job はリストア完了後はいつ削除しても大丈夫です。

$ kubectl get jobs moco-restore-restore
NAME                   COMPLETIONS   DURATION   AGE
moco-restore-restore   1/1           14s        25s

$ kubectl get mysqlclusters restore
NAME      AVAILABLE   HEALTHY   PRIMARY   SYNCED REPLICAS   ERRANT REPLICAS   LAST BACKUP
restore   True        True      0         1                                   <no value>

$ kubectl moco mysql restore -- -D foo -t -e 'SELECT * FROM t'
+---+------+
| i | data |
+---+------+
| 1 | aaa  |
| 2 | bbb  |
+---+------+

リストア処理の詳細は Job が作った Pod のログで確認できます。 Job を削除する前にログの内容を確認しておきましょう。

$ kubectl logs moco-restore-restore-vj2ql
{"level":"info","ts":1622490569.9764667,"msg":"waiting for a pod to become ready","name":"moco-restore-0"}
{"level":"info","ts":1622490571.98407,"msg":"waiting for the mysqld to become ready","name":"moco-restore-0"}
{"level":"info","ts":1622490581.0054455,"msg":"restoring from a backup","dump":"moco/default/test/20210531-192024/dump.tar","binlog":"moco/default/test/20210531-192024/binlog.tar.zst"}
Loading DDL, Data and Users from '/work/dump' using 4 threads.
Opening dump...
Target is MySQL 8.0.25. Dump was produced from MySQL 8.0.25
Checking for pre-existing objects...
Executing common preamble SQL
Executing DDL script for schema `foo`
[Worker000] Executing DDL script for `foo`.`t`
Analyzing table `foo`.`t`
[Worker001] foo@t@@0.tsv.zst: Records: 1  Deleted: 0  Skipped: 0  Warnings: 0
Executing user accounts SQL...
Executing common postamble SQL
Resetting GTID_PURGED to dumped gtid set
1 chunks (1 rows, 6 bytes) for 1 tables in 1 schemas were loaded in 1 sec (avg throughput 6.00 B/s)
0 warnings were reported during the load.
{"level":"info","ts":1622490581.1993299,"msg":"loaded dump successfully"}
{"level":"info","ts":1622490581.231039,"msg":"applied binlog successfully"}
{"level":"info","ts":1622490581.2483056,"msg":"restoration finished successfully"}

バージョンアップ

MySQL のバージョンを上げるには、MySQLCluster の mysqld コンテナのイメージを差し替えるだけです。 必要な処理は全て MOCO が自動的に行います。念のため、バージョンアップ前に臨時バックアップを取るのがお勧めです。

なお、ダウングレーディングは MySQL がサポートしていないため MOCO もサポートしていません。

メトリクス

MOCO は各 MySQLCluster および mysqld のインスタンスについて多種のメトリクスを Prometheus 形式で出力しています。

InnoDB の統計情報などは mysqld_exporter で取得できる機能が備わっています。 以下のように spec.collectors を MySQLCluter に指定すると mysqld_exporter がサイドカーコンテナとして追加され、InnoDB や performance_schema の統計情報を出力可能です。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  # mysqld_exporter の collector flag (collect. は省略) を指定してください
  # 詳しくは https://github.com/prometheus/mysqld_exporter/blob/master/README.md#collector-flags
  collectors:
  - engine_innodb_status
  - info_schema.innodb_metrics
  ...

出力されるメトリクスの詳細や scraping ルールについてはユーザーマニュアルをご覧ください。

my.cnf のカスタマイズ

my.cnf の設定は ConfigMap を作成することで行えます。 以下のように data の key-value 形式で指定してください。

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: default
  name: mycnf
data:
  long_query_time: "0"
  innodb_log_file_size: "10M"

作成した ConfigMap 名を以下のように MySQLCluster で指定すれば設定が変更できます。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  # 同一 Namespace 内の ConfigMap 名
  mysqlConfigMapName: mycnf
  ...

performance-schema-instrument 等一部の設定は複数回指定しないといけないため、ConfigMap の key-value 形式では指定できません。

このような場合 _include というキーに my.cnf にそのまま転記される内容を書くことができます。

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: default
  name: mycnf
data:
  _include: |
    performance-schema-instrument='memory/%=ON'
    performance-schema-instrument='wait/synch/%/innodb/%=ON'
    performance-schema-instrument='wait/lock/table/sql/handler=OFF'
    performance-schema-instrument='wait/lock/metadata/sql/mdl=OFF'

innodb_buffer_pool_size の自動設定

innodb_buffer_pool_size は MySQL の性能に極めて重要な影響を持つパラメーターです。

ConfigMap で指定した my.cnfinnodb_buffer_pool_size が指定されておらず、mysqld コンテナの resources.requests.memory (ない場合は resources.limits.memory) が指定されている場合、MOCO はこのパラメータを自動設定します。

例えば以下の MySQLCluster では 100 GiB のメモリをリクエストしているので、innodb_buffer_pool_size70Gi に設定されます。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  podTemplate:
    spec:
      containers:
      - name: mysqld
        image: quay.io/cybozu/mysql:8.0.25
        resources:
          requests:
            memory: 100Gi

Pod 定義のカスタマイズ

これまで見てきたように、MySQL Pod の定義は自在にカスタマイズできます。

お勧めは podAntiAffinity で Pod が複数のホストに分散するように設定することです。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  podTemplate:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                - mysql
              - key: app.kubernetes.io/instance
                operator: In
                values:
                - test   # metadata.name の値を指定
            topologyKey: "kubernetes.io/hostname"
  ...

app.kubernetes.io/nameapp.kubernetes.io/instance は MOCO が自動的につけるラベルです。

Service 定義のカスタマイズ

プライマリとレプリカ用に作成される Service もカスタマイズできます。

例えば type: LoadBalancer にすれば、Kubernetes クラスタの外部からアクセス可能な MySQL になります。

apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  serviceTemplate:
    spec:
      type: LoadBalancer
  ...

Slow query log

MySQL は実行に長時間かかったクエリを記録する slow query log という機能があります。 運用上不可欠に近いものです。

Slow query log は mysqld の標準出力ないし標準エラー出力には出せないため、MOCO slow log の内容を吸い出して標準出力に流すサイドカーコンテナを自動で追加しています。 また、slow query log のファイルは一定時間毎に自動でローテートされています。

$ kubectl logs moco-test-0 -c slow-log
/usr/local/mysql/bin/mysqld, Version: 8.0.25 (Source distribution). started with:
Tcp port: 3306  Unix socket: /run/mysqld.sock
Time                 Id Command    Argument

Loki 等でコンテナのログを収集している環境であれば、slow query log の内容も自動で収集することが可能です。

PodDisruptionBudget を自動設定

PodDisruptionBudget とは Kubernetes の Node 退役処理などで Pod を drain する際、アプリケーションが壊れないように制限を課す機能です。

MOCO はインスタンス数が 3 の MySQLCluster であれば 1 つまで、インスタンス数が 5 の MySQLCluster であれば 2 つまで Pod が drain されて良いような PodDisruptionBudget を自動で作成します。

まとめと今後の予定

サイボウズでは今後 cybozu.com の全ての MySQL インスタンスを自社 Kubernetes 基盤である Neco に移行していきます。 MOCO はその作業の要となるソフトウェアで、近い将来数百のクラスタを稼働することになる見通しです。

MOCO は現時点で最も機能が充実している MySQL オペレーターの一つです。ぜひお試しください。

github.com