カカリアスタジオブログ

Happy Elementsのゲームブランド「カカリアスタジオ」の公式ブログです。

fluentdとMongoDBでユーザー行動を見える化

はじめに

エンジニアの@ryooo321です。
よろしくお願いします。
今回は弊社で運用中の全アプリで利用している行動分析プラットフォームについてご紹介したいと思います。
2012年の6月に作ってから、約9ヶ月ほど運用しています。

特徴

・手がかからないデータストア
・さまざまな問い合わせ対応で利用できる柔軟なクエリ
・機敏なMap/Reduceによる集計
・集計結果をCSVやグラフで可視化

目的

・ユーザーの問い合わせに効率的に対応し、アプリの企画・開発に集中するため
・ユーザーの行動を抽象化・可視化することでPDCAの質を向上させるため


行動ログのフロー

1. ユーザーからRuby on Rails製のソーシャルゲームにリクエスト

2. Railsからローカルのfluentdにログ出力(fluent-logger-ruby)

3. ローカルのfluentdから行動分析プラットフォームのfluentdにログ転送(fluentd)

4. 行動分析プラットフォームのfluentdに集約後、MongoDBにinsert(fluent-mongo)

5. MongoDB上でMap/Reduce集計し、集計結果をMongoDBに保存

6. 集計結果をCSVやグラフ出力



目次

1. アプリについて

2. fluentdの利用

3. MongoDBの利用

4. 行動をミクロな視点でみる

5. 行動をマクロな視点でみる

6. fluentdとmongo連携でのはまりどころ



1. アプリについて

現在メインで担当しているアプリは、全webサーバーで6,300,000req/dayほどの規模です。
アプリはRuby on Railsで作っており、Cassandra + MySQL + Memcacheを組み合わせて利用しています。



2. fluentdの利用

fluentdはイベントログの転送をするためのツールです。
起動やログを出力する方法は公式githubでご覧ください。

fluentdにはさまざまなプラグインが用意されていますので、
多種多様な言語・構成・目的を楽に乗り越えて活躍してくれるプロダクトだと思います。


起動/死活監視

GODを使っているのですが、fluentdは高負荷がかかっても落ちず高い可用性を発揮してくれます。


出力タイミング

サーバーのログからtailしつつ収集する方法もありますが、弊社ではRailsアプリから直接fluentdにログを送っています
Rubyのhashの形式のままfluentdを通じてMongoDBに保存できます。
fluentdは高負荷や落ちている場合でも処理をブロックしないので、アプリには影響が出ません。

弊社の場合Google analyticsも利用しているので、リクエスト統計などはそちらで収集しています。


出力するログの例

ユーザーのリクエスト毎(主にpost/delete系)に、下記のようなログを0〜数件出力しています。
document:
  _id: yyyymm_user_id_xxxxxxxxx
  user_id: xxxxxx
  category: ログ種別(user/payment/card/mission etc..)
  type: ログタイプ(in/out/use/get etc..)
  platform: xxx
  request:
    page: payment/finish
    viewer_id: xxxxxx
    request_id: 同じリクエストで複数ログを出すときは、先頭のログの_idを入れる
  device: xxx
  info:
    ここにログ種別ごとの詳細な情報をhashで出力
  condition:
    ログ出力時点でのユーザーの状態をすべてのログで出力
    level: xxx
    st: xxx
    bp: xxx
    max_st: xxx
    max_bp: xxx
    gacha_pt: xxx
    cards_count: xxx
    friends_count: xxx
    leader_card_id: xxx
    training_card_id: xxx
    joined_at: xxx
  time: ログ出力日時(fluentdが付与)
このようなログが1,000,000 rec/dayほど出力され、fluentdによりMongoDBに保存されます。



3. MongoDBの利用


Capped collectionを利用

MongoDBにはCapped collectionという固定サイズのコレクションを持つ仕組みがあります。
・固定サイズでアロケートされた領域にデータを書き込むため、通常のinsertより高速。
・固定サイズまでデータが溜まった場合、古いデータから自動で削除されていきます。

メリット

弊社ではこの仕組みを使うことで、チャンクの監視・手動のシャーディングを行わないで済むようにしています。
Capped collectionを使うことで運用から手が離れることのメリットが大きいです。

デメリット

一方で決して小さくないデメリットもあります。
・Capped collectionではデータのシャーディング(水平分割)ができませんので、Map/Reduceの分散処理の利点が十分に活かせません。
現在、1,000,000ほどの日次レコードを150ほどのMap/Reduceスクリプトで分析するのに、約30〜45分ほどかかっています。
レプリカセットという仕組みでの分散はできるのですが、MapReduceの利用には制限がかかります。(結果保存できずinlineでしか使えない)

・古いデータが削除されてしまう。
前述のログで600byte/レコードです。1日1,000,000レコードで0.6GBほどになります。
現在、100GBのCapped collectionを用意しているので、5ヶ月ほどは消えません。

