【GCP】転職先のRailsをreplicaに対応させたので色々記録
きっかけ
DB負荷的にそろそろだし今やっとく?みたいな流れ。
環境
まずrails側
- ruby 3.0.3
- Rails 6.1.6.1
そしてインフラ
- GKE
- CloudSQL(MySQL5.7)
- CloudSQL Proxy
現状のインフラ構成と大まかな変更内容の確認
Kubernetes上のdefaultネームスペースでRails等、sqlproxyというネームスペースでCloudSQL Proxyが動いている。
defaultがDBにアクセスするときは、sqlproxy内のproxyに繋ぎ、DBとやり取りをしている状態。
今回はこれに、proxyを一つ追加しPrimaryDB用とReplicaDB用の二つのproxyを立てる構成で行こうと思います。
CloudSQLにreplicaインスタンスの追加
ここは特に説明することもないので、replicaを追加したいインスタンスのサイドバーから「レプリカ->リードレプリカを作成」でOKです。細かい設定は各自お好みで。
replica用のユーザー追加
後述しますが、今回のやり方ではRailsがreplicaに接続している時は自動でUpdate等を禁止してくれるのですが、念のためreplica用に権限を減らしたユーザーを追加しておきます。
GCP上から「ユーザー->ユーザーアカウントを追加」で追加してください。注意として、replicaインスタンスからはユーザー追加できませんので、primaryインスタンスで操作してください。
そして、このままの状態のユーザーは権限を過剰に持っていますので、SELECT権限のみ付与します。
mysql> revoke all privileges, grant option from '<YOUR_REPLICA_USER>'@'cloudsqlproxy~%';
mysql> grant select on <YOUR_REPLICA> to '<YOUR_REPLICA_USER>'@'cloudsqlproxy~%';
Railsの設定
過去にはreplicaとの接続の切り替えを勝手にやってくれるgemもあったらしいですが、今回はRails6から?標準で存在する機能を使って実現したいと思います。
標準機能を使う場合、主に二つ方法があって
- GETやHEADのリクエストを受け取るとreplica、PUTやDELETEのリクエストを受け取るとprimaryに自動で接続する(もう少し正確に言うとロールを切り替える?)
- replicaを使いたい時に都度設定を変える
となるのですが、今回はシステムの要件的に後者で実装することになりました。
database.ymlの修正
primaryとreplicaの二つのDBに対応させるため、以下のようなymlになります。
default: &default
adapter: mysql2
pool: 5
host: <%= ENV.fetch('RAILS_DB_HOST', '') %>
username: <%= ENV.fetch('RAILS_DB_USER', 'root') %>
password: <%= ENV.fetch('RAILS_DB_PASSWORD', 'rootroot') %>
socket: <%= ENV.fetch('RAILS_DB_SOCKET', '/tmp/mysql.sock') %>
port: <%= ENV.fetch('RAILS_DB_PORT', 3306) %>
charset: utf8mb4
collation: utf8mb4_bin
encoding: utf8mb4
development:
primary: # development環境でprimaryインスタンスに接続するための設定
<<: *default
database: development
replica: # development環境でreplicaインスタンスに接続するための設定
<<: *default
database: development
host: <%= ENV.fetch('RAILS_DB_REPLICA_HOST', '') %>
username: <%= ENV.fetch('RAILS_DB_REPLICA_USER', 'replica') %>
password: <%= ENV.fetch('RAILS_DB_REPLICA_PASSWORD', 'replica') %>
replica: true # replicaはこれが必要
production:
primary: # production環境でprimaryインスタンスに接続するための設定
<<: *default
database: production
replica: # production環境でreplicaインスタンスに接続するための設定
<<: *default
database: production
host: <%= ENV.fetch('RAILS_DB_REPLICA_HOST', '') %>
username: <%= ENV.fetch('RAILS_DB_REPLICA_USER', 'replica') %>
password: <%= ENV.fetch('RAILS_DB_REPLICA_PASSWORD', 'replica') %>
replica: true # replicaはこれが必要
各環境ごとにprimaryとreplicaという設定が増えていて、それぞれ名前の通りインスタンスごとの接続設定を追加しています。
ここで重要なポイントとして、
development:
replica:
...
replica: true # <-これ
このような値を指定してるのですが、このreplica: true
が設定されている接続がreplicaとして認識され、この接続を使うとInsertやUpdateはできなくなります。
application_record.rbの修正
次はapplication_recordになります。
先ほど設定した接続情報を実際に使える状態にします。
class ApplicationRecord < ActiveRecord::Base
...
connects_to database: { writing: :primary, reading: :replica }
end
以上でRailsでの準備は終了です。
Kubernetes
Cloud SQL Proxyはほぼコピペで同じものを二つ用意します。
apiVersion: v1
kind: Service
metadata:
name: mysql-proxy
namespace: sqlproxy
labels:
app: mysql-proxy
spec:
ports:
- name: mysql
protocol: TCP
port: 3306
targetPort: mysql
selector:
app: mysql-proxy
---
apiVersion: v1
kind: Service
metadata:
name: mysql-replica-proxy
namespace: sqlproxy
labels:
app: mysql-replica-proxy
spec:
ports:
- name: mysql
protocol: TCP
port: 3306
targetPort: mysql
selector:
app: mysql-replica-proxy
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-proxy
namespace: sqlproxy
labels:
app: mysql-proxy
spec:
selector:
matchLabels:
app: mysql-proxy
replicas: 1
template:
metadata:
labels:
app: mysql-proxy
spec:
containers:
- name: sqlproxy
image: gcr.io/cloudsql-docker/gce-proxy:1.28.0
imagePullPolicy: IfNotPresent
command:
- "/cloud_sql_proxy",
- "--dir=/cloudsql",
- "-instances=<YOUR_CONNECTION_NAME>=tcp:0.0.0.0:3306",
- "-credential_file=/secrets/cloudsql/credentials.json",
ports:
- name: mysql
containerPort: 3306
volumeMounts:
# ...
volumes:
# ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-replica-proxy
namespace: sqlproxy
labels:
app: mysql-replica-proxy
spec:
selector:
matchLabels:
app: mysql-replica-proxy
replicas: 1
template:
metadata:
labels:
app: mysql-replica-proxy
spec:
containers:
- name: sqlproxy
image: gcr.io/cloudsql-docker/gce-proxy:1.28.0
imagePullPolicy: IfNotPresent
command:
- "/cloud_sql_proxy",
- "--dir=/cloudsql",
- "-instances=<YOUR_CONNECTION_NAME>=tcp:0.0.0.0:3306",
- "-credential_file=/secrets/cloudsql/credentials.json",
ports:
- name: mysql
containerPort: 3306
volumeMounts:
# ...
volumes:
# ...
つづいてアプリケーションのpodです。
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
replicas: 3
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: asia.gcr.io/XXX
env:
- name: RAILS_DB_NAME
value: "production"
# primary用
- name: RAILS_DB_HOST
value: "mysql-proxy.sqlproxy" # 他のnamaespaceのpodを参照するときの書き方
- name: RAILS_DB_USER
value: "root"
- name: RAILS_DB_PASSWORD
value: "password"
# replica用
- name: RAILS_DB_REPLICA_HOST
value: "mysql-replica-proxy.sqlproxy"
- name: RAILS_DB_REPLICA_USER
value: "replica"
- name: RAILS_DB_REPLICA_PASSWORD
value: "replica"
ports:
- containerPort: 3000
name: app
primary用、replica用の環境変数を間違えないように定義しましょう。
以上で一通りの設定は完了になります。それでは、実際にreplicaに対してクエリを発行してみましょう。
replicaに対してクエリを発行する
replicaにクエリを発行する場合は、ActiveRecord::Base.connected_to
というものを使う必要があります。
users = nil
ActiveRecord::Base.connected_to(role: :reading) do # <-これ
users = User.all
end
render json: users
これでUser.all
がreplicaに対して実行されます。逆に言うとActiveRecord::Base.connected_to
を使わなければ(ブロックの外であれば)、primaryに対して実行されます。
以上でreplicaへの対応は終了になります。お疲れ様でした。
参考
Discussion