はじめに
一時テーブルを作成し、その 1 秒後にそのテーブルを読み取るクエリを実行したところ、テーブルが存在しないというエラーで失敗する、そんな状況を想像してみてください。
これは通常の意味でのバグではありません。レプリカが複数あるサービスでは、ClickHouse の一時テーブルと名前付きセッションは、作成されたレプリカ上にのみ存在します。後続のクエリが負荷分散で別のレプリカに振り分けられると、その一時テーブルは作成されていなかったかのように見えることがあります。
そこで私たちは Replica-aware routing を構築しました。一時テーブルやセッションに常にアクセスできるようにするためです。Replica-aware routing がやっていることは単純で、リクエストを同じレプリカに送るだけです。一時セッションやテーブルへのアクセスに加えて、read-after-write 整合性といった使い方にも対応できます。
本日は、その使い方、私たちがどう構築したか、そしてもちろん、どのような場面で使うべきかを紹介します。Replica-aware routing は現在、Enterprise のユーザー向けにパブリックベータとして提供されており、順次、みなさまの組織でも利用できるようになります。
仕組み
read-after-write 整合性のユースケースで考えてみましょう。HTTP を使っている場合は、任意のルーティングキーを載せたヘッダーを付けてクエリを送るだけです。ネイティブ接続の場合は、SNI の値を上書きするだけです。
### Replica-aware routing over HTTP
# Write, tagged with a routing key
echo "INSERT INTO events VALUES (now(), 'signup')" | curl \
-H 'X-ClickHouse-User: default' \
-H 'X-ClickHouse-Key: <password>' \
-H 'X-ClickHouse-Replica-Tag: amy_test' \
'https://<host>:8443/' -d @-
# Read it back on the same replica, using the same tag
echo 'SELECT count() FROM events' | curl \
-H 'X-ClickHouse-User: default' \
-H 'X-ClickHouse-Key: <password>' \
-H 'X-ClickHouse-Replica-Tag: amy_test' \
'https://<host>:8443/' -d @-
### Replica-aware routing over native
# Write with routing key
clickhouse client --user default \
--password <password> \
--host <host> \
--query "INSERT INTO events VALUES (now(), 'signup')" \
--secure \
--tls-sni-override jan_key.sticky.<host>
# Read with routing key
clickhouse client --user default \
--password <password> \
--host <host> \
--query 'SELECT count() FROM events' \
--secure \
--tls-sni-override jan_key.sticky.<host>これだけです 🙂 リクエストが同じ X-ClickHouse-Replica-Tag または SNI のオーバーライドを持つので、それらは同じレプリカに届きます。そのため、他のレプリカがまだレプリケーションに追いついていなくても、読み取りで書き込みの結果が見えます。別の値を使えば、その値は独立にハッシュされ、別のレプリカに届く可能性があります。
どのような場面で使うべきか
どのツールでも同じですが、Replica-aware routing が力を発揮するのは特定の場面です。繰り返し挙がるものが 3 つあります。
一時テーブルまたは名前付きセッションを使っている場合。 セッションスコープのオブジェクトは、それを作成したレプリカ上にのみ存在します。セッション全体で 1 つのルーティングキーを使い続ければ、クエリは同じレプリカで実行されます。
レプリカのキャッシュをウォームに保ちたい場合。 同じレプリカが同じワークロードを処理し続けると、そのローカルキャッシュはウォームなままです: ファイルシステムキャッシュ、解凍済みブロック、遅延読み込みされる主キーとインデックス、そしてクエリキャッシュです。(正直に言えば、これに対する長期的な答えとしては分散キャッシュのほうが優れていますが、レプリカのメモリ上にも価値のあるキャッシュは残っています。)
read-after-write の整合性。 レプリカが複数あるサービスでは、あるレプリカへの書き込みが、レプリケーションが追いつくまで他のレプリカから見えないことがあります。Replica-aware routing を使えば、他のレプリカがまだ追いついていない間でも、自分の書き込みを読み取れます。これはインタラクティブなアプリケーションや、挿入を検証してから次に進む ETL ジョブでかなり便利です。関連して、スキーマを変更したものの、それがまだレプリカ間で同期されていない場合もあります。このとき Replica-aware routing を使えば、確実に新しいスキーマに対して挿入でき、エラーを避けられます。
どのように構築したか
ハッシュに使うキーを探す
私たちのプロキシ層は Istio と Envoy の上に構築されています。Istio (正確には Istio Pilot) が設定を管理し、Envoy はデータプレーン、つまり実際にバイトを運ぶプロキシを担います。
スティッキールーティングに対する最初のアプローチは、URL ベースのサブドメインでした。しかしこの方法は、私たちが利用している証明書プロバイダー側の制約のためにスケールしませんでした。議論を重ねるうちに、L4 プロキシではなく L7 プロキシを動かすという案に行き当たりました。L7 であれば、Envoy はクエリパラメーターのようにリクエスト自体を読み取り、それに基づいてルーティングできます。L4 プロキシではうまくいきません。Envoy から見えるのは TCP パケットだけだからです。
最初のアプローチは session_id を使うものでした。しかし ClickHouse は 1 つのセッション内で一度に 1 つのクエリしか実行できないため、この方法は取りやめました。1 セッションに 1 クエリという制約は、到底理想的とは言えません。
session_id の代わりに選んだのは、ClickHouse が単に無視するヘッダーです。ClickHouse 固有の設定には X-ClickHouse の名前空間を使っています。ユーザーがヘッダーで値を渡す → Envoy がその値をコンシステントハッシュに使う → リクエストがいずれかのレプリカにルーティングされる、という流れです。
これはご自身で試せます。次のコマンドは、インスタンスを繰り返し呼び出し、応答したレプリカのホスト名を表示します。HTTP ベースのスティッキールーティングが有効であれば、常に同じレプリカが表示されます。まず、クラスターにレプリカが 2 つ以上あることを確認してください。1 つしかない場合、このデモの面白さはかなり減ります 😉
while true; do
echo 'select hostname()' | curl \
'https://abcdefghij.eu-west-1.aws.clickhouse.cloud:8443' \
-H 'X-ClickHouse-Replica-Tag: some-string' \
-H 'X-ClickHouse-User: YOUR-USER' \
-H 'X-ClickHouse-Key: YOUR-PASSWORD' \
-d @-
doneネイティブ接続のサポート
しかし HTTP だけで終えるわけにはいきませんでした。ネイティブ接続を使うユーザーもいます。
最初の制約を思い出してください: スティッキールーティングを使うインスタンスごとに新しい証明書を発行するのは、スケールしません。では、すでに持っている証明書をそのまま再利用できるとしたらどうでしょうか。
ClickHouse クライアントには、Server Name Indication (SNI) を上書きする方法が用意されています。
clickhouse client --help | grep tls-sni-override
# --tls-sni-override arg Override the SNI host name used for TLS connectionsそこで考えたのは、TLS 接続の際にグローバル証明書を検証しつつ、ルーティングには SNI を使うという方法です。ネイティブ接続のサポートは、まさにこの発想に基づいています。
while true; do
clickhouse client \
--host abcdefghij.eu-north-1.aws.clickhouse.cloud \
--secure \
--tls-sni-override some_string.sticky.abcdefghij.eu-north-1.aws.clickhouse.cloud \
--query 'select hostname()'
done--host は、証明書の中で検証するホストです。--secure は、この接続で TLS を使うことを示します。--tls-sni-override がルーティングに使う値です。クライアントはリージョンの証明書を検証します。このとき、SNI の値も送信し、Envoy がそれをハッシュしてルーティングに使います。これは最初のアプローチと同じ、ホスト名に対するリングハッシュの仕組みですが、新しい工夫が加わっています: --tls-sni-override によって、ルーティングに使うホスト名 (SNI) と、証明書を検証する対象のホスト名 (--host) を切り離せます。
Golang では次のようになります:
tlsConfig.ServerName = sniOverride
// disable the default check. If we don't disable then our
// client will try to match some_string.sticky.abcdefghij.eu-north-1.aws.clickhouse.cloud
// which the cert does not cover
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
// get all the certs
certs := make([]*x509.Certificate, 0, len(rawCerts))
for _, raw := range rawCerts {
cert, err := x509.ParseCertificate(raw)
if err != nil {
return err
}
certs = append(certs, cert)
}
// error if no certs
if len(certs) == 0 {
return fmt.Errorf("no certificate presented by %s", dialHost)
}
// check the --host instead of the SNI value
// pool is for intermediate certs
opts := x509.VerifyOptions{DNSName: dialHost, Intermediates: x509.NewCertPool()}
for _, cert := range certs[1:] {
opts.Intermediates.AddCert(cert)
}
// validate the leaf cert, which
// abcdefghij.eu-north-1.aws.clickhouse.cloud
_, err := certs[0].Verify(opts)
return err
}これで Replica-aware routing の使い方は 2 通りになりました。1 つは HTTP でヘッダーを使う方法、もう 1 つはネイティブ接続で SNI のオーバーライドを使う方法です。
留意しておきたい点
本番環境で驚かずに済むよう、率直に注意点をいくつか挙げます。
- スティッキー性はベストエフォートであり、保証ではありません。 アップグレード、再起動、スケールインやスケールアウトなど、サービスの構成を変えるものはいずれもルーティングを崩し、クエリを別のレプリカに到達させる可能性があります。そうなるとキーは別のレプリカに移り、頼っていた一時テーブルやセッション設定は作り直す必要があります。
SELECT hostName()を実行すれば、現在どのレプリカにいるかはいつでも分かります。 - ワークロードの分離ではありません。 Replica-aware routing はリクエストを処理するレプリカを制御しますが、そのレプリカは引き続き他のトラフィックも処理します。
- Enterprise 限定です。 この機能は Enterprise ティアのプランに順次展開されており、サービスの設定ページから利用できます。まだアカウントに届いていない場合は、早めに有効化するために、お気軽にサポートチケットを起票してください。

TLDR
要点だけ知りたい方のために、まとめておきます :) Replica-aware routing は、クエリを同じレプリカにルーティングします。Enterprise ティアのアカウントであれば、HTTP またはネイティブ接続から利用でき、標準の ClickHouse Cloud アカウントと BYOC の両方で使えます。スティッキー性がベストエフォートであることだけは覚えておいてください。詳細はドキュメントを参照してください。それでは、よいクエリを!
今すぐ始める
ClickHouse がご自身のデータでどのように動作するか試してみませんか? ClickHouse Cloud なら数分で始められ、300 ドル分の無料クレジットを受け取れます。
サインアップ


