KubernetesでMySQLのクラスタツールVitessを動かす

# 目的

DockerやKubernetes(k8s)のおかげで様々なソフトウェアを気軽に試す、あるいは開発で利用できるようになりましたが、本番利用を考えると多くの考慮点を解決しなければいけません。その1つがNFRs（Non-Functional Requirements: 非機能要件）です。例えば可用性の観点では、スケールアウトしにくいRDBはk8sとは相性が悪いです。

有名なMySQLはレプリケーションに対応していますが、マスタが死んだときのスレーブの自動昇格をどうやって実現するかの考慮が必要ですし、昇格までの時間はサービス停止になります。また、Oracle公式のMySQL Clusterはk8sへの対応が明確に表明されてないと思っています。

そこで、今回はそこをよしなにしてくれるVitessをk8sで動かしてみます。

VitessはYouTubeが開発したMySQLのクラスタリングツールです。最近[CNCF](https://cncf.io/)にプロジェクトがホストされたので、それなりに由緒正しいものになりました。

> https://vitess.io/

検証は以下の環境で行いました。

- IBM Cloud Private 2.1.0.2 (Kubernetes 1.9.1)

# 手順

## Helm版について

Chartは以下サイトで公開されていますが、残念ながらまだalphaクオリティとのことです。なので今回は公式サイトの手動の手順に従って検証します。

> https://github.com/vitessio/vitess/tree/master/helm/vitess

## etcd-operatorのインストール

先にetcd-operatorをデプロイしておきます。今回はnamespaceをdefaultからteruに変えます。ServiceAccountはdefaultではなくetcd-operatorという名称で新規作成し、ClusterRoleを割り当てます。RoleではなくClusterRoleにする理由は、最初のetcd-operatorはCRD(Custom Resource Definition)というものを自動作成しますが、それにはRoleではなくClusterRoleが必要になるため、のようです。このように必要最低限の権限を付与するのがk8sの本番運用では考慮が必要になります（これが大変です）。

ServiceAccountを作成します。

```
$ kubectl create sa etcd-operator -n teru
```

ClusterRoleとClusterRoleBindingを作成します。

```
$ git clone https://github.com/coreos/etcd-operator.git
$ cd etcd-operator
$ export SA_NAME=etcd-operator
$ export ROLE_NAME=etcd-operator
$ export ROLE_BINDING_NAME=etcd-operator
$ export NAMESPACE=teru

$ sed -e "s/<ROLE_NAME>/${ROLE_NAME}/g" example/rbac/cluster-role-template.yaml | kubectl create -f - -n ${NAMESPACE}
$ sed -e "s/<ROLE_NAME>/${ROLE_NAME}/g" -e "s/<ROLE_BINDING_NAME>/${ROLE_BINDING_NAME}/g" -e "s/<NAMESPACE>/${NAMESPACE}/g" example/rbac/cluster-role-binding-template.yaml -e "s/  name: default/  name: ${SA_NAME}/" | kubectl create -f - -n ${NAMESPACE}
```

Deploymentを作成します。

```
$ sed -e "s/^    spec:/    spec:\n      serviceAccountName: ${SA_NAME}/" example/deployment.yaml | kubectl create -f - -n ${NAMESPACE}
```

ログにエラーが出てないか確認します。

```
$ kubectl get pod -n teru | grep etcd
etcd-operator-65f6cd5964-l98p6       1/1       Running   0          6m

$ kubectl logs etcd-operator-65f6cd5964-l98p6 -n teru
time="2018-03-28T17:40:25Z" level=info msg="etcd-operator Version: 0.9.1"
time="2018-03-28T17:40:25Z" level=info msg="Git SHA: 16f0e1b3"
time="2018-03-28T17:40:25Z" level=info msg="Go Version: go1.9.4"
time="2018-03-28T17:40:25Z" level=info msg="Go OS/Arch: linux/amd64"
time="2018-03-28T17:40:25Z" level=info msg="Event(v1.ObjectReference{Kind:\"Endpoints\", Namespace:\"teru\", Name:\"etcd-operator\", UID:\"1938312d-32af-11e8-a731-000c293da360\", APIVersion:\"v1\", ResourceVersion:\"603114\", FieldPath:\"\"}): type: 'Normal' reason: 'LeaderElection' etcd-operator-65f6cd5964-l98p6 became leader"
```

## Goのインストール

Vitessの各種ツール類を作成するためにGoをインストールします。

```
$ cd
$ wget https://dl.google.com/go/go1.10.linux-amd64.tar.gz
$ tar zxvf go1.10.linux-amd64.tar.gz 
$ sudo cp -r go /usr/local/
```

環境変数に次のものを追加します。

```text:~/.bash_profile
export GOPATH=${HOME}/go
export PATH=${PATH}:/usr/local/go/bin:${GOPATH}/bin
```

## vtctlclientのビルドとインストール

以下のコマンドでvtctlclientのビルドとインストールをします。成功すれば`${GOPATH}/bin/vtctlclient`が出来ます。

```
$ go get vitess.io/vitess/go/cmd/vtctlclient
$ ls ${GOPATH}/bin/vtctlclient 
/home/teru/go/bin/vtctlclient
```

## バックアップの設定

configure.shを実行します。ここでは主にバックアップをどこに保管するかを指定します。GCS(Google Cloud Storage)かNFSが選べます。今回はNFSにしますが、NFSはPod内にマウントされている必要があります。ここでは/mntにしておきます。

```
$ cd ${GOPATH}/src/vitess.io/vitess/examples/kubernetes
$ ./configure.sh 
Vitess Docker image (leave empty for default) []: 
Backup Storage (file, gcs) [gcs]: file
Root directory for backups (usually an NFS mount): /mnt

NOTE: You must add your NFS mount to the vtctld-controller-template
      and vttablet-pod-template as described in the Kubernetes docs:
      http://kubernetes.io/v1.0/docs/user-guide/volumes.html#nfs
```

NOTEの通り、2つのファイルを修正し、NFS領域をマウントする設定をテンプレートファイルに追記します。

```vtctld-controller-template.yaml
(略)
          volumeMounts
            - name: backup             # 追加
              mountPath: /mnt          # 追加
(略)
       volumes:
        - name: backup                 # 追加
          persistentVolumeClaim:       # 追加
            claimName: vitess-backup   # 追加
```

```vttablet-pod-template.yaml
(略)
          volumeMounts
            - name: backup             # 追加
              mountPath: /mnt          # 追加
(略)
          volumeMounts
            - name: backup             # 追加
              mountPath: /mnt          # 追加
(略)
          persistentVolumeClaim:       # 追加
            claimName: vitess-backup   # 追加
```

PersistentVolumeClaimを作っておきます。私は[以前の記事](https://qiita.com/teruq/items/17609eecc29b70e2cbe7)でNFSの動的プロビジョニングのためのStorageClassを作っておいたので、それを使います。

```vitess-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vitess-backup
  annotations:
    volume.kubernetes.io/storage-class: "nfs"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 8Gi
```

```
$ kubectl create -f vitess-pvc.yaml
```

## etcdの起動

環境変数VITESS_NAMEにnamespaceを、CELLSにセル名をセットし、etcdを実行します。

```
$ export VITESS_NAME=teru
$ export CELLS=vitess

$ ./etcd-up.sh 
Creating etcd service for 'global' cell...
etcdcluster "etcd-global" created
Creating etcd service for 'vittes' cell...
etcdcluster "etcd-vittes" created
```

Podの状態を確認します。しばらく待つとRunningになります。

```
$ kubectl get pods -n teru | grep etcd
etcd-global-2z5mpjjqmt               1/1       Running   0          11s
etcd-global-4kzbnlkqwf               1/1       Running   0          27s
etcd-global-r66b9nsqrq               1/1       Running   0          36s
etcd-operator-65f6cd5964-ff24g       1/1       Running   0          1h
etcd-vitess-5wxv9g7tv8               1/1       Running   0          35s
etcd-vitess-ltfflh2gxq               1/1       Running   0          27s
etcd-vitess-nbxfjqsjxd               1/1       Running   0          11s
```

## vtlctldの起動

```
$ ./vtctld-up.sh 
Creating vtctld ClusterIP service...
service "vtctld" created
Creating vtctld replicationcontroller...
replicationcontroller "vtctld" created
persistentvolumeclaim "vitess-backup" created

To access vtctld web UI, start kubectl proxy in another terminal:
  kubectl proxy --port=8001
Then visit http://localhost:8001/api/v1/proxy/namespaces/teru/services/vtctld:web/
```

言われたとおりに`kubectl proxy --port=8001`してURLにアクセスしてみましょう。まだ何も定義がありませんが、Vitessのコントロールパネルにアクセスできます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/241424/24976098-c215-1e01-bda0-8d61eb95c661.png)

## トポロジーの設定

トポロジーを設定します。

```
./kvtctl.sh AddCellInfo -server_address http://etcd-${CELLS}-client:2379 ${CELLS}
```

## vttabletの起動

vttabletを起動します。vttabletとは、mysqldと同じPodで動くものだそうです。環境変数KEYSPACEに任意の値をセットしておきます。また、起動するPod数を調整します。以下の例では、全体のPod数を3、そのうち1つをReadOnlyにしています。デフォルトでは5個のPodが起動しそのうち2つがReadOnlyなのですが、私の検証機ではスペック不足で全て起動できませんでしたので、数を減らしました。

```
$ export KEYSPACE=teru_keyspace
$ export TABLETS_PER_SHARD=3
$ export RDONLY_COUNT=1
$ ./vttablet-up.sh
Creating teru_keyspace.shard-0 pods in cell vitess...
Creating pod for tablet vitess-0000000100...
pod "vttablet-100" created
Creating pod for tablet vitess-0000000101...
pod "vttablet-101" created
Creating pod for tablet vitess-0000000102...
pod "vttablet-102" created
```

先ほどのコントロールパネルを見ると、teru_keyspaceで1つのシャードができています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/241424/410aecbf-fe05-8f0a-1821-fedf674b2712.png)