また、古いデータが削除されてしまうため、MapReduceによる過去分の集計はできなくなります。
しかし、日次のMapReduceの結果を保存しておき、その結果は5ヶ月以上残し、MapReduceの結果に対してMapReduceで集計することで過去分の集計ができます。
そのため、データの持ち方や集計フローの設計が非常に重要になります。



4. 行動をミクロな視点でみる

行動調査

MongoDBでは、user_idとtimeにindexを貼っています。
これにより、ユーザーの行動は時系列で並べて調査できます。


出力したログはリクエストや対象モデルごとに絞り込みができるほか、json形式のログの全内容を確認することももちろん可能です。


行動ログの出力タイミング

ログはリクエストの最後にまとめて出力するのではなく、CassandraやMySQLに対してデータを処理した直後に都度出力しています。

これにより下記のようなメリットがあります。
スキーマのデータ構造をシンプルに保て、検索や集計をシンプルに行えます。
・エラーなどによる出力漏れの可能性がほぼなくなりログの信頼性が大きく向上しました。



5. 行動をマクロな視点でみる

Map Reduceで集計

MongoDBのサーバー上にjavascriptで書いたMapReduceスクリプトを配置し、バッチで全scriptを実行しています。

共通ファンクション群のロード

下記のようにすることで、いくつかの便利なファンクション群をMapReduceスクリプトで使えるようにしています。
load(pwd() + '/map_reduces/lib.js')

MongoDBのmapReduceメソッドのscopeオプションを渡すことでこのメソッド群をmapファンクションやreduceファンクションでも利用できます。

集計の元ドキュメント

{
  category: 'payment',
  info: {
    price: 100,
    item_id: 20,
    count: 1
  },
  time: xxx,
  condition: {
    level: 10,
    joined_at: xxx,
  },・・・
}

一次集計されたドキュメント

_idが同じもののvalueを足し合わせてくれる単純なMapReduce集計を利用し、元ログを下記のように一次集計します。
一次集計することで、重い元データの集計を最低限にしています。
// 日付・購入物・属性の組み合わせごとに集計された状態です。
{
  _id: {
    ymd: '20130318',
    item_id: 20,
    level: 10,
    play_term: 20,
  },
  value: {
    count: 1,
    price: 100,
    arppu: 100,
  }
}

商品ごとの集計結果

一次集計した結果を元に、下記のように再集計します。
// 日付・購入物ごとにいくらの売上があったかの集計です。
{
  _id: {
    ymd: '20130318',
    item_id: 20,
  },
  value: {
    count: 1,
    price: 100,
    arppu: 100,
  }
}

プレイ日数ごとの集計結果

一次集計した結果を元に、下記のように再集計します。
※ レベルやプレイ日数は1単位ごとに集計してもよいのですが、あまり意味がないので5や10ごとに丸めて集計しています。
// 日付・プレイ日数ごとにいくらの売上があったかの集計です。
{
  _id: {
    ymd: '20130318',
    play_term: 20,
  },
  value: {
    count: 1,
    price: 100,
    arppu: 100,
  }
}

このような形で、さまざまな切り口での売上、商品のin/out、ユーザー数など、150ほどの集計を行っています。


さまざまなグラフによる可視化

日次で集計した結果を毎日必要なものをグラフやCSVで取得できます。




ルール

MapReduceした結果を簡単にCSVで出力したりグラフ化するに当たって、データを汎用的に取り扱うためいくつかのルールを決めました。
・_idとvalueの値は一次元のハッシュとすること
このルールにより、CSVやグラフの出力をシンプルに行えています。

・_idに必ずymdを持つこと
CSVの出力期間はymdでフィルターし、グラフ化のX軸はymdとし、Y軸はymd以外の_id値としています。
valueに複数のキーがあっても、valueのキーごとにグラフを出力します。
集計は日次単位でしか行っておらず、過去分の分析は通常行っていません。
※ 先ほどの例だと、X軸がymd、Y軸がプレイ日数、値がpriceのグラフなどが見れます。



6. fluentdとmongo連携でのはまりどころ

・fluent-mongoでMongoDBにデータを流すにあたって、MongoDBのRubyドライバでBSONエンコードできないデータを流すと「__broken_data」として登録されます。
例えばハッシュのキーがintだと「__broken_data」になります。

・fluentdからCapped collectionに流す
collectionに流す際は、fluentdの設定ファイルで「capped」指定と「capped_size」指定が必要です。



一緒に働きたい方、絶賛 募集中

京都でスキルアップしたいエンジニアの皆さん、ご応募お待ちしています!
京都でスキルアップしたい学生さん、アルバイトも可能なのでご応募お待ちしています!
大阪、滋賀、神戸から通勤実績あり


以上、長文にお付き合い下さいましてありがとうございました。
© Happy Elements K.K