Kubernetes を使ってスケールする MySQL クラスタ Vitess を試す


このエントリは [Kubernetes Advent Calendar](http://qiita.com/advent-calendar/2015/kubernetes) の 2 日目の記事です。公開が遅れてすみません。

## Kubernetes

最近の Kubernetes の動向は [1 日目の記事](http://netmark.jp/2015/12/kubernetes.html) で語られた通りです。

ついに v1.0.0 General Availability を迎え、
様々な場所で Kubernetes の話を聞くようになりました。

さて、本稿では一段階進んで、
Kubernetes が大体どんなものかは分かった、
でも実際どんな風に使われてるの？という疑念に対して、
Google の傘下である Youtube によって公開されている、
スケールする MySQL クラスタ [Vitess](http://vitess.io/) を Kubernetes で試してみる事とします。

## Vitess

[Vitess](http://vitess.io) は Youtube が MySQL をスケールさせるために開発した Go 製の OSS です。

公式サイトの画像ですが、次のようなアーキテクチャとなっています。

![](https://raw.githubusercontent.com/youtube/vitess/master/doc/VitessOverview.png)

それぞれのコンポーネントは以下の様な役割を担っています。

- vtctl
    - CLI から Vitess クラスタを操作するためのツール
- vtctld
    - GUI から Vitess クラスタを操作するための HTTP サーバ
- vtgate
    - アプリケーションからのクエリをルーティングする Proxy サーバ
        - vttablet からの結果をまとめてアプリケーションに返す
- vttablet
    - MySQL のスループットを最適化する Proxy サーバ
        - コネクションプーリング
        - クエリ書き換え・重複除去 
- Topology
    - 稼働しているサーバに関する情報を保存する
        - シャーディング・スキーマ
        - レプリケーショングラフ
    - Kubernetes では etcd を使用
    - etcd 以外には ZooKeeper をサポート

### Vitess を GKE 上で動かしてみる

さて、Vitess を Kubernetes を使って実際に動かしてみます。

ここでは、Kubernetes クラスタの構築に関しては本質では無いので、
現在最も簡単に Kubernetes クラスタを準備できる Google Container Engine (GKE) を使用します。

公式ドキュメントの [Running Vitess on Kubernetes](http://vitess.io/getting-started/) に従って作業を進めます。

まず vtctl をインストールします。

```bash
$ go get github.com/youtube/vitess/go/cmd/vtctlclient
```

公式だと上記の手順になっていますが、現在はもしかしたら protobuf が必要かもしれません。
`go get` 時にエラーが出た場合は次のコマンドを実行してから再度実行してみます。

```bash
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
```

#### GKE 環境の準備

GKE の環境を用意します。

gcloud コマンドは既に使用できる前提としておきます。
使用する方法に関しては [gcloud Tool Guide](https://cloud.google.com/sdk/gcloud) 等を参考にすると良いでしょう。
今なら日本語の情報も多く見かけます。

Vitess クラスタを稼働させる対象の project をセットします。

```bash
$ gcloud config set project PROJECT
```

GKE クラスタを稼働させるための `kubectl` をアップデートしておきます。

```bash
$ gcloud components update kubectl
```

GKE クラスタを稼働させるリージョンを選択します。
日本から最も近いのは asia-east1 なのでそれにしておきます。

```bash
$ gcloud config set compute/zone asia-east1
```

実際に Kubernetes クラスタを起動します。
また、後述のバックアップ用 bucket に書き込むために、 `storage-rw` のスコープを付与します。

```bash
$ gcloud container clusters create example --machine-type n1-standard-4 --num-nodes 5 --scopes storage-rw
```

また、Vitess のバックアップを保存するための、
Google Cloud Storage (GCS) の bucket を作成しておきます。
Amazon の S3 等と同様に、bucket 名はユニークである必要があるので、
必要に応じて書き換えてください。

```bash
$ gsutil mb gs://my-backup-bucket
```

#### Vitess クラスタの初期設定

起動した Kubernetes クラスタを vitess に認識させるために初期設定を行います。

```bash
$ cd $GOPATH/src/github.com/youtube/vitess/examples/kubernetes
```

```bash
$ ./configure.sh
```

ここで BSD の `sed` だと `-r` オプションが無いとエラーが出るかもしれませんが、
スクリプトの中身を確認したところ、
`gcloud` コマンドからプロジェクトを自動決定するのに失敗するだけなので、
大きな問題は無さそうです。

次のように GCP の Project と先ほど作成した bucket の名前を入力します。

```
Backup Storage (file, gcs) [gcs]: gcs
Google Developers Console Project []: my-project-id
Google Cloud Storage bucket for Vitess backups: my-backup-bucket
```

なお、backup storage に file を指定した場合は、
読み書き可能なネットワークディスクを、
 `vttablet` 並びに `vtctld` の pods に mount しておく必要があります。

また、GCS 以外の blob storage store へのバックアップは、
 [backupstorage plugin](https://github.com/youtube/vitess/blob/master/go/vt/mysqlctl/backupstorage/interface.go) を使用して実装することが出来るみたいです。

#### Topology の起動

次に、Topology を担う etcd のクラスタを起動します。

```bash
$ ./etcd-up.sh
```

実際に起動したかどうかは次のコマンドで確認します。

```bash
$ kubectl get pods
NAME                READY     STATUS    RESTARTS   AGE
etcd-global-ab123   1/1       Running   0          7m
etcd-global-cd456   1/1       Running   0          7m
etcd-global-ef789   1/1       Running   0          7m
etcd-test-a1111     1/1       Running   0          7m
etcd-test-b2222     1/1       Running   0          7m
etcd-test-c3333     1/1       Running   0          7m
```

### Vtctld の起動

次に vtctld を起動します。

```bash
$ ./vtctld-up.sh
```

同様に確認します。

```bash
$ kubectl get pods
NAME                READY     STATUS    RESTARTS   AGE
etcd-global-ab123   1/1       Running   0          7m
etcd-global-cd456   1/1       Running   0          7m
etcd-global-ef789   1/1       Running   0          7m
etcd-test-a1111     1/1       Running   0          7m
etcd-test-b2222     1/1       Running   0          7m
etcd-test-c3333     1/1       Running   0          7m
vtctld-abc12        1/1       Running   0          6m
```

vtctld が正しく起動しているか確認するために kube-proxy を起動します。

```bash
$ kubectl proxy --port=8001
```

その後、次の URL にアクセスすると次のような画面が表示されます。

- http://localhost:8001/api/v1/proxy/namespaces/default/services/vtctld:web/ 

![vtctld.png](https://qiita-image-store.s3.amazonaws.com/0/48761/55150690-c4ef-0d30-d71f-e46e9c4bb1ec.png "vtctld.png")

#### vttablet 並びに MySQL クラスタの起動

いよいよ vttablet 並びに MySQL のクラスタを起動します。

```bash
$ ./vttablet-up.sh
```

このコマンドで 5 つの vttablet の pods が起動します。
先ほどと同様 `kubectl get pods` で確認できます。

また、`vtctl` のラッパーである CLI ツールの `kvtctl.sh` でも確認できます。

```bash
$ ./kvtctl.sh ListAllTablets test
```

古い Mac だと `kvtctl.sh` が呼んでいる、
`env.sh` の中にある `mktemp` でエラーが発生するかもしれません。
その場合 27 行目付近を次のように書き換える必要があります。

```
     27 -  tmpfile=`mktemp`
     28 +  tmpfile=`mktemp -t tmp`
```


また、新しい `kubectl` を使っている場合は、
deprecated warning が出るかもしれませんが、とりあえず気にする必要はありません。
どうしても気になる場合は `kubectl` の引数 `-t` を、
全て `--template` に置き換えると上手く行きそうです。

#### MySQL クラスタのセットアップ

まず、新しいシャードを作成するために keyspace を作成します。

```bash
$ ./kvtctl.sh RebuildKeyspaceGraph test_keyspace
```

次にシャードを作成します。
最初のマスターになるノードを指定します。
他のノードは指定したマスターノードに接続しレプリケーションを開始します。

```bash
$ ./kvtctl.sh InitShardMaster -force test_keyspace/0 test-0000000100
```

また、この時に併せてデフォルトのデータベースも作成します。
最初に作成した keyspace が `test_keyspace` なので、
MySQL には `vt_test_keyspace` というデータベースが作成されます。

再度 `ListAllTablets` を実行すると、
指定したノードが Master になり、
幾つかのノードが replica もしくは rdonly になっている事が確認できます。

```bash
$ ./kvtctl.sh ListAllTablets test
```

rdonly のノードはバッチ処理やバックアップ処理に使用する事を想定しているようです。
また、replica に関しては、
Web トラフィックの(恐らく参照クエリ)を処理する事を想定しているようです。
中身に関して全く見てないのですがレプリケーションの種類が違ったりするんでしょうか。
気になります。

#### テーブルの作成

作成した MySQL クラスタにテーブルを作成してみます。
`ApplySchema` コマンドに `-sql` フラグと SQL 自体を渡し、keyspace を指定します。

```bash
$ ./kvtctl.sh ApplySchema -sql "$(cat create_test_table.sql)" test_keyspace
```

`create_test_table.sql` の中身は普通の `CREATE TABLE` 句になっています。

```SQL
CREATE TABLE messages (
  page BIGINT(20) UNSIGNED,
  time_created_ns BIGINT(20) UNSIGNED,
  keyspace_id BIGINT(20) UNSIGNED,
  message VARCHAR(10000),
  PRIMARY KEY (page, time_created_ns)
) ENGINE=InnoDB
```

作成したスキーマは `GetSchema` で確認することが出来ます。

```bash
$ ./kvtctl.sh GetSchema test-0000000100
```

最後に、アプリケーションから利用するために `vtgate` を起動します。

```bash
$ ./vtgate-up.sh
```

また、次のようにしてバックアップを取得することも出来ます。
この時、恐らく rdonly の pod を指定するのが良いみたいです。

```bash
$ ./kvtctl.sh Backup test-0000000104
```

取得したバックアップは次のようにして確認することが出来ます。

```bash
$ ./kvtctl.sh ListBackups test_keyspace/0
```

#### アプリケーションからの利用 (guestbook)

Vitess を利用したアプリケーションの例として、
Python で書かれた guestbook が用意されています。
次のようにして起動します。

```bash
$ ./guestbook-up.sh
```

GCP の Firewall やロードバランサ等を設定する手順もありますが割愛して、
ここでは実際のアプリケーションがどのようにして Vitess に接続するのか見てみます。
アプリケーションは `guestbook/` 配下に設置されています。
中身は普通の Flask アプリケーションのようです。

次のようなモジュールをインポートしています。

```
from vtdb import keyrange
from vtdb import keyrange_constants
from vtdb import vtgatev2
from vtdb import vtgate_cursor
```

アプリケーションはまず `vtgate` に接続します。

```python
  # Connect to vtgate.
  conn = vtgatev2.connect({'vt': [addr]}, timeout)
```

次に、keyspace と接続する vttablet の種類を指定します。
その後、普通の [PEP249](https://www.python.org/dev/peps/pep-0249) のような形式で SQL を実行出来ます。

```python
  # Insert a row on the master.
  keyspace_id = get_keyspace_id(page)
  keyspace_id_int = unpack_keyspace_id(keyspace_id)
  cursor = conn.cursor('test_keyspace', 'master', keyspace_ids=[keyspace_id], writable=True)

  cursor.begin()
  cursor.execute(
      'INSERT INTO messages (page, time_created_ns, keyspace_id, message)'
      ' VALUES (%(page)s, %(time_created_ns)s, %(keyspace_id)s, %(message)s)',
      {
        'page': page,
        'time_created_ns': int(time.time() * 1e9),
        'keyspace_id': keyspace_id_int,
        'message': value,
      })
  cursor.commit()
```

参照の際は、 `replica` を指定して参照クエリを発行しています。

```python
  cursor = conn.cursor('test_keyspace', 'replica', keyspace_ids=[keyspace_id])

  cursor.execute(
      'SELECT message FROM messages WHERE page=%(page)s ORDER BY time_created_ns',
      {'page': page})
  entries = [row[0] for row in cursor.fetchall()]
```

肝心の keyspace の指定ですが、次のようになっていました。

```python
def get_keyspace_id(page):
  """Compute the keyspace_id for a given page number.

  In this example, the keyspace_id is the first 64 bits of the MD5 hash of
  the sharding key (page number). As a result, pages are randomly distributed
  among the range-based shards.

  The keyspace_id is returned as a packed string. Use unpack_keyspace_id() to
  get the integer value if needed.

  For more about keyspace_id, see these references:
  - http://vitess.io/overview/concepts.html#keyspace-id
  - http://vitess.io/user-guide/sharding.html
  """
  m = hashlib.md5()
  m.update(uint64.pack(page))
  return m.digest()[:8]
```

今回の場合は page の値をベースに keyspace を決定している模様です。
MD5 hash の最初の 64bit を使用しています。
Vitess を使ってパフォーマンスやスケールを意識する場合は、
この keyspace_id の設計が重要になると考えられます。

この Guestbook の例は Python ですが、
Vitess には次の言語のクライアントライブラリが存在しているようです。

- Go
- Python
- PHP
- Java

[Vitess Client Libraries](http://vitess.io/user-guide/client-libraries.html)

## まとめ

Kubernetes の使われている例として、スケールする MySQL クラスタの Vitess を紹介しました。

このようにトポロジを持ち、クラスタに対してプロキシを行ったり、
特定のコンテナをセットで管理するアプリケーションの配置先として、
Kubernetes は非常に適しているのだろうと考えられます。

また、Vitess には本稿で紹介した以外にも多数の機能があり、
大規模の MySQL クラスタを管理するにあたって必要な機能を多く持っているように見えます。
特に GKE を使えば Kubernetes を管理するのも非常に簡単なので、
GCP 上で大規模な MySQL を使用したい場合には、
CloudSQL を使うより魅力的な選択肢かもしれません。

Google 社の事だから Vitess もそのうち CloudSQL の代替もしくは改善案として、
GCP のマネージドサービスとして出てくるかもしれませんね。

## 参考文献

- [Vitess](http://vitess.io)
- [Running Vitess on Kubernetes](http://vitess.io/getting-started/)
- [Vitess Client Libraries](http://vitess.io/user-guide/client-libraries.html)
- [Scaling MySQL in the cloud with Vitess and Kubernetes](http://googlecloudplatform.blogspot.jp/2015/03/scaling-MySQL-in-the-cloud-with-Vitess-and-Kubernetes.html)
- [Cloud Native MySQL Sharding with Vitess and Kubernetes](http://googlecloudplatform.blogspot.jp/2015/10/Cloud-Native-MySQL-Sharding-with-Vitess-and-Kubernetes.html)