Podの状態を表示します。全てRunningになっています。

```
$ kubectl get pods -n teru | grep vtt
vttablet-100                         2/2       Running   1          59s
vttablet-101                         2/2       Running   1          59s
vttablet-102                         2/2       Running   1          59s
```

Tabletの状態を表示します。

```
$ ./kvtctl.sh ListAllTablets ${CELLS}
Starting port forwarding to vtctld...
vitess-0000000100 teru_keyspace 0 replica 10.1.45.220:15002 10.1.45.220:3306 []
vitess-0000000101 teru_keyspace 0 replica 10.1.225.43:15002 10.1.225.43:3306 []
vitess-0000000102 teru_keyspace 0 rdonly 10.1.45.221:15002 10.1.45.221:3306 []
```

Replicaが2つ、ReadOnlyが1つできています。Masterはまだなくてよいです。

## データベースの初期化

データベースを初期化します。

```
$ ./kvtctl.sh InitShardMaster -force ${KEYSPACE}/0 ${CELLS}-0000000100
Starting port forwarding to vtctld...
W0404 03:09:31.694664   23743 main.go:58] W0403 18:09:31.688983 reparent.go:181] master-elect tablet vitess-0000000100 is not the shard master, proceeding anyway as -force was used
W0404 03:09:31.696059   23743 main.go:58] W0403 18:09:31.689397 reparent.go:187] master-elect tablet vitess-0000000100 is not a master in the shard, proceeding anyway as -force was used
```

