背景
Bigtableはレプリケーションを有効にしたマルチクラスタ構成にすることで負荷分散、高可用性を保証することが可能です。
一方でマルチクラスタにすることで
といったデータ整合性での問題が発生します。
今回はその対策としてアプリプロファイルを用いた方法を紹介します。
アプリプロファイルとは
アプリプロファイルはリクエストのルーティングや単一行トランザクションの許可するかどうかを制御する設定です。
各インスタンスであらかじめ作成しておき、SDKなどでアプリプロファイルを選択して使うことが可能です。
マルチクラスタルーティング
マルチクラスタ環境ではデフォルトでは以下のように近いクラスタにルーティングされます。
自動的にフェイルオーバーできる一方で、最初に述べたように単一行トランザクションは禁止されますし、レプリケーション遅延によるデータの不整合が発生します。
ref: https://cloud.google.com/bigtable/docs/replication-overview#consistency-model
とあるように多くのユースケースでは問題にならない程度ではありますが、書き込み後すぐに読み取りが発生するケースでは考慮が必要です。
シングルクラスタルーティング
アプリプロファイルを設定すると、以下のように全てのリクエストを単一のクラスタにルーティングさせることが可能です。
こちらの場合はフェイルオーバーは手動になりますが、単一行トランザクションは利用可能で同じクラスタにアクセスするのでレプリケーション遅延を考慮しなくて済みます。
ルーティングの特徴を比較
ルーティングの比較をすると以下の通りになります。
特徴 | Single-cluster routing | Multi-cluster routing |
---|---|---|
リクエストのルーティング | 全て1つのクラスタへルーティング | 最も近い使用可能なクラスタにルーティング |
フェイルオーバー | ▲手動 | 自動 |
ユースーケース例 | CPU使用率の高い負荷を分離したい | 高可用性が求められる リクエストを全般的に負荷分散したい |
書き込み後読み取りの一貫性 | あり | ▲なし |
単一行トランザクション | あり | ▲なし |
ユースケースについては、例えば通常のアプリケーションサーバに加えて、バッチ処理でreadしかないが局所的にCPU負荷が上がるような要件があり、アプリケーション側に影響を与えないようクラスタを分離したいといったケースがあります。
方針
したがって
- 通常はマルチクラスタルーティングのアプリプロファイルを使って高可用性を保証する
- 単一行トランザクション・書き込み後読み取りの整合性保証をしたい時はシングルクラスタルーティングのアプリプロファイルを使う
という2つのアプリプロファイルを併用することで、高可用かつデータ整合性も保証することができそうです。
実際にコードで検証してみる
今回は
- 単一行トランザクションが有効か
- 書き込み後読み取りの一貫性の保証
の2つを検証します。
アプリプロファイルの設定
あらかじめレプリケーションを有効にしたマルチクラスタ構成のインスタンスを用意します。
GCPでアプリプロファイルの作成
次にアプリプロファイルを作成します。
書き込み用
書き込み用のアプリプロファイルを作成します。
読み取り用
defaultと同じ設定ですが、読み取り用のアプリプロファイルも作成しておきます。
最終的にアプリプロファイルはこのようになります。
コード
次にGoでクライアントコードを用意します。
bigtable.ClientConfigで先ほど作成したアプリプロファイルIDを指定します。
ctx := context.Background() writer, err := bigtable.NewClientWithConfig(ctx, projectID, instanceID, bigtable.ClientConfig{ AppProfile: "writer", }) if err != nil { panic(err) } defer writer.Close() reader, err := bigtable.NewClientWithConfig(ctx, projectID, instanceID, bigtable.ClientConfig{ AppProfile: "reader", }) if err != nil { panic(err) } defer reader.Close() client := &Client{ writer: writer.Open(tableName), reader: reader.Open(tableName), }
単一行トランザクションが有効か
writer
とreader
のそれぞれで条件付き書き込みが利用可能か確認します。
以下のようなコードを用意し、
mut := bigtable.NewMutation() mut.Set(columnFamilyName, columnQualifier, bigtable.Timestamp(0), []byte(value)) filter := bigtable.ChainFilters( bigtable.FamilyFilter(columnFamilyName), ) conditionalMutation := bigtable.NewCondMutation(filter, nil, mut) if err := c.writer.Apply(context.Background(), rowKey, conditionalMutation); err != nil { return err } return nil
Apply()
を呼び出す際にc.writer
とc.reader
それぞれで実行してみます。
動作検証
writer
問題なく実行できます。利用可能であることが分かります。
$ go run main.go $
reader
以下のエラーが発生しました。アプリプロファイルの通り禁止されているようです。
$ go run main.go panic: rpc error: code = FailedPrecondition desc = Single-row transactions are not allowed by this app profile
書き込み後読み取りの一貫性
writer
とreader
のそれぞれで書き込み後読み取ったデータが一致するか確認します。
func writeAndRead(client *Client) (bool, error) { rowKey := "key1" now := time.Now().UnixNano() in := strconv.FormatInt(now, 10) err := client.write(rowKey, columnFamilyName, in) if err != nil { return false, err } out, err := client.read(rowKey, columnFamilyName, false) if err != nil { return false, err } var match bool if in == out { match = true } return match, nil }
再現性確認のため100回程度ループさせてみます。
var matched, unmatched int for i := 0; i < 100; i++ { match, err := writeAndRead(client) if err != nil { panic(err) } if match { matched++ } else { unmatched++ } } fmt.Printf("matched: %d, unmatched: %d\n", matched, unmatched)
動作検証
writerで読み込みも
全試行で一致することが確認できます。一貫性を保証できていると言えるでしょう。
$ go run main.go matched: 100, unmatched: 0
readerで読み込み
半分程度しか一致しないことが分かりました。一貫性がありません。
$ go run main.go matched: 54, unmatched: 46
その他
手動フェイルオーバー方法
writer
のようにシングルクラスタルーティングなアプリプロファイルを利用している場合、そのクラスタが落ちた際に手動でフェイルオーバーさせる必要があります。
ルーティング先を生きているクラスタに変更する方法としては
- Webコンソールで編集する
- gcloudコマンドで変更する
- Terraformで変更する
があります。
gcloudコマンド
gcloudコマンドは以下です。
$ gcloud bigtable app-profiles update APP_PROFILE_ID \ --instance=INSTANCE_ID \ --route-to=CLUSTER_ID
Terraform
Terraformの場合はcluster_id
とignore_warnings
を修正します。
resource "google_bigtable_app_profile" "writer" { instance = "my-instance" app_profile_id = "writer" single_cluster_routing { cluster_id = "cluster01" // here allow_transactional_writes = true } ignore_warnings = true // here }
サンプルコード
今回のサンプルコードはこちら
まとめ
アプリプロファイルを利用することで、マルチクラスタ構成においても
- 単一行トランザクションの利用が可能
- 書き込み後読み取りのデータ一貫性が保証される
といったことが実現できることが分かりました。
基本的な読み取りは負荷分散・高可用性の観点からマルチクラスタルーティングで十分ですが、上記のような要件が発生した場合はシングルクラスタルーティングも併用するのが良いでしょう。