イントロダクション
ランサーズでは、現在CakePHP2 → CakePHP4へのバージョンアップを行っています。
CakePHP1.3 → 2へのバージョンアップと比べ、単純な手順で移行するわけにはいきません。
CakePHP → 3 or 4へのバージョンアップは、非常に変更が多く、ほぼ作り直しに等しい工数がかかります。
作り直しになるのであれば、このタイミングで今までの負債を解消してしまおうという方針で、バッチと管理画面を別リポジトリに分離して再構築中です。
https://speakerdeck.com/ykanazawa/ransazufalse-cakephp4yi-xing-nituite?slide=9
また、この過程でCakePHP4以降のノウハウを貯めておき、後のlancers本体のCakePHP4移行で応用するという方針です。
参照クエリのReplica分散
大抵のWebサービスは、更新よりも圧倒的に参照クエリの方が多いと思います。
DBの特にCPU負荷に関しては、スケールアウトしてReplicaに分散することが有効な対策の1つになります。
参考:【SRE】成長するサービスとDB負荷との闘い
https://qiita.com/yKanazawa/items/4514eb2d17761a0da48e
cakephp-master-replicaプラグイン
ランサーズでは、参照クエリのReplica分散処理を独自に実装していましたが、CakePHP4に移行中のプロジェクトでは、以下のを利用して分散しています。
cakephp-master-replicaプラグイン
https://packagist.org/packages/connehito/cakephp-master-replica
このプラグインはCakePHP3でも利用可能で、CakePHP2用にも以下の形で用意されています。
https://packagist.org/packages/connehito/cakephp2-master-replica
参照SQLを自動的にReplicaに振り分ける
このプラグインを使えば、手軽にReplicaへの負荷分散が可能ですが、参照SQLの実行前にReplicaに切り替え、実行後にMasterに戻す処理をその都度入れるのは面倒です。
そこで、Tableクラスを継承したBaseTableクラスを作成し、参照SQLを発行する関数を実行する前にReplicaに切り替え、実行後に戻すという処理を実装しました。
参照SQL発行直前にReplicaに切り替え、実行直後にmasterに戻す
初めに実装していたのでこの方法です。
abstract class BaseTable extends Table
{
public function __call($method, $args)
{
if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
$this->getConnection()->switchRole('replica');
$result = parent::__call($method, $args);
$this->getConnection()->switchRole('master');
return $result;
}
return parent::__call($method, $args);
}
public function find(string $type = 'all', array $options = []): Query
{
$this->getConnection()->switchRole('replica');
$result = parent::find($type, $options);
$this->getConnection()->switchRole('master');
return $result;
}
…
ところが、この方法だと、状況によってはReplicaに切り替わらないことがわかりました。
※原因は調査中ですが、switchRole関数で頻繁にReplica、Masterの切り替え行うと実行されないことがあるようです。
(クエリログを見る限りは、switchRole関数で切り替えたときに、必ずしも切り替わるわけではないらしいことまでは確認しました。)
参照SQL発行前にReplicaに切り替え、更新SQL実行前にmasterに切り替え
そこで、参照SQLを発行する際にReplicaに切り替え、更新時にMasterに切り替えるという処理にしました。
この方法であれば、switchRole関数の発行回数が大幅に減ります。
ただし、更新クエリを発行する関数全てにMasterに切り替える処理を入れないとエラーとなってしまうため注意が必要です。
現在、この方法を採用し、どんな状況でもReplicaに切り替わることを確認しています。(確認した限りですが)
abstract class BaseTable extends Table
{
...
public function __call($method, $args)
{
if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
$this->_switchReplica();
return parent::__call($method, $args);
}
$this->_switchMaster();
return parent::__call($method, $args);
}
...
public function query(): Query
{
$this->_switchMaster();
return parent::query();
}
public function updateAll($fields, $conditions): int
{
$this->_switchMaster();
return parent::updateAll($fields, $conditions);
}
...
トランザクション実行時はswitchRoleを行わない
トランザクションを実行中にswitchRoleを行うと、トランザクションが切れてしまうため、トランザクション実行中は切り替えないように実装します。
private function _switchMaster(): void
{
$conn = $this->getConnection();
if ($conn->inTransaction()) {
return;
}
$conn->switchRole('master');
}
private function _switchReplica(): void
{
$conn = $this->getConnection();
if ($conn->inTransaction()) {
return;
}
$conn->switchRole('replica');
}
そして、トランザクションを実行する直前に、ここだけmasterに手動切り替えが必要になります。
$conn = $this->Users->getConnection();
$conn->switchRole('master');
$conn->transactional($saveProcess);
実装したコード
上記を踏まえて実装したコードが以下になります。
※このソースコードはGithubでも公開しています。
https://github.com/LancersDevTeam/PHP_versionup/blob/master/CakePHP4/src/Model/Table/BaseTable.php
src/Model?table?Basetable.php
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\Datasource\EntityInterface;
use Cake\ORM\Query;
use Cake\ORM\Table;
/**
* @SuppressWarnings(PHPMD)
*/
abstract class BaseTable extends Table
{
private function _switchMaster(): void
{
$conn = $this->getConnection();
if ($conn->inTransaction()) {
return;
}
$conn->switchRole('master');
}
private function _switchReplica(): void
{
$conn = $this->getConnection();
if ($conn->inTransaction()) {
return;
}
$conn->switchRole('replica');
}
public function __call($method, $args)
{
if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
$this->_switchReplica();
return parent::__call($method, $args);
}
$this->_switchMaster();
return parent::__call($method, $args);
}
public function find(string $type = 'all', array $options = []): Query
{
$this->_switchReplica();
return parent::find($type, $options);
}
public function findAll(Query $query, array $options): Query
{
$this->_switchReplica();
return parent::findAll($query, $options);
}
public function findList(Query $query, array $options): Query
{
$this->_switchReplica();
return parent::findList($query, $options);
}
public function findThreaded(Query $query, array $options): Query
{
$this->_switchReplica();
return parent::findThreaded($query, $options);
}
public function get($primaryKey, $options = []): EntityInterface
{
$this->_switchReplica();
return parent::get($primaryKey, $options);
}
public function findOrCreate($search, ?callable $callback = null, $options = []): EntityInterface
{
$this->_switchMaster();
return parent::findOrCreate($search, $callback, $options);
}
public function query(): Query
{
$this->_switchMaster();
return parent::query();
}
public function updateAll($fields, $conditions): int
{
$this->_switchMaster();
return parent::updateAll($fields, $conditions);
}
public function deleteAll($conditions): int
{
$this->_switchMaster();
return parent::deleteAll($conditions);
}
public function exists($conditions): bool
{
$this->_switchReplica();
return parent::exists($conditions);
}
public function save(EntityInterface $entity, $options = [])
{
$this->_switchMaster();
return parent::save($entity, $options);
}
public function saveOrFail(EntityInterface $entity, $options = []): EntityInterface
{
$this->_switchMaster();
return parent::saveOrFail($entity, $options);
}
public function saveMany(iterable $entities, $options = [])
{
$this->_switchMaster();
return parent::saveMany($entities, $options);
}
public function saveManyOrFail(iterable $entities, $options = []): iterable
{
$this->_switchMaster();
return parent::saveManyOrFail($entities, $options);
}
public function delete(EntityInterface $entity, $options = []): bool
{
$this->_switchMaster();
return parent::delete($entity, $options);
}
public function deleteMany(iterable $entities, $options = [])
{
$this->_switchMaster();
return parent::deleteMany($entities, $options);
}
public function deleteManyOrFail(iterable $entities, $options = []): iterable
{
$this->_switchMaster();
return parent::deleteManyOrFail($entities, $options);
}
public function deleteOrFail(EntityInterface $entity, $options = []): bool
{
$this->_switchMaster();
return parent::deleteOrFail($entity, $options);
}
}