事の起こり
先日、とあるECサイトにて、タイムセールなどのバーストトラフィック発生時に、オンライン処理が遅延するという問題が発生し、お客様からまかせいのうに相談が持ち込まれました。さっそく商用環境から遅延発生時の性能情報を取得し、ボトルネックがどこにあるのか突き止めるための解析を実施したところ、奇妙なことが判明しました。バーストトラフィック発生の瞬間ではなく、バーストトラフィックが発生してからおよそ20分後に一斉にオンライン処理が遅延していたのです。
バーストトラフィックを受けた瞬間に、オンライン画面の処理が遅延するのは珍しい話ではありません。CPUやメモリといった物理的なリソースが枯渇したり、コネクションプールやスレッドの不足といった原因でもスローダウンは発生します。しかし、高負荷の時間帯を乗り切った後に、タイムラグをもって遅延が発生するというのは珍しいケースでした。
データベースを解析した結果、定期実行されるセッション情報の削除バッチがユーザのセッション情報を格納するテーブルに対して、ロック競合を発生させることで、オンライン処理の遅延が起きていました。
RDBMSに持たされているセッション情報
このケースでの遅延発生のメカニズムをまとめると、
- バーストトランザクション時にセッションテーブルが巨大化し、
- オンライン処理と、裏で定期実行されるセッションクリアバッチが競合し、
- オンライン処理が長時間にわたってロック待機で待たされる
という流れでした。
RDBのレコードに対するロック待機が原因の遅延は、小手先のDBチューニングでは解消が難しく、「ロック粒度を極力小さくする」「バッチ実行スケジュールを変える」といったアプリケーション変更や運用対処といった、実行コストが高く苦しい対処を迫られることがしばしばです。
このケースではアプリケーションが、画面遷移のたびにアクセス日時やreferer情報を更新するという作りになっていました。これはセッションテーブルに対し、極めて高い更新負荷を与えます。
インメモリデータストアが効果的
このケースのような性能特性を持つシステムの場合、そもそも、どのようなアーキテクチャにしておくべきだったのでしょうか。
結論から先に書くと、今回のケースではセッション情報を保持するストアとして、RDBではなくNoSQLの一種である「インメモリKVS(キーバリューストア)」を採用するのが最適だったと思われます。それは、セッション情報が次のような条件を満たしいているからです。
- データ構造が単純
- 永続的に保存する必要がない(揮発性が高くてよい)
- 参照・更新頻度が高い
一般に、セッション情報のデータ構造は、非常に単純な作りをしており、以下のようなキーに対して数個の要素がひもづくシンプルな配列の形で表されます。
データ構造が「単純」ということはつまり、正規化されたデータのように複数のテーブル間での関連が存在せず、かつレコード同士の間にも関連性がないということです[1]。一般的なKVSは、正規化された複数のテーブルを結合したりデータを集計したりといった、SQLのようなリッチな表現力は持っていませんが、こういう単純なデータ構造ならば問題なく扱うことができます。
次に、セッション情報というのは永続的に保持する必要のないデータです。ユーザがログアウトしたり一定時間経過してタイムアウトすれば、寿命を終えます。同じユーザでも再度ログインすれば別のセッションIDが割り振られるため、再利用されることもありません。したがって、ディスクなどの永続的ストレージにわざわざ保存する必要性は低いことが多いのです。7年の保存が義務付けられる企業の会計情報のような基幹データとは異なる点です。また、データをセットする際にexpire[2]を設定できるため、RDBの際に必要となる「セッションの刈り取りバッチ」の実装も不要になります。
そして最後に、永続性がなくてよいということは、データをメモリ内(インメモリ)で持つことができるということです。これによって、RDBの際に必要だったディスクI/Oを省略することが可能となり、処理の高速化を図ることができます。
インメモリKVSの製品
本稿のケースに向いているNoSQL製品としては、「memcached」または「Redis」といったインメモリKVSが考えられます。どちらもメモリ上で完結して動作するため、セッション情報のようなシンプルデータに対するアクセスを高速化するにはうってつけです。しかし、揮発性の高いメモリで処理を行うということは、耐障害性の観点で問題を発生させることになります。KVSのプロセスがダウンすれば保持したデータが消えることになるからです。
memcachedとRedisは、この問題に対して異なるアプローチをとっています。memcachedの場合、memcachedプロセスをレプリケーションさせて可用性を高める「repcached」(KLab社が開発)のような手段により、可用性を高めます。
一方、メモリ上のデータをディスクにバックアップする(bgsave)という戦略を取るのがRedisです[3]。この機能を利用する場合、メモリ上に保持しているデータをバックアップする際に、fork()
してデータを2重で保持することになるため、保存するデータサイズのほぼ2倍の物理メモリを確保しておく必要があります[4]。とはいえ、保持するデータが比較的小さいことと、昨今のメモリ単価の低下を考慮すれば、これはそれほど大きな障害にはならないでしょう。このほか、ディスクにデータをコピーする際にはかなりディスク負荷を高くするため、物理的に分離されたストレージを利用するなどの注意も必要です。
また、一口にユーザに紐づくデータといっても、「ログイン日時」のような、それこそ生存期間が数時間程度というデータもあれば、「お気に入り商品」や「音楽のプレイリスト」といった、ある程度の永続性を持たせたいデータもあります。「7年保存」ほどガチガチではないが「緩い永続性」が欲しい、かつデータ構造も比較的単純という、このような場合はRedisはフィットするでしょう。
最後に簡単にまとめると下表のような感じになります。
製品名 | DBタイプ | 処理速度 | ディスクI/O | データ永続性 | 備考 |
---|---|---|---|---|---|
memcached | KVS | ◎ | なし | △ | レプリケーションにより可用性向上 |
Redis | KVS | ◎ | なし | ○ | ディスクにバックアップを保持できる |
MySQL、Oracleなど | RDB | △ | あり | ◎ | ディスクにデータを持つためI/Oがボトルネックになりやすい |
おわりに
セッション情報をインメモリで保持して高速化を図るというのは、KVSの利用シーンとしては一般的なものです。しかし、セッション情報に限らず、上述のような条件を満たすデータであれば、同じような手段で高速化を行う余地があります。反対に、データ同士に複雑な関連が存在している場合や、更新時にデータ間の整合性を考慮する必要がある場合は、やはりRDBとSQLの出番です。
これから新規のシステム基盤構成を考える場合は、従来型のRDBMSとKVSの特徴を捉え、両者をうまく組み合わせたシステム構成を検討することが必要になるでしょう。
[3] Redisはバージョン3.0でクラスタリングを実装したため、今後はRedisにおいてもクラスタ構成によって可用性を高める手段も選択肢になってくると思われます。
[4] COW(Copy On Write)なので親プロセスと子プロセスで共有される領域があり、厳密には2倍以下にはなりますが、安全のため2倍で見積もるのがよいでしょう。