今回は初回なのでまだマスターが存在しない状態です。そのため、InitShardMaster -forceで一番目のTabletであるvitess-0000000100を強制的にマスターにしています。改めてTabletの状態を見ると、masterが出現しました。

```
$ ./kvtctl.sh ListAllTablets ${CELLS}
Starting port forwarding to vtctld...
vitess-0000000100 teru_keyspace 0 master 10.1.45.220:15002 10.1.45.220:3306 []
vitess-0000000101 teru_keyspace 0 replica 10.1.225.43:15002 10.1.225.43:3306 []
vitess-0000000102 teru_keyspace 0 rdonly 10.1.45.221:15002 10.1.45.221:3306 []
```

## テーブルの作成

それではテーブルを作成します。ソースツリー付属のテストテーブルを使います。

```
$ cat create_test_table.sql
CREATE TABLE messages (
  page BIGINT(20) UNSIGNED,
  time_created_ns BIGINT(20) UNSIGNED,
  message VARCHAR(10000),
  PRIMARY KEY (page, time_created_ns)
) ENGINE=InnoDB

$ ./kvtctl.sh ApplySchema -sql "$(cat create_test_table.sql)" ${KEYSPACE}
Starting port forwarding to vtctld...
```

次のコマンドで各tabletにテーブル定義がレプリケーションされていることがわかります。

