NewSQL TiDBを支える分散KVS "TiKV"入門

TiDB は MySQL 互換のスケーラブルな NewSQL です。 2015 年に創立された PingCAP 社により開発されており、2021 年には日本法人も立ち上げられました。 2022 年 7 月に開催された TiDB User Day 2022 では、スマレジ・@cosme・カプコン等の事例も紹介されており、日本国内での利用も進んでいるようです。

ノードを増やすことでのスケーリング・HA 構成の構築も非常に容易です。 さらに、MySQL 互換なので既存のアプリケーションの改修は最低限に抑えられ、エコシステムも最大限活用することができます。 マネージドサービス TiDB Cloud の提供も開始され、導入は手軽になっています。 こういった魅力が TiDB が広く使われる理由かと感じています。

この TiDB は、主に PingCAP 社によって開発されている TiKV によって支えられています。 TiKV はトランザクション可能な KVS ストレージで、TiDB はステートレスなクエリエンジン等という構成です。 TiKV は単体で Redis,memcached のように手軽に利用が可能ですが、非常に高機能で使いやすいです。

今回は TiDB を支えている TiKV を単体で構築・利用し、TiDB を支える技術要素に対しての理解を深めましょう。


この記事は、TiDB,TiKV 開発元の PingCAP が提供する TiDB User Day 2022 インフルエンサープログラムの支援をいただいたブログ記事です。 https://pingcap.co.jp/tidb-user-day-2022-thank-you/

TiKV 概要

TiKV は Rust で構築された分散 KVS で、永続化には RocksDB を利用しています。 ざっくりとした特徴は以下のようになっています。

  • トランザクション対応
  • HBase の設計に近いレンジパーティショニング
  • 構築やクライアントライブラリの利用が容易
  • HA 構成が構築可能
  • メンテナンスが容易
  • 安定したレイテンシ

https://tikv.org/

CNCF プロジェクトへ登録されており、成熟度は Graduated とされています。 https://www.cncf.io/projects/tikv/

なお、本記事では以下の製品バージョンを利用しています。

  • tiup: v1.10.3
  • TiKV: v6.2.0

TiKV クラスタ

この記事では、TiUP を利用したローカル環境でのセットアップを行います。 ざっくりと TiKV のクラスタの性質を紹介します。

TiKV は複数ノード用意することで、可用性の向上・パフォーマンスの向上が可能です。

TiKV を利用するためには、TiKV の他に PD(Placement Driver)コンポーネントを動作させる必要があります。 PD は主にスケジューリングとトランザクションの管理を行っています。 PD は etcd を組み込んでおり、こちらも複数ノード用意することで可用性を上げることが可能です。

データは一定の容量ごとにリージョンという単位で分割され、各ノードのストアに格納されます。 データをどの TiKV ノードに保存するかは PD が管理しており、常に指定されたレプリケーション係数を維持するようにレプリカ数を調整したり、クライアントへデータ・ノードの対応付けに必要な情報を伝えます。 クライアントはデータの取得を行う際に、PD の情報を元に格納ノードを特定して、該当の TiKV へ直接通信を行います。 この性質から、TiKV クライアントからはすべての TiKV ノードに加えて PD ノードへの通信を行えるように設定する必要があります。

TiKV, PD は主な x86,ARM CPU を搭載する Linux サーバであれば動作可能ですが、 最適なパフォーマンスを得るためのマシンスペックの目安が公式ドキュメントで案内されています。

https://tikv.org/docs/6.1/deploy/install/prerequisites/

TiKV クラスタ 構築方法

公式で提供されている PD/TiKV/TiDB クラスタの展開方法は以下のとおりです。

今回は開発環境・本番環境共に構築を行うことができる推奨ツール TiUP で構築を行います。

本番環境の構築を行う際には、最初に Terraform 等のツールで Linux インスタンスを構築します。 ホストリスト等の情報を TiUP へ渡すことで、PD/TiKV 等の必要なコンポーネントの展開が自動的に行われます。 普段 Ansible 等で行っているオペレーション等を、巻き取ってくれるものとして認識して良いと思います。

また、公式で推奨されてはいませんが、いずれのコンポーネントも GitHub 等でコンパイル済み実行ファイルの配布が行われています。 依存関係も少ないので、手動でバイナリダウンロード・配置を行うことでデプロイを行うことも可能です。 クラスタの理解を深めるために、こういった手動でのデプロイも経験すると良いように感じます。

