LoginSignup
6
0

More than 1 year has passed since last update.

Laravelのレプリケーション遅延対策

Last updated at Posted at 2021-07-05

LaravelのDBとしてAWSのAuroraを使っていてマスタースレーブ構成にしていたのですが、レプリケーション遅延により書き込んだはずのデータが見当たらないという事態が発生していました。
データを追加したユーザーが一覧ページに戻ってみると、追加したはずのデータが見当たらないというのはユーザー体験として良くないので、自分が取った対応方法について書いていこうと思います。

状況

コントローラの処理としては、以下のようなstoreアクションで書き込んだ後リダイレクト先のindexアクションで読み込みをしていて、この読み込みの際に書き込んだデータが反映されていないという現象が5回に1回程度発生します。

MenuController.php
<?php

namespace App\Http\Controllers\Web\Professional;

use Illuminate\Routing\Controller;
use App\Models\Menu;

class MenuController extends Controller
{
    public function index(FetchOrderedMenusService $service)
    {
        $menus = Menu::all();
        return view('menu.index', compact('menus');
    }

    public function store(Request $request)
    {
        Menu::create($request->all());
        return redirect()->route('menus.index');
    }
}

Auroraのレプリカラグが20ms、書き込み処理からリダイレクトして読み込み直前までの時間を計測すると約100msなので、読み込みのタイミングでスレーブに反映されていないということは無い気がするのですが、たまたまレプリケーションに時間がかかったというのとリクエストの処理時間が早かったという状況が重なって発生しているのではと考えました。
実際に対策をすると今回の現象は発生しなくなりました。

対策

方針

config/database.phpstickyを有効化している場合だと、同一リクエスト上ではマスターに書き込みを行った後はマスターを参照するようになります。ただ、今回はリダイレクトしたときにマスターを参照して欲しいのでstickyを有効化するだけでは対応できません。

stickyを有効化している場合の処理を追ってみると、DB::recordsHaveBeenModified()を呼び出すことでそれ以降の処理ではマスターを参照するようだったので、この関数を使って書き込み直後のリクエストでマスターを参照させます。

実装

以下のようなミドルウェアを作成し、既存の実装に影響が出ないようにします。
ミドルウェアの実装内容としては、DBに書き込みを行った直後のリクエストかどうかを判別できるように書き込みのタイミングでセッションに値を保存しておき、次のリクエストでセッションに値が存在すればDB::recordsHaveBeenModified()を呼び出します。

<?php

namespace App\Http\Middleware;

use Closure;
use DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;

class UseWriteConnectionWhenHaveBeenModified
{
    public function handle(Request $request, Closure $next, ...$modelClassNames): mixed
    {
        $callback = function () {
            session()->flash('haveBeenModified');
        };

        foreach ($modelClassNames as $modelClassName) {
            if (!is_subclass_of($modelClassName, Model::class)) {
                continue;
            }

            $modelClassName::created($callback);
            $modelClassName::updated($callback);
            $modelClassName::deleted($callback);
        }

        if (session()->has('haveBeenModified')) {
            DB::recordsHaveBeenModified();
        }

        return $next($request);
    }
}

書き込み直後のリクエストを処理し終わったらセッションから値が削除されて欲しいのでflash関数を使っています。

作成したミドルウェアをコントローラから利用できるようにKernel.phpに追記します。

app/Http/Kernel.php
protected $routeMiddleware = [
    ...
    'haveBeenModified' => \App\Http\Middleware\UseWriteConnectionWhenHaveBeenModified::class,
];

最後にコントローラのコンストラクタにミドルウェアを追加します。

MenuController.php
public function __construct()
{
    $this->middleware('haveBeenModified:' . Menu::class);
}

指定したeloquentモデルのデータに書き込み処理が行われると、直後のリクエストはマスターを参照するようになります。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0