こんにちは。
個人開発をしているとき、将来のことを考えて遅々として進まないことありますよね。 これを捕らぬ狸の皮算用といいます。
この記事はSymfonyでAPIサーバー構築をしている時に、DB接続のレプリケーション選択がどのように動いているのかを調べたものです。
doctrine-bundle:2.7.2
時点のソースコードをもとにしています。
どうやればレプリケーションをうまく使えるのか
DoctrineBundle Configuration Reference(2.7.2時点)に書いてある通り設定するだけで問題なく動作します。
いつも通りコネクションを設定したうえで、doctrine.dbal.replicas.[replicaName]
(複数のコネクションを設定する場合は doctrine.dbal.connections.[connectionName].replicas.[replicaName]
)にレプリケーションを受けているDBの設定を記述してください。
設定さえしておけば、あとはフレームワーク側より先の処理でいい感じに接続先DB選択などをしてくれます。
いかがでしたか。設定するだけでいい感じにしてくれるなんてDoctrineは優秀ですね。
仕組み
「いい感じに接続先DBを選択してくれる」と言われて「はいそうですか」と何も考えずに使うのは難しいですよね。 実際のところどのように動いているのかを少し掘ってみたので、再度確認するときのフックになる程度に書いておきます。
設定
Doctrine\Bundle\DoctrineBundle\DependencyInjectio\DoctrineExtension
に、設定読み取りとサービスコンテナへの登録処理が書かれています。
dbal
のコネクションに関する設定の中で replicas
1 といった設定を検知したとき、wrapper_class
にDoctrine\DBAL\Connections\PrimaryReadReplicaConnection
2が設定されます。
この PrimaryReadReplicaConnection
が名前の通り、プライマリかレプリカへの接続を自動で選択するコネクションクラスです。
接続先DB選択
前述したとおり PrimaryReadReplicaConnection
とその周囲が接続先DBを選択しています。
その選択基準は PrimaryReadReplicaConnection
のコメントにしっかり書かれていました。
以下のリストはソースコード中のコメントを要約して追記したものです。
- 一度もプライマリに接続したことがない時点で
Connection::getWrappedConnection()
やConnection::executeQuery()
を通じて実行したクエリはレプリカに送信される- NativeQueryを実行したり、DoctrineのDriverを直接触るようなときにこのケースに引っかかります
Connection::executeStatement()
やConnection::beginTransaction()
などを通じたクエリの実行はプライマリに送信される- QueryBuilderを経由した場合などはこのケースに当たります
Doctrine\ORM\Query\SqlWalker
で実行するクエリに合わせて接続先が自動で選択されます- ただし
SELECT ~ FOR UPDATE
はレプリカに流れてしまいそうな雰囲気がありました3
- QueryBuilderを経由した場合などはこのケースに当たります
- 一度でもプライマリに接続すると、以降のクエリ実行はすべてプライマリで行われる
- この挙動は、設定の
keep_replica
によって変更可能keep_replica: false
: プライマリへの接続を確立したとき、レプリカ接続としても確立したプライマリへの接続を使うようになる- トランザクションを張っていろいろした後に、トランザクション外でSELECTクエリを実行してもプライマリに送信される
- トランザクションを張らずに更新系クエリを実行した後のSELECTクエリはプライマリに送信される
keep_replica: true
: プライマリへの接続を確立したとしても、レプリカへの接続へは影響を及ぼさない- トランザクションを張っていろいろした後に、トランザクション外でSELECTクエリを実行するとレプリカに送信される
- この挙動は、設定の
- レプリカでクエリを実行する場合、レプリカ設定が複数存在するなら接続時にランダムに1つ選択される。接続を明示的に閉じるまで選択されたレプリカでクエリが実行される
結論
keep_replica
は、特に意図がなければデフォルトのFALSEのままにしておこう- 複数のクエリを実行する場合はトランザクションをちゃんと張ろう
- トランザクションを張らずにクエリを実行する場合はクエリビルダーを経由しよう
要するに、よく考えられているなぁということです。