TiUP を利用した開発環境構築

TiUP は本番環境の構築に推奨されたデプロイツールですが、ローカルマシンの開発環境の構築も非常に簡単に行うことができます。 主な手順がこちらのドキュメントで紹介されています。

https://tikv.org/docs/6.1/concepts/tikv-in-5-minutes/

まず TiUP コマンドのインストールを行います。 シェルを実行することで自動的にインストールされます。 公式ドキュメントではインストール後に source ~/.bash_profile を行うよう指示がありますが、TiUP 上では source ~/.bashrc を行うように指示がありましたので、自分は ~/.bashrc の読み込みを行いました。

$ curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
$ . ~/.bashrc
$ tiup --version

次に下のコマンドで開発環境の TiKV の構築を行います。 TiUP の playground サブコマンドは、ローカル環境でのテスト環境立ち上げを行うためのものです。

$ tiup playground --mode tikv-slim

起動が完了すると、コンソール上に以下のようなエンドポイント情報が表示されます。 PD の情報は、TiKV クライアントへ接続情報として渡す必要があります。 TiKV と PD に加えて、TiKV,PD を監視するための Prometheus とダッシュボードを閲覧するための Grafana も立ち上がります。

PD client endpoints: [127.0.0.1:2379]
To view the Prometheus: http://127.0.0.1:9090
To view the Grafana: http://127.0.0.1:3000

また、TiUP playground に渡す引数を変更することにより、異なる構成でのクラスタ構築も可能です。 以下の例では PD,TiKV それぞれ 3 台の構成としています。

$ tiup playground --mode tikv-slim --pd 3 --kv 3

PD client endpoints: [127.0.0.1:2379 127.0.0.1:2382 127.0.0.1:2384]
To view the Prometheus: http://127.0.0.1:9090
To view the Grafana: http://127.0.0.1:3000

ちなみに、PD のポート番号を変更するためのオプションは、次期バージョンの TiUP にて実装される予定のようです。

https://github.com/pingcap/tiup/pull/1931

Grafana の URL にアクセスして、ユーザ名・パスワードを admin admin としてログインすると、以下のように詳細なメトリクスを閲覧することができます。 パフォーマンステストやトラブルシューティングの際に役に立つダッシュボードが公式で提供されています。 非常に網羅性が高く使い勝手も良いので、実務においてもこれだけあれば問題になることは無いように感じます。

TiKV, PD の管理

それぞれ TiKV は tikv-ctl 、PD は pd-ctl コマンドで管理を行うことができます。 主にクラスタやスケジューラに対しての操作・設定・参照は pd-ctl で行い、TiKV ノードの様々なデバッグ情報の参照・操作は tikv-ctl で行います。 CLI は非常に機能が豊富で紹介しきれませんが、TiKV,PD は安定性が高く、個人開発等の小さな規模であれば日常的に触ることは無いように感じます。

今回は TiUP 経由での pd-ctl を実行し、今回構築したクラスタ情報を表示します。

$ tiup ctl:v6.2.0 pd -u http://127.0.0.1:2379 cluster
{
  "id": 7139831296082586572,
  "max_peer_count": 3
}

詳細は以下のリファレンスを参照してください。

https://docs.pingcap.com/tidb/v6.2/pd-control https://docs.pingcap.com/tidb/v6.2/tikv-control

TiKV のクライアントライブラリ

TiKV を利用するためのクライアントライブラリは、特に tikv/client-go tikv/client-java tikv/client-rust が活発にメンテナンスされているようです。

各クライアントのリポジトリにサンプルコードも含まれています。 今回は Go で TiKV クラスタに接続して値の読み書きを行ってみましょう。

TiKV が提供する API は大まかに 2 種類存在しています。

  • RawKV
    • 非トランザクション API
    • CAS(CompareAndSwap)操作向けの API が存在しており、データ競合を防ぐ事ができる
  • TxnKV
    • トランザクションを利用する API
    • RawKV に比べると、負荷やレイテンシが高くなることが多い

なお現在の TiKV バージョンにおいては、1 つのクラスタへのアクセス方法は RawKV,TxnKV どちらか片方を利用する必要があります。 将来的に RawKV,TxnKV が読み書きできる領域を内部的に分割することで、両方のアクセスをサポートすることも検討されているようです。 https://github.com/tikv/tikv/issues/3922

