Cloud SQL に Reader Endpoint 的なものを作ってみる
この記事は Google Cloud Japan Advent Calendar 2021 の 21 日目の記事です。
はじめに
Google Cloud のマネージドなデータベースサービスである Cloud SQL には今年も多くのアップデートがありました。
中でもダウンタイムの削減(90%!)などメンテナンス関連のアップデートはマネージドサービスの弱点とも言える部分を改善するものであり 、Cloud SQL をより使いやすいものにしているのではないでしょうか。
そんな進化を続ける Cloud SQL ですが、まだまだたくさんの機能要望をいただきます。その中の1つがリードレプリカにリクエストを分散してくれる単一のエンドポイント(Reader Endpoint)です。
このようなエンドポイントを実装する方法はいくつかあります。例えばリバースプロキシを使用する方法や DNS ラウンドロビンを使用する方法などです。ちょうど先日 Cloud DNS でもこのラウンドロビン機能が使えるようになりました。
ということは Cloud DNS でラウンドロビンの設定をするだけで、簡単に Cloud SQL のリードレプリカにリクエストを分散するエンドポイントを作れそうです。早速試してみたいと思います。
(もう少し正確には DNS ルーティング ポリシー という機能で、使用できるポリシーとして重み付きラウンドロビンがあります。
DNS ルーティング ポリシーは一般提供前のプレビュー機能となり、限定的なサポートの提供などの制限がありますのでご了承ください。詳しくはこちら。)
Cloud SQL の設定
asia-northeast1 の各ゾーン(a, b, c)に 1 つずつ、合計 3 つのリードレプリカを作成しました。
パブリック IP を外し、プライベート IP を有効にしたこと以外はほぼデフォルトの設定です。
gcloud コマンドを使用して、設定した Cloud SQL の情報を見てみましょう。
ソースインスタンスの情報
「ipAddresses」から プライベート IP のみを持っていること、「replicaNames」から、このインスタンスが 3 つのレプリカインスタンスを持っていることがわかります。
❯ gcloud sql instances describe readreplica-dns-rr
databaseInstalledVersion: MYSQL_8_0_18
databaseVersion: MYSQL_8_0
gceZone: asia-northeast1-a
instanceType: CLOUD_SQL_INSTANCE
ipAddresses:
- ipAddress: 10.201.209.14
type: PRIVATE
name: readreplica-dns-rr
region: asia-northeast1
replicaNames:
- readreplica-dns-rr-replica-a
- readreplica-dns-rr-replica-b
- readreplica-dns-rr-replica-c(実行結果の抜粋です)
レプリカインスタンスの情報
「gceZone」からこのインスタンスがどのゾーンに存在するか、「instanceType」からこのインスタンスがリードレプリカであること、「ipAddresses」から プライベート IP のみを持っていることがわかります。
- asia-northeast1-a に作成したリードレプリカインスタンスの情報
❯ gcloud sql instances describe readreplica-dns-rr-replica-a
databaseInstalledVersion: MYSQL_8_0_18
databaseVersion: MYSQL_8_0
gceZone: asia-northeast1-a
instanceType: READ_REPLICA_INSTANCE
ipAddresses:
- ipAddress: 10.201.209.16
type: PRIVATE
masterInstanceName: naraoka-playground:readreplica-dns-rr
name: readreplica-dns-rr-replica-a
region: asia-northeast1(実行結果の抜粋です)
- asia-northeast1-b に作成したリードレプリカインスタンスの情報
❯ gcloud sql instances describe readreplica-dns-rr-replica-b
databaseInstalledVersion: MYSQL_8_0_18
databaseVersion: MYSQL_8_0
gceZone: asia-northeast1-b
instanceType: READ_REPLICA_INSTANCE
ipAddresses:
- ipAddress: 10.201.209.18
type: PRIVATE
kind: sql#instance
masterInstanceName: naraoka-playground:readreplica-dns-rr
name: readreplica-dns-rr-replica-b
region: asia-northeast1(実行結果の抜粋です)
- asia-northeast1-c に作成したリードレプリカインスタンスの情報
❯ gcloud sql instances describe readreplica-dns-rr-replica-c
databaseInstalledVersion: MYSQL_8_0_18
databaseVersion: MYSQL_8_0
gceZone: asia-northeast1-c
instanceType: READ_REPLICA_INSTANCE
ipAddresses:
- ipAddress: 10.201.209.20
type: PRIVATE
kind: sql#instance
masterInstanceName: naraoka-playground:readreplica-dns-rr
name: readreplica-dns-rr-replica-c
region: asia-northeast1(実行結果の抜粋です)
Cloud DNS の設定
まず限定公開ゾーン「cloudsql」を作成し、
次に、作成した限定公開ゾーンにルーティングポリシーが重み付きラウンドロビンなレコードセット「rr.cloudsql.private.」を作成しました。
- RFC 6762 によるとプライベート向け TLD は「.local.を利用しない」「.intranet., .internal., .private., .corp., .home., .lan. のいずれかを利用する」とのことだったので限定公開ゾーンの TLD は「.private」に設定
- 重み付きラウンドロビンのリソースレコードデータには Cloud SQL リードレプリカのプライベート IP を設定
- 各リードレプリカにリクエストを均等に分散するため、同じ重み (0) を設定
- リードレプリカの追加/変更時のタイムラグを少なくするために、TTL は短く(1 秒に)設定
(※今回は検証のため TTL を 1 秒にしていますが、実際には要件に合わせて適切な値を設定してください。)
動作確認
DNS ラウンドロビンの確認
上記で設定した重み付きラウンドロビンが意図したとおりに IP アドレスを均等に返すかを VPC 内の Compute Engine から dig コマンドを使って確認してみます。
# while true; do dig rr.cloudsql.private +short; sleep 2; done
10.201.209.18
10.201.209.16
10.201.209.20
10.201.209.16
10.201.209.16
10.201.209.18
10.201.209.16
10.201.209.16
10.201.209.20
10.201.209.16
10.201.209.18
10.201.209.18
いい感じにラウンドロビンされてそうですね。
エンドポイントに対する MySQL 接続が分散されることの確認
DNS 名「rr.cloudsql.private」に対して、実際に MySQL クライアントから接続を行い、接続が分散されることを確認してみます。
# while true; do mysql -u root -ppassword -e "select connection_id()\G" -h rr.cloudsql.private; echo; sleep 2; done 2>/dev/null
*************************** 1. row ***************************
connection_id(): 2326*************************** 1. row ***************************
connection_id(): 4337*************************** 1. row ***************************
connection_id(): 4213*************************** 1. row ***************************
connection_id(): 4338*************************** 1. row ***************************
connection_id(): 4339*************************** 1. row ***************************
connection_id(): 4340*************************** 1. row ***************************
connection_id(): 2327*************************** 1. row ***************************
connection_id(): 2328*************************** 1. row ***************************
connection_id(): 4214*************************** 1. row ***************************
connection_id(): 4215*************************** 1. row ***************************
connection_id(): 4216
Cloud SQL では show variables で取得できる hostname の値がすべて localhost になっているため、どのインスタンスに接続しているかの判断に使用できません。代わりに connection_id を使用して接続が振り分けられていることを確認してみたいと思います。
connection_id はコネクション毎(接続毎)に増えていく値となっています。検証開始時点でリードレプリカ毎に connection_id の起点が異なっています。(2326, 4337, 4213)
この connection_id の起点を基に値の増え方を観察してみると、
* 2326 → 2327 → 2328
* 4337 → 4338 → 4339 → 4340
* 4213 → 4214 → 4215 → 4216
の 3 つの流れがあることがわかります。
このことから Cloud DNS 上に設定した DNS 名 rr.cloudsql.private を単一のエンドポイントとして、そこに対する接続が 3 つのリードレプリカに分散されていることがわかります。
期待したとおり、Cloud DNS の重み付きラウンドロビンの設定だけで、簡単に Cloud SQL のリードレプリカにリクエストを分散するエンドポイントを作ることができました!
おまけ
Cloud SQL のリードレプリカへのリクエストを分散するエンドポイントを簡単に作ることができましたが、リクエストの分散だけでなく、リードレプリカをチェックして利用できなくなった際にはエンドポイントから外したり、逆にリードレプリカを追加した際にはエンドポイントに追加したりとエンドポイント操作も自動化したいところです。
そこで本記事ではおまけとして、これらの機能を Google Cloud のサービスで作ってみたいと思います。
構成
Cloud SQL のリードレプリカをチェックし、必要に応じて Cloud DNS の重み付きラウンドロビンを更新(IP アドレスの追加/削除)する Cloud Run アプリケーションをデプロイし、それを Cloud Scheduler から定期的に実行します。
(先日発表された Cloud Run の CPU Allocation 機能を使用して、バックグラウンドでチェックを動かし続けても面白いですね。Cloud Scheduler が不要になり、Cloud Scheduler の実行間隔にも依存しなくなるので、30 秒毎など、より短い間隔でチェックすることも可能になります。)
処理の流れは以下のようなものになります。
- Cloud Scheduler が定期的に Cloud Run を実行
- Secret Manager からデータベースパスワードを取得し Cloud Run の環境変数として設定(参考)
- ソースデータベースに紐づくリードレプリカのプライベート IP を取得
- 取得したプライベート IP を使用してリードレプリカに接続可能かどうかを確認
- Cloud DNS のエントリ(IP アドレスのリスト)を取得。④でチェックした現在利用可能なリードレプリカのプライベート IP のリストと比較して、異なる場合は DNS のエントリを更新
Cloud Run で動かすアプリケーションのサンプルコード
※あくまでサンプルコードであり、完全な動作を保証するものではありませんのでご了承ください。
(クライアントライブラリを使用して API 実行し、状況に応じてマネージドサービスの設定を変えているという雰囲気を感じていただければ。。)
対象とする Cloud SQL のインスタンスなどの情報は環境変数を通じて設定しています。このアプリケーションを環境変数を設定しつつ Cloud Run にデプロイするには以下のようなやり方があります。
$ gcloud run deploy <サービス名> \
--image <コンテナイメージの URL> \
--set-env-vars "PROJECT_ID=<プロジェクト ID>,\
CLOUDSQL_INSTANCE_NAME=<Cloud SQL のインスタンス名>,\
CLOUDDNS_MANAGED_ZONE_NAME=<Cloud DNS のゾーン名>,\
CLOUDDNS_RRSET_NAME=<Cloud DNS の Resource Record Set 名>,\
CLOUDDNS_RRSET_TYPE=<Cloud DNS の Resource Record Set タイプ>" \
--update-secrets=CLOUDSQL_PASSWORD=<データベースパスワードを格納している Secret Manager Secret> \
--vpc-connector <Serverless VPC Acccess コネクタ名>
それではこのアプリケーションが動作した時の様子を見てみましょう。
リードレプリカを追加したときの様子
アプリケーションログ
追加したリードレプリカの検出、ヘルスチェック、DNS エントリの更新が行われています。
Cloud DNS のレコードセット
追加したリードレプリカのプライベート IP が DNS エントリに追加されました。
リードレプリカを削除したときの様子
アプリケーションログ
リードレプリカを削除したことによって発生した、現在利用可能なリードレプリカのプライベート IP のリストと重み付きラウンドロビンの IP アドレスリストの差異を検出し、DNS エントリを更新しています。
Cloud DNS のレコードセット
削除したリードレプリカのプライベート IP が DNS エントリからも削除されました。
おわりに
もちろんこういった機能はサービスの標準機能として提供されるのが一番なのですが、Google Cloud のサービスの組み合わせと少しの実装で実現することもできますので、参考になれば幸いです。