こんにちは!hiroshiです!
今回は、最近DECOLOG界隈で大ブームのredisについて、その利用用途や導入方法についてお話ししたいと思います。
今回のお話と関連する過去エントリに以下がありますので、こちらに目を通していただいた上で本エントリを読んでいただくと分かりやすいと思います。
…と、これまでのエントリからは実運用できてるのかどうか微妙なタッチになっているかもしれませんが、結論からいうと実運用できてます!
「redis導入後にトラブル発生、そのレポート」ではTTLを設定した場合にうまくいかないケースがあったのですが、TTLなしのデータでは特に問題なく運用できました。
現在のredisの利用用途
今のところ以下の用途ですね。
- カウンターデータ
- 広告配信データ
- ユーザーのアクティビティ統計データ
最初に導入してみたのは、広告配信データでした。
なぜこれを選んだか、ですが、MySQL以外のツールを探し始めたきっかけが、readよりもwrite負荷、つまりmaster負荷が原因でスループットがあがらない処理がいくつかあり、そこをなんとかしたかったんです。
shardingすれば済むし、実際にそうやって対応していましたが、処理内容(カウントアップするだけ)や保持しているデータ(前述のデータはkey-value形式なデータで、valueは数値のみ)を考えると、なんとも「サーバがもったいない。富豪エンジニアリングになってしまってるなあ。」という感を強く持っていました。
※「富豪エンジニアリング」は必ずしも悪だとは考えてないです。念のため。
余談ですが、金をかければだいたいのことが解決するわけです。なので技術陣は他所からの要求をただ実現するだけでなく「低コスト」でやってナンボだと思っています。「サーバ増やせばできるよ」という結論も多々あると思いますが、「サーバを増やさずできる」方法を常日頃から考えておきたいところです。
話を元に戻します。前述の課題となっていたウチのデータ群では広告配信データがもっとも更新負荷が高いデータだったので、「ここがイケれば全部イケル」ということからここを選びました。
更に広告配信データは「DECOLOGでのMySQL Archiveエンジンの使い方」と同じスキームでやっていました。つまり
- archiveエンジンを使ってとにかくinsert
- 定期的にテーブルスワップし、スワップしたテーブルに対して集計プログラムを走らせる
- で、その結果を集計DBに格納
といった按配です。
広告配信データは、1日に1回のテーブルスワップだと半端ないデータ量になってしまうのと、運用側としてもimpressionなどの集計データの本日分が参照できないのはリアルタイム性にかけすぎるので、テーブルスワップのサイクルは1時間に1回でやっていました。
なので、「ここがイケれば全部イケル」という理由に加えて、集計スパンを既存に合わせれば、仮に導入試験中にredisが原因で未集計のデータがぶっ飛ぶようなことがあっても「MAX1時間分のデータがパーになる」というのが想定される最大の被害規模であり、それだったら基本的に内部以外に迷惑かけずに済む、という算段もありました。
設計
設計ってほどのことでもないですが、1台でどこまでいけるのか?というのを試したかったのでredisサーバは1台のみです。仮にぶっ壊れたりしたとしても、プログラムをを2,3行コメントアウトしてリリースすればよいですし。
ところでredisにはdatabaseという概念があります。これは数字で指定する、ちょうど配列の添字みたいなものです。この単位でデータは管理されています。なので、データのflushもこの単位でできます。で、これは上限があるのかどうかわからないんですが、任意の数を指定できます。この添字的なものをdbidと言うっぽいです。
データ構造のイメージとしてはこんな感じ。
[0] { array(key1 => value, key2 => value, key3 => value ・・・) }, [1] { array(key1 => value, key2 => value, key3 => value ・・・) } : : [n] { array(key1 => value, key2 => value, key3 => value ・・・) }
※設定ファイルに指定した数-1
広告は1時間ごとに集計したいので、この仕様を利用して、databaseの数を24にしておきます。こうするとDBは0~23の範囲を指定できます。
プログラムでも、この仕様に合わせる形を取っておきます。
<?php class My_Redis_AdViewLog extends My_Redis { const KEY_PREFIX = "ad_view"; function __construct($last_hour=false) { // redisのDBは時間(h)と同期をとる。つまり0~23の範囲をとる。 $time = strtotime(($last_hour ? "-1 hour" : "now")); $index = date("G", $time); parent::__construct(REDIS_SERVER_AD_VIEW, $index); } function logging($ad_id, $carrier) { $this->increment($ad_id."/".$carrier); }
これはいわゆるDBクラスですね。
parent::__construct(REDIS_SERVER_AD_VIEW, $index);
の$indexがdbidです。
現在時間(h)をdbidとして扱っているため、10時のデータはdbid=10のところに入っているわけです。
KEY_PREFIXは、keyの一部になります。ベースクラスで、このKEY_PREFIXを元に
{ENV}/{KEY_PREFIX}/{KEY}
というkeyを組み立てるようにしてあります。
$this->increment($ad_id."/".$carrier);
のincrementですが、redisにはincrというコマンドがあり、これがチョー便利です。
valueが数値だった場合に$deltaの数だけインクリメントしてくれる、というシンプルなものなのですが、何が便利って該当するkeyがない場合にエラーとしないで、key=$deltaを自動的に作ってくれるんです!
「キーが存在すればupdate、存在しなければinsert」みたいなコードを書かなくていいのはすごく気持ちがいいです。
こんな感じでデータを収集していきます。
24時間経つと、dbidが重複することになってしまいますが、1時間ごとにデータ集計バッチが走るので、そこで現在時間-nのDBをflushする、というコマンドを入れて対応しています。
導入に当たって
新しいものを導入するときのウチの基本なのですが、いきなり切り替えず、既存ロジックと平行させて導入します。
かつ、100分の1で新しい処理を通るようにする、みたいなコードを入れて、グラフを見ながら徐々に100%にあげていきます。
100%になれば、既存ロジックの集計結果と同じになるはずなので、それを確認します。
広告集計結果をストアするsummaryというテーブルがあるとすると、summary_testというまったく同じスキーマのテーブルを用意します。
redisで取得したデータの集計データは、summary_testに格納します。あとは、確認するだけです。
で、これがけっこうあっさりうまくいっちゃったので、苦労話とか特に無いです。いや、これをやること自体がそれなりに苦労するんですけど。
しかもこれまで7台でやっていたものを1台で受けきってかれこれ数ヶ月問題も起きていません。
感想
いやー、7台が1台になっちゃいました!
正確にいうと、7台で構成されていた既存DBは、actionlog、ランキングデータ、広告配信データが格納されており、いずれもそれぞれ別のredisサーバに分けられたので「7台が3台になった」が正しいですね。ただ、こう書くと約半分程度なので、ちょっとインパクトに欠けるかもしれません(?)が、この例ではないところで4台が1台にできたのは確認できています。
実際は、valueにオブジェクトも格納でき、それを利用しているところもあるのですが、がっちり使っているのはvalueに数値を持ち、カウントアップしていく性質のデータを対象としています。
っていうか、そういうデータだったらかなり最強の部類に入るのではないか、と思っています。
また、redisの開発はかなり活発です。あまりにも活発すぎて燃え尽きないか、と心配になるくらいです。
DECOLOGが使っているphpクライアントのphpredisの開発も活発です。
こういうのは多くの人が使えば、今後ももっともっと活発に活動し続けてくれると思うので、みなさん奮ってご利用ください!!
see also:「仲間大募集のお知らせ」
ではでは!