Go から TiKV クラスタへ接続

今回利用するサンプルコードはこちらに用意しています。 https://github.com/kamijin-fanta/tikv-sandbox

まずは RawKV を利用してみます。 最初に以下のような形で RawKV 向けのクライアントを生成します。 指定するアドレスは、TiKV ではなく PD のアドレスです。 クライアント内では、PD からノード情報・データの配置情報を取得して、データアクセスを行っています。

cli, err := rawkv.NewClient(
  ctx,
  []string{"127.0.0.1:2379"},
  config.DefaultConfig().Security,
)

次に、キーに対してのいくつかの操作方法を紹介します。

// クライアントで扱うKey,Valueは共に[]byte型
key := []byte("key-example")
val := []byte("bytes-value")

// キーに値をセット
err = cli.Put(ctx, key, val)

// キーの値を取得
val, err = cli.Get(ctx, key)

// キーの値を削除
err = cli.Delete(ctx, key)

サンプルコードのリポジトリ内で go run ./cmd/rawkv-basic/. とすることで、一連の操作を試すことができます。 https://github.com/kamijin-fanta/tikv-sandbox/blob/master/cmd/rawkv-basic/rawkv-basic.go

スキャン

TiKV は、キーを辞書順にソートした大きな Sorted Map のような仕組みになっています。 こちらは、 BigTable, HBase 等に近いデザインです。 キー範囲は自動的に一定の大きさで分割されますが、クライアントは PD からの情報を元に透過的にアクセスを行います。

こういった性質を持っていることから、TiKV では一定のキー範囲に対してのスキャンを行うことが可能です。 リスト構造や時系列データのように連続したアクセスを行うことが多いデータを扱う際に非常に便利です。

Go のクライアントライブラリで試してみましょう。

// 事前に range-test_0 ~ range-test_9 をPUTしておく
for i := 0; i < 10; i++ {
  key := fmt.Sprintf("range-test_%d", i)
  value := fmt.Sprintf("value-%d", i)
  _ = cli.Put(ctx, []byte(key), []byte(value))
}

// スキャン対象のキー下限・上限
getLowerKey := []byte("range-test_2")
getUpperKey := []byte("range-test_7")

// 10件を上限にスキャンを行う
// 戻り値は、key,value共に[][]byte型
keysRes, valuesRes, err := cli.Scan(ctx, getLowerKey, getUpperKey, 10)

// キーとその内容を表示
for i, key := range keysRes {
  log.Printf("'%s'->'%s'", key, valuesRes[i])
}

こちらの出力結果は以下のようになります。

# Scan出力結果

'range-test_2'->'value-2'
'range-test_3'->'value-3'
'range-test_4'->'value-4'
'range-test_5'->'value-5'
'range-test_6'->'value-6'

下限に指定した range-test_2 は結果に含まれており、上限に指定した range-test_7 は含まれません。 TiKV ではスキャンを行う際に、下限キー自体を含み(Inclusive)、上限キー自体は含まない(Exclusive)という仕様になっています。

また、 cli.Scan() は昇順のスキャンを行っていましたが、降順のスキャンを行う cli.ReverseScan() もサポートされています。

// スキャン対象のキー下限・上限
getLowerKey := []byte("range-test_2")
getUpperKey := []byte("range-test_7")

// 10件を上限に降順のスキャンを行う
// 戻り値は、key,value共に[][]byte型
keysRes, valuesRes, err = cli.ReverseScan(ctx, getUpperKey, getLowerKey, 10)

// キーとその内容を表示
for i, key := range keysRes {
  log.Printf("'%s'->'%s'", key, valuesRes[i])
}

結果はこちらのようになります。 Scan の場合と同様に、下限キーは Inclusive、上限キーは Exclusive の関係となっています。

# ReverseScan出力結果

'range-test_6'->'value-6'
'range-test_5'->'value-5'
'range-test_4'->'value-4'
'range-test_3'->'value-3'
'range-test_2'->'value-2'

こちらも、サンプルコードのリポジトリ内で go run ./cmd/rawkv-range/. とすることで、一連の操作を試すことができます。 https://github.com/kamijin-fanta/tikv-sandbox/blob/master/cmd/rawkv-range/rawkv-range.go

スキャンの Inclusive, Exclusive 指定

