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構成が構築可能
- メンテナンスが容易
- 安定したレイテンシ
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 (推奨)
- Kubernetes TiDB Operator https://github.com/pingcap/tidb-operator
- Docker Compose (開発環境向け) https://github.com/pingcap/tidb-docker-compose
今回は開発環境・本番環境共に構築を行うことができる推奨ツール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_9
の range-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/
資料等
- アーキテクチャ・デプロイ方法などが紹介されています https://tikv.org/docs/dev/concepts/overview/
- TiKVの内部構造について紹介されています https://tikv.github.io/deep-dive-tikv/overview/introduction.html
- 内部アーキテクチャについてのブログ記事が多く掲載されています https://pingcap.medium.com/