こんにちは、内定者アルバイトの浅野です。普段は、スマホアプリ「モンスターストライク」(以下モンスト)のサーバサイドの開発に携わっています。今回は、モンストの開発環境をInnoDB memcached pluginを使って改善した取り組みについてご紹介します。
開発環境におけるタイムトラベラー機能
モンストでは、次のイベントで利用するデータなどを確認するために、テスト用の環境において管理ツールでタイムトラベル機能(別名 タイムトラベラー)を提供しています。
タイムトラベルの機能は、キャッシュとRubyのGemであるtimecopを利用することで提供しています。
タイムトラベルがどのように動作するか説明します。
管理画面よりタイムトラベルしたい日時をセットすると、キャッシュにタイムトラベルする日時がセットされます。このキャッシュを使ってTimecop.travel
を実行することで、任意の日時にすることができます。タイムトラベルを終了すると、タイムトラベラーによりセットした日時が書かれたキャッシュが削除されるような仕組みになっています。
また、タイムトラベルはサーバ全体で日時をずらすことで、全ユーザがタイムトラベルする全体タイムトラベラーと、ユーザが個別に指定した日時にタイムトラベルできる個人タイムトラベラーがあります。個人タイムトラベラーは、個人を特定するIDと、タイムトラベルしたい日時をキャッシュに載せることで実現しています。
モンストでは、ユーザのデータなど、多くの場面でキャッシュが活用されています。つまり、タイムトラベル中には未来もしくは過去の時点でユーザのデータなどに関するキャッシュが作成されます。それでは、タイムトラベルを終了するとどうなるでしょうか?タイムトラベラーに関するキャッシュが削除されますが、タイムトラベル中に作成されたキャッシュは削除されることはありません。
このため、タイムトラベラーに関するキャッシュと、その他のキャッシュの不整合が生じることになります。これを避けるために、タイムトラベラーを行う際は事前にキャッシュをクリアし、かつタイムトラベラーを終了する際もキャッシュをクリアしていました。
全体タイムトラベラーを使用する際には、使用前および使用後でキャッシュのクリアを必須として問題ありません。これは、すべてのユーザが未来もしくは過去の時点のキャッシュを作成しているため、キャッシュを削除しなければ正しく動作の確認ができないからです。
一方個人タイムトラベラーでも、全体タイムトラベラーと同じようにキャッシュをクリアすれば問題ありません。しかし、キャッシュの空間は全ユーザ共通であるため、誰かがタイムトラベラーを使用した際に毎回キャッシュを全部クリアすることは、その時点でタイムトラベルしていない他のユーザのキャッシュなどもすべて削除せざるを得ないことになります。このため、テストやQA業務に影響していました。
これを防ぐために、ユーザごとにキャッシュの空間を分離する必要がありました。キャッシュの空間を分離するには、次のいずれかの方法が考えられます。
- すべてのキャッシュにおいてユーザごとにキャッシュを分離する
- 個人タイムトラベラーを利用しているユーザだけ、ユーザごとにキャッシュを分離する
前者の方法は実装のコストが低く、すぐに実現できる方法になります。しかし、この実装を行うと、本番環境では行っていないユーザごとのキャッシュ空間の分離を常に行うことになります。これでは、テスト環境の意味がなくなるため、あまり行うべきではない方法です。
後者の方法は、個人タイムトラベラーを使用したユーザに対して、タイムトラベル中のみキャッシュの空間を分離するという方法です。こうすることで、タイムトラベルを行わないときおよび、全体タイムトラベラーを使用している間は本番と同じ動作になり、個人タイムトラベラーを使用している間だけはキャッシュの空間を分離するということができます。今回はこちらの方法を採用することにしました。
キャッシュ空間の分離
キャッシュの空間の分離自体には、モンストが採用しているmemcachedクライアントである、Dalliで利用できるnamespaceを利用します。namespaceは、キャッシュのキーのプレフィックスを指定することができるオプションです。これにIDを付与すれば、簡単に個人でキャッシュの空間を分離することができます。
キャッシュの空間を分離することは実現できましたが、キャッシュのクリアについてはどうでしょうか?プレフィックスを元に、キーを検索して削除すれば良いということが思いつきます。しかし、memcachedはキーの一部から検索して列挙したり、削除することが難しく、実現することができません。また、開発環境のためにmemcachedから移行するわけにもいきません。
分離の問題は開発環境のみの問題であるため、これを解決する方法として、開発環境では、memcachedプロトコルを利用できる、memcachedではないものを採用することにします。
InnoDB memcached Plugin + MySQL
そこで、memcachedプロトコルを利用できるMySQLを利用します。MySQL 5.6から、InnoDB memcached Pluginが導入されました。InnoDB memcached Pluginは、MySQLでNoSQLを実現するために導入されたもので、memcachedプロトコルを利用できながらも、内部では通常のMySQLと同様にデータを扱える、InnoDBで利用できるプラグインになります。
セットアップが終われば、データについてはmemcachedプロトコルと同様に扱うことができます。もちろん、MySQLから該当のデータベースにアクセスすれば、データの削除や挿入も行うことが出来ます。
開発環境においてはキャッシュにInnoDB memcached pluginを導入したMySQLを使うことで、キャッシュ周りに大きく変更を加えることなく目的を実現できます。
個人タイムトラベラーを利用する際は、namespaceがついたデータを通常のmemcachedプロトコルで追加します。一方で、戻ってくる際はSQL文を発行して、namespaceがついているレコードを削除すればよいということになります。これで、キャッシュの空間を個人で分離することができました。
運用における注意
InnoDB memcached Pluginは、memcachedプロトコルでmemcachedと同様に扱うことができますが、運用する際にはいくつかの注意が必要でした。
KeyとValueを許容するサイズ
セットアップの際、注意するべきこととしてはKeyとValueに対するスキーマです。memcachedのKeyは250バイトまでということと、標準のセットアップスクリプトではValueが1024バイトまでしか許容していないということがあります。厳密に運用する場合には、利用しているmemcachedのクライアントの設計に合わせる必要があります。今回は、標準のセットアップスクリプトから、Keyであるc1
をVARCHAR(250)
とし、c2
をTEXT
としました。
Expireしないキャッシュ
memcachedでは、挿入するキーに有効期限を設定することができます。もちろん、Dalliからも有効期限をつけてデータが挿入されます。しかし、実際に動作を確認してみると、Dalliで有効期限をつけて挿入したデータの有効期限が無視されてデータが残り続けることがわかりました。
memcachedは、指定されたメモリのサイズが溢れそうになると、自動的に削除されます。しかし、InnoDBの場合はデータを自動的に削除するという仕組みはついておらず、ディスクフルになるまで増え続けます。
有効期限自体は問題なくc5
というカラムに保存されるため、もし自動で削除するようにしたければ、cronなどで削除スクリプトを定期的に実行することで対処できます。
まとめ
InnoDB memcached Pluginを用いて、memcachedプロトコルでもSQL文を用いて強力な検索などを行うことができることがわかりました。また、これを活用してキャッシュの空間を分離することができました。これにより、開発環境におけるキャッシュの全クリアを大幅に減らすことができました。
また、実際に記事の通りに動かすには、アプリケーションがどのようにキャッシュしているかについて十分に調査し、動作を変更するため、少し時間がかかりました。本番環境と開発環境についての差が生まれてしまうことになるので、一通りのドキュメントを作るなど、開発以外にも経験することが多く、貴重な経験になりました。