キーの最後に 0x00 を付与することで、それぞれ下限キー自体を含まず(Exclusive)、上限キー自体を含む(Inclusive)アクセスを行うことも可能です。

getLowerKey := []byte("range-test_2")
getUpperKey := []byte("range-test_7")

// キーの最後に0x00を付与
getLowerKey = append(getLowerKey, 0)
getUpperKey = append(getUpperKey, 0)

keysRes, valuesRes, err := cli.Scan(ctx, getLowerKey, getUpperKey, 10)

// 以下、出力処理は省略
# Lower=Exclusive, Upper=Inclusive 出力結果

'range-test_3'->'value-3'
'range-test_4'->'value-4'
'range-test_5'->'value-5'
'range-test_6'->'value-6'
'range-test_7'->'value-7'

範囲の削除

先程用意したテストデータの削除を行います。 TiKV ではキー範囲を指定することで、個別にキーを指定するよりも負荷を掛けずにキー削除を行うことができます。

今回は range-test_0 から range-test_9range-test_ プレフィックスを持つ値を削除したいので、 range-test_ から range-test_\xFF を削除対象としました。

delStartKey := []byte("range-test_")
delStopKey := []byte("range-test_\xFF")
err = cli.DeleteRange(ctx, delStartKey, delStopKey)

トランザクションの利用

今まで RawKV を利用して TiKV へアクセスしていましたが、次は TxnAPI を利用して TiKV へアクセスを行ってみます。 クライアントの作成方法や、API が一部異なりますが、基本的なアクセス方法に変わりは有りません。

まずは、トランザクションを開始した上でキーに値をセットします。

// PDを指定してクライアントの作成
client, err := txnkv.NewClient(addresses)
defer client.Close()

// トランザクションを開始
tx, err := client.Begin()

// キーのセット
err = tx.Set([]byte("key-1"), []byte("value-1"))
err = tx.Set([]byte("key-2"), []byte("value-2"))

// コミット
err = tx.Commit(ctx)

次に、今回セットした値を読み出してみましょう。

// トランザクションを開始
tx, err = client.Begin()

// キーの取得
v, err := tx.Get(ctx, []byte("key-1"))
log.Printf("Get key from TiKV '%s'", v)

他にもトランザクションの性質等の関係上、多少 API デザインが異なりますが、削除・スキャン等も同様に行うことができます。

こちらも、サンプルコードのリポジトリ内で go run ./cmd/txnkv-basic/. とすることで、一連の操作を試すことができます。 https://github.com/kamijin-fanta/tikv-sandbox/blob/master/cmd/txnkv-basic/txnkv-basic.go

さいごに

TiDB を支えているストレージシステム TiKV を駆け足で紹介させていただきました。

TiKV は単体でも利用することが可能なスケーラブルな分散 KVS で、最近では TiDB 以外にも juicedata/juicefs 等のコミュニティで開発される OSS でも利用可能になっています。 今回、構築も簡単でクライアントライブラリの利用も非常に手軽があることが分かっていただけたかと思われます。 ミドルウェアやアプリケーションへの組み込みも行いやすいと思いますので、今後さらに TiKV の利用が進むことを楽しみにしています。

また、こういった素晴らしいストレージシステムの上に構築されている TiDB も、非常に使いやすく魅力的な NewSQL です。 TiUP では TiDB の構築も可能ですし、 フルマネージド TiDB サービスの TiDB Cloud もトライアル頂くと魅力が伝わるのでは無いかと思います。

自分と TiKV の関わり

2018 年頃に業務で開発していたプロダクトに導入できないかということで、TiKV へ PR の提出等を行いました。 今回紹介したスキャンは当時昇順のスキャンのみが行える状況でした。 アクセスの性質上 ReverseScan をサポートする必要があり、HBase 等でも利用可能な API ということもあったので実装を行いました。

https://github.com/tikv/tikv/pull/3724 https://github.com/tikv/client-go/pull/13

その後 TiKV を利用した製品は、さくらインターネットの”sakura.io”サービスの”データストア(V2)“としてサービスリリースを行っています。これをきっかけに社内で TiDB を利用したサービスもいくつかリリースされています。 参考記事: https://knowledge.sakura.ad.jp/29695/

また、個人的に TiKV が好きで同人誌も書きました。 こちらは 2019 年に開催された技術書典 7 にて頒布を行いました。

https://kinyoubenkyokai.github.io/book/techbook07/

資料等