初めに
こんにちは。CYBIRDエンジニア Advent Calendar 25日目に再登板の@daisuke-senmyouです。24日目は@koki_yamadaさんのUnityのコード編集にVimを使ってみたでした。今の@koki_yamadaさんと同じエンジニア1年目の時、自分は秀丸でコーディングしてたなーと懐かしんでいたら、未だに(たまに)秀丸使ってました。
今回書くこと
お陰様で弊社の恋愛ゲームもたくさんのユーザー様に遊んでいただいており、当然RDBの負荷を分散させるためにインメモリDBなどにデータをキャッシュさせるという方法が必須になっています。今回は技術面というよりも、なにをどうやってキャシュし、また不整合が起きないように更新しているのかという戦略面について書いていこうと思います。
恋愛ゲームで扱うデータ
まず弊社の恋愛ゲームで扱うデータを分類わけすると、大きくは3つになります。それぞれについてキャッシュの仕方が異なりますが、今回の主題はマスターデータです。
- リソースデータ
- 画像・音声・Unityのプレハブ などのバイナリデータ
- トランザクションデータ
- ユーザーの操作によって常に変動する動的データ
- 体力・所持アイテム・親密度(経験値みたいなもの)など
- マスターデータ
- ゲーム内で共通の静的データ
- カードであればその名称・レア度・魅力値(戦闘力のようなもの)など
リソースデータ
最も容量の大きい(一式で数百MB)データで、全てCDN(CloudFront)にキャッシュさせています。こちらはRDBとは無関係なので今回の本題ではありませんが、通信量を最小限に抑えるための配慮はしています。
トランザクションデータ
更新頻度が高いためほとんどのデータがキャッシュできません。ただしランキングデータ・おすすめフレンド・PvPの対戦相手候補などは数分ごとの更新でも許容されるためキャッシュ対象としています。同時にこれらは全ユーザーのデータを集計対象とするのでRDBに負荷を掛けがちです。
キャッシュの手段
3つともRedisのソート済みセット型と相性が良いのでこれを利用しています。例えばおすすめフレンドはゲームの進行度合いが近いユーザーを選定したいので、進行度合いをスコアとして計算しソート済みセット型に全ユーザー分を放り込んでおきます。クライアントからおすすめフレンド表示のリクエストが来たら、対象ユーザーの前後にいるフレンドを取得して表示するといった具合です。
マスターデータ
主にインメモリDBのキャッシュ対象となるデータです。JSON形式で表現されるデータで容量は大きくありません(一式で数MB)。とはいえリクエストの度に複数のテーブルを結合して応答するのでキャッシュしなかった場合のRDBへの負荷は無視できません。
キャッシュの手段
こちらは普通にRedisの文字列型としてkey-valueでキャッシュしています。最初はキャッシュがない状態ですが、誰かがマスターデータ取得用のAPIをリクエストすると、そのレスポンス(JSON)がデータ種別毎にRedisにキャッシュされ、2人目以降はRedisのキャッシュを参照することになります。
また同時にRDBにも同様のkey-valueを格納しています。万が一Redisのキャッシュが揮発してしまった場合に比較的容易にキャッシュを再構築するためと、最悪、Redisで障害が起きて応答しなくなってもRDBだけでレスポンスできるようにという2つの意図からこのようにしています。実際にRedisが調子悪く、まれに応答できない時間帯が発生したのですがユーザーには(たぶん)気づかれずに縮退運転が継続できました。
クライアントへの通知の方法
マスターデータを更新して本番反映したタイミングで、マスターデータ全体のバージョン番号を1つインクリメントします。そしてそのバージョン番号のハッシュ値を全てのAPIのレスポンスヘッダーに含めています。クライアントはマスターデータが必要になったタイミングでこのハッシュ値を参照して、ローカルにあるデータを保存した際のハッシュ値と比較して古くなっていれば最新のマスターデータをリクエストします。
ハッシュ値を更新するとクライアントが一斉にマスターデータの更新を始めるのでWEB APIのトラフィックが急増します。下のグラフは最近ハッシュ値を更新した日のトラフィックの推移です。AM11:10頃にハッシュ値を更新してからトラフィックが急増し、そのまま深夜をすぎるまで通常の数倍のトラフィックになっています。
これだけトラフィックが増えているにも関わらず、AM11:10前後におけるRDBの負荷はほとんど変動がありませんでした。これはRDBの負荷を上手くRedisに分散できていることを表しています。
※18:00にスパイクしているのはイベントのフィーバー時間帯が始まったためです。
キャッシュ更新
key-value の key には上記のハッシュ値も含めていて、これがキャッシュ自体のバージョン番号となっています。現在のキャッシュを削除してから新しいキャッシュを再構築とすると、再構築中のリクエストに応答できなくなってしまうので、新しいハッシュ値をkeyに含むキャッシュが構築完了してから最新のバージョン番号を表わす部分を書き換えます。
Capistrano がデプロイの最後にシンボリックリンクを書き換えるのと似た流れになっています。
注意点
有効期限
不要になったキャッシュがいつまでもRedisに残り続けないように、1日1回の頻度でキャッシュが expire されるようにしています。有効期限は、念のためトラフィックの少ない早朝時間帯にしています。さらに念のために、データ種別ごとに時間帯を少しずつずらして有効期限を設定することで、キャッシュ再構築のためのRDBへのリクエストが集中しないように配慮しています。
ユニットテストの際のキャッシュクリア
あえてRedisキャッシュを有効にした状態でユニットテストを行っている部分がありますが、たまに古いキャッシュが邪魔をしてテストが落ちることがあります。自動テストの方は対策済みですが、作業PCで手動でユニットテストを回す場合などはキャッシュクリアを忘れないようにする必要があります。
CI環境のRedisはdocker化する。
当初、共通のRedisサーバーに対して並列処理でユニットテストを回していたのですが、タイミングが悪いとキャッシュ更新が同時に行われてしまいユニットテストが落ちることがありました。現在はユニットテストのスレッド毎にdockerでRedisサーバーを起動しているので解消しました。
デバッグユーザーはキャッシュを参照しない。
ユニットテスト同様、デバッガーさんも古いキャッシュを見てしまいバグとして誤検知されることが度々あります。予めデバッグユーザーとして登録されたユーザーに対してはキャッシュは参照せず、常にRDBから最新のデータを参照するような仕組みにしています。
最後に
さて、CYBIRD エンジニア Advent Calendarいかがだったでしょうか。最後になりますがお決まりの採用告知をしておきます。
このアドベントカレンダーからもにじみ出ているように、CYBIRD ではエンジニアも非エンジニアも、オジサンも若手も、新しいもの好きもレガシー好きも、みんな一緒になっていろんなサービスを提供しています。チームワークと職場の雰囲気の良さは間違いなく業界トップクラスだと思います。そんな CYBIRD に興味を持っていただけた方は、是非お気軽にエントリーしてみて下さい。
採用サイト:http://www.cybird.co.jp/recruit/
では、また来年!