LaravelのセッションドライバをDBにしてみる

0
1272

PHPでセッションを管理する場合、通常はデータの保存先はファイルです。
Laravelでもデフォルトはファイルなのですが、設定によっては保存先をDBにできます。今回はその環境構築方法に関してです。
なお、「セッションって何?」と言う方はGoogle先生に聞いてください。

そもそも、なぜセッションデータの保存先をファイル以外にしたいかですが、負荷分散等の理由でL4ロードバランサを経由して複数のWebサーバ(同じLaravelロジックが動作)を使用する場合にファイル保存だと問題があるからです(「L4ロードバランサって何?」と言う方はGoogle先生に聞いてください)。
例えばWebサーバA,Bが存在した場合、あるクライアントからのアクセスはその都度A,Bのいずれかに振り分けられ、どちらにアクセスするかは特定されません。L4ロードバランサでも接続元IPアドレスによってアクセス先Webサーバを固定すると言う機能を持ったものもあるようですが、クライアントがモバイル端末の場合などは接続元IPが通信している間に変わることもあるので、この方式でもアクセス先Webサーバが特定できる保証はないと思われます(具体的に検証したことはありませんが)。加えてL4ロードバランサの全てが同機能を持っている訳でもないと思いますので、L4ロードバランサを使う前提であれば接続先Webサーバは特定できない前提で考えるべきかと思います。
上記前提において、例えばサーバAへのアクセスでセッションが確立された場合、そのセッションデータが保存されるファイルはサーバA上にしか存在しません。よって、次回以降のアクセスがサーバBに振り分けられると、同処理ではセッションデータが取得できません。ログイン状態などはセッションで管理されることが多いため、突然ログアウトされたような状態になる訳です。当然ながら運用に支障を来すことになります。

上記のような状況を考慮して、複数サーバで負荷分散的にPHPを運用する場合は「memcached」を使用してセッションデータを統合的に管理する方法を用いることが一般的です。昨今ではmemcachedと類似した仕組み(ファイルを介在させずメモリのみでデータ管理を行う方式)として「Redis」を用いるケースもあるようで、Laravelでもこれら2つの利用を想定した設定ができるようになっています。
ただ、個人的には以前担当したmemcachedを用いてセッションデータを行うシステム(他社制作の運用を引き継いだもの)でセッション上のデータが喪失しているように思われる障害の対応で苦労した経験があり、memcachedの利用には少々及び腰です。Redisに関しては使ったことがないと言うこととmemcachedと類似した仕組みであると言うことで前述のmemcachedのマイナスイメージを引きずって、こちらも少々ハードルが高い印象を持っています(いずれも著名な方法であるため適切な設定ができれば問題ないと思いますが、その適切な設定ができるかどうかを含めた判断において現時点では今一つ積極的に使用する気になれないと言ったところです)。

一方で、普段からデータの保存・参照で使い慣れたDBにセッションデータも格納してしまうと言う方式は、仕組み的には明快で、負荷分散にも対応できる(と言うか、セッション以前に複数Webサーバとそれらが扱うデータを統合的に管理する1つのDBサーバの組み合わせは負荷分散の基本形)と言うことで、かなり期待度の高い方式です。
他の方法と比較して性能的に劣る印象はありますが、この辺は程度問題なので、まずは試しに運用してみて様子を見てみたいところです。

と言うことで、前置きが長くなりましたが、Laravelでセッションデータの保存先をDBにする設定を行っていきたいと思います。
なお、環境としては毎度のことながら「Virtualbox/vagrant」で生成した仮想サーバ上に「Webmin/Virtualmin」でLAMP環境を構築しています。
この条件においてインストールされるMariaDBは2020年12月26日現在「5.5.68」です。
またLaravelは現状のLTSである6(6.20.8)を使用します。
実はこの組み合わせが後ほど若干の問題となります。

テーブル構築

DBでセッションデータを管理するためには当然ながらそのためのテーブルが必要になります。
Laravelでは上記テーブル構築をコマンドで実施できるようになっています(この辺がLaravelの便利なところ)。

元々Laravelには「マイグレーション」と言う仕組みがあり、テーブル構築を行うための処理(PHP)を生成しておき、これを実行することで目的のテーブルを構築する形をとっています。
通常のテーブルに関しては構造自体が未確定であるためLaravelの仕組みとしては雛形作成までで中身は自力で記述することになりますが、セッション管理用のテーブルに関しては構造が決まっている(と言うかLaravelが想定する構造になっている必要がある)ため、以下のコマンド実行で該当する処理(ファイル)が自動的にできてしまいます。

php artisan session:table

生成されるファイルからテーブル生成関数のみ抽出したものが以下です。

public function up()
{   
    Schema::create('sessions', function (Blueprint $table) {
        $table->string('id')->unique();
        $table->unsignedBigInteger('user_id')->nullable();
        $table->string('ip_address', 45)->nullable();
        $table->text('user_agent')->nullable();
        $table->text('payload');
        $table->integer('last_activity');
    }); 
}

