PHPでセッション処理が遅いのはGCのせいかもしれない

f:id:hacktk:20181026154725j:plain

PHPでWebサイトやWebシステムをつくるとき、セッションを使うと思います。
そしてセッションストレージにはRedisやMemcachedを使うのが現在の主流ですが、MySQLやPostgreSQLなどのRDBに保存しているパターンもまだまだ多いのではないでしょうか?
今回は、セッションストレージにRDBを使用している場合に、レコード数が増えるとレスポンスタイムが非常に劣化する問題を調べたときの話です。

このときの状況

冒頭のとおり、セッションストレージにRDB(MySQL)を使ったWebサイトです。(クライアント側ではcookieでセッションIDを保持しています)
セッションの有効期間をかなり長くとっており、新規アクセス数が増えるにつれてRDBのレコード数もどんどん増えていきました。
そしてサービスインから数週間たったころ、レスポンスタイムがかなり劣化しているというアラートが発報されたのです。
そこで原因を調べたところ、たまにセッション周りの処理で時間がかかっているということに行き着きました。

DBに発行されるクエリと速度

さてこのケースにおいて、1回のリクエストでDBのセッションテーブルにどんなクエリが発行されているのでしょうか。
※ 前提として SessionHandlerInterface を使用していることを想定しています。
※ セッションテーブルに貼ってあるindexはprimary keyのものだけです。

  1. SessionHandlerInterface::read でセッションデータを読み出す
  2. SessionHandlerInterface::write でセッションデータを書き込む

実は基本的にはこの2つだけです。
1はセッションIDをキーとしたselectで、非常に高速です。
2はcookieにセッションIDが含まれている場合はupdate、そうでない場合はinsertです。
計測してみましたが、select,update,insertどれも単体ではそれほど遅くありませんでした。
もしかするとinsertやupdateでロックされて他のリクエストが遅くなっているのかとも思いましたが、MySQLのInnoDBは行ロックなので今回のケースでは関係ありませんでした。

どれも遅くないとすると、たまに遅くなるこの事象の原因がわかりません。

GC処理

ところでセッションには有効期限を設定するかと思います。
有効期限を過ぎたデータは SessionHandlerInterface::gc により削除されるわけですが、この処理はいつ実行されるのでしょうか。

SessionHandlerInterface::gcの説明 には、 session.gc_divisor、 session.gc_probability および session.gc_maxlifetime の設定に基づいて とあります。
session.gc_maxlifetime は有効期限です。タイミングに関係するのはsession.gc_divisor と session.gc_probability で、毎回のセッション開始時に gc_probability / gc_divisor の確率でgcが実行されます。
gc_probability=1, gc_divisor=100 の場合は1%の確率ということですね。

このgcが実行されたときのクエリを計測したところ非常に遅かったのですが、これは有効期限を保持しているフィールドにindexが貼られていなかったことが理由でした。
なのでindexを貼って解決、でも良いのですが、そもそもこのgcはそんなに頻繁に実行されるべきものなのでしょうか?

デフォルトのsession.gc_divisor, session.gc_probabilityの値では1%の確率で実行されるので、たとえば毎秒100リクエストあるWebサイトでは毎秒gcが走ることになります。
もちろんケースバイケースなのですが、対象のシステムによってgcの頻度を調節するべきかなと思います。(極端な例だと、gc_probabilityの値を0にすることでgcが実行されないようにし、cronなどでgcを定期実行する方法もあります)

ちなみにLaravelではsession.phpのlotteryに設定する配列で調節できますので、お試しください。