```
$ ./kvtctl.sh GetSchema ${CELLS}-0000000100
$ ./kvtctl.sh GetSchema ${CELLS}-0000000101
$ ./kvtctl.sh GetSchema ${CELLS}-0000000102

(例)
Starting port forwarding to vtctld...
{
  "database_schema": "CREATE DATABASE /*!32312 IF NOT EXISTS*/ {{.DatabaseName}} /*!40100 DEFAULT CHARACTER SET utf8 */",
  "table_definitions": [
    {
      "name": "messages",
      "schema": "CREATE TABLE `messages` (\n  `page` bigint(20) unsigned NOT NULL,\n  `time_created_ns` bigint(20) unsigned NOT NULL,\n  `message` varchar(10000) DEFAULT NULL,\n  PRIMARY KEY (`page`,`time_created_ns`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8",
      "columns": [
        "page",
        "time_created_ns",
        "message"
      ],
      "primary_key_columns": [
        "page",
        "time_created_ns"
      ],
      "type": "BASE TABLE",
      "data_length": "16384",
      "row_count": "0"
    }
  ],
  "version": "5b2e5dbcb5766b6c69fe55c81b6ea805"
}
```

## データのバックアップ

ここで最初のバックアップをとります。バックアップはReadOnlyノードの1つを指定して取得します。なぜReadOnlyノードを指定するかというと、バックアップの間はレプリケーションが一時的に停止するので、静止点が取れるのだそうです。

```
$ ./kvtctl.sh Backup ${CELLS}-0000000102
Starting port forwarding to vtctld...
```

正しくとれたか確認してみます。

```
$ ./kvtctl.sh ListBackups ${KEYSPACE}/0
Starting port forwarding to vtctld...
2018-04-03.181103.vitess-0000000102
```

このバックアップがあると、今後ノードを足したときに自動的にこれを使ってリストアし、マスタからの差分をレプリケーションしてくれるのだそうです。便利ですね。

## Routing Schemaの初期化

説明を読んでもよくわかりませんでしたが、何かの初期化のようです。

```
$ ./kvtctl.sh RebuildVSchemaGraph
Starting port forwarding to vtctld...
```

## vtgateの起動

構成作業の最後ですが、クライアントから生きてるMySQLにルーティングするためのvtgateを起動します。デフォルトではレプリカが3つ起動しますが私の検証環境はリソース不足なので1個にします。

```
$ export VTGATE_REPLICAS=1
$ ./vtgate-up.sh
Creating vtgate service in cell vitess...
service "vtgate-vitess" created
Creating vtgate replicationcontroller in cell vitess...
replicationcontroller "vtgate-vitess" created

$ kubectl get pods -n teru | grep vt
vtctld-rcdtg                         1/1       Running   0          2h
vtgate-vitess-4cb6w                  1/1       Running   0          1m
vttablet-100                         2/2       Running   1          39m
vttablet-101                         2/2       Running   1          39m
vttablet-102                         2/2       Running   1          39m
```

以上でセットアップは完了です。今後、実際に動かしていろいろ検証したいと思います。