ただ、残念ながら上記で生成されたファイルをそのまま実行してもエラーとなります。
問題を起こすのは以下の処理です。

$table->string('id')->unique();

発生するエラーは以下の通り。

SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table `sessions` add unique `sessions_id_unique`(`id`))

Laravel6では「config/database.php」内でデフォルトで指定されている「charset」が「utf8mb4」(絵文字使用可能)になっています。テーブル内で扱う各種文字列に絵文字が使用される可能性を想定した場合、この方針は踏襲したいところです。
しかし、この場合1文字が最大4バイトになります。先に示した問題の処理では単に「string(varchar)」と指定しているだけなので、文字数は255文字を想定することになりますので最大1020バイトのデータが入力されることを想定する必要が生じます。
一方、MariaDB(少なくとも5.5.68)ではUNIQUEキーを付加したカラムの最大長は767バイトまでとなっているため、前述のように「長すぎるよエラー」となっている訳です。

上記テーブルのIDに設定される値はセッションIDであり767バイトを超える値が設定されることはなさそうなので同カラムの長さのみ調整すると言う方法もありそうですが、根本的に本テーブルのcharsetがutf8mb4である必要自体がないように思われます。
セッションデータは「payload」と言う1つのカラムにまとめて格納されますが、これは関連データを多次元連想配列としてまとめたものをbase64エンコードしたもののようです。よってセッション上に絵文字が存在しても「payload」に格納される段階ではbase64で扱う64種類の文字のみで構成される文字列に変換されているため、本テーブルとしては絵文字の使用を考慮する必要がない訳です。

と言うことで、デフォルトのcharsetはutf8mb4のまま、本テーブルのみcharsetをutf8とし、関連して照合順序(Collation)も「utf8_unicode_ci」にしたいと思います。
同処理を含んだ設定関数の内容は以下になります。

public function up()
{   
    Schema::create('sessions', function (Blueprint $table) {
        $table->charset = 'utf8';
        $table->collation = 'utf8_unicode_ci';

        $table->string('id')->unique();
        $table->unsignedBigInteger('user_id')->nullable();
        $table->string('ip_address', 45)->nullable();
        $table->text('user_agent')->nullable();
        $table->text('payload');
        $table->integer('last_activity');
    }); 
}

これでマイグレーションを実行してもエラーが出なくなります。

.envの設定

前述のテーブルを作成した上で、Laravelとしてセッションデータの保存先をDBとするよう指定し直します。
具体的には「.env」内で「SESSION_DRIVER」として「file」と指定されている箇所を「database」に変更します。

SESSION_DRIVER=database

Laravelとしてのセッションデータの保存先変更操作は実はこれだけ。
テーブル作成でゴチャゴチャしたことと比べれば呆気ないです。

動作確認

上記までの作業(テーブル構築と設定変更1箇所だけですが)ができたら実際にセッションを使用するような処理を行ってみます。代表的なところとしては認証処理辺りでしょうか。まぁ、お好みで。
実行後に先ほど作成したセッションデータ管理用のテーブルを見るとそれらしいレコードが生成されていることが確認できると思います。

環境構築が実に簡単で、ファイル以外の保存先としては魅力的な選択肢です。
ただ、性能(処理速度)がやはり気になるところ。途中で触れたようにbase64エンコードなども行っていますし、他の方法(ファイル、memcached、Redis)と比較して重いんだろうなぁ、と言う印象は否めません。現時点で細かく計測した訳ではありませんが、ChromeのDevToolで見た印象でもファイルの場合と比較して若干遅くなっているような気がします。
しかし、そもそもセッションとは、本来は相互に独立したHTTPリクエスト+HTTPレスポンスの複数の実行において先の実行でサーバ内に発生したデータをクライアントを経由せず(セッションID=cookieの送受信のみで)次回以降の実行に引き継げるようにする管理や制御であり、そのような制御にどの程度依存するかと言う点も考えてみるべきかもしれません。
今までの例から考えるとフォームからのデータ入力において途中に確認画面を挟む場合、一旦セッションにデータを保存しておいて確認画面表示、OKであればセッションからデータを取得して改めて必要な処理(DBへの登録等)を行い、フォーム画面に戻る場合は直前の入力内容の復元のためセッション上のデータを用いる、と言うようなことをしているケースが多いと思います。しかし、昨今ではVue.jsのようなjavascriptのフレームワークを使用することで上記のような処理過程をフロントエンドに集約することができるようになり、バックエンドとしては最終的に確定したデータが渡されてきて、それをその場で処理するのみと言うような作業分担も増えて来ているように思います。このようなケースではデータをセッションに保持する必要が生じません(事前に認証操作を経ていれば、その状態管理にセッションを使用したいくらいでしょう)。

本記事の主題はセッションデータの保存先の変更であり、それはセッションデータの必要性を前提とするものです。その結果到達した方式の弱点をセッションへの依存脱却で正当化するのはなんだか逆説的ですが、要は性能も依存度も程度問題と言うことかと思います。
その辺の検証は今後の課題と言うことで。