筋肉とエンジニアリングで すべてを解決するブログ

筋トレ、JavaScript、Ruby で世界を変えてやります。

Rails4 + Fluentd + MySQL でランキング機能作ってみた

f:id:ma3tk:20141013165016j:plain

アクセスランキングを作ってみよう!

今、Rails4 を使ってアクセスログからランキング作ってみたいなぁーと思ってたんですが、みんなどうやってんだろうって聞いてみたところ、 Fluentd ってのを組み込むだけで簡単に集計とかできるよーって話だったので、使ってみました。

Fluentd とは

まず読み方は、 「ふるーえんとでぃー」であって、「ふるーえんど」とかじゃないっぽいです。d はたぶんデーモンの d 。

色々説明面倒なんで省略しますが、ログ収集元とか出力先が簡単に設定できて、自分の欲しい形でログ保持できますよーって感じっぽい。なので、 「シェルスクリプトで処理した tail の処理結果をファイルに」「nginx から出力されたアクセスログを mongoDB に」とかそういう処理を自前で書く必要なく、簡単な設定だけでいけちゃう。

詳しくは → 柔軟なログ収集を可能にする「fluentd」入門 - さくらのナレッジ

今回の Fluentd

今回は Rails である処理が実行された時に、アクセスログApache だけじゃなく、MySQL に保持してランキング作りたい!ってのが要望。

調べてみると、 Rails + Fluentd + MySQL の設定で書かれた記事がなかったので、設定した備忘録としてログを残してみます。

前提

  • rails のアプリケーションを作ってて、実行環境がもうできてる
  • gem とか mysql あたりの設定ももちろん終わってる
  • zsh 使ってる(そうでなければ適宜 .zshrc とかを .bashrc に置き換えてください)
  • vim で編集できる(よね…)

おおまかな流れ

  1. 置き場となるデータベース/テーブルを作成する
  2. fluentd をインストールする (本体)
  3. fluent-plugin-mysql をインストール (出力の準備)
  4. fluent-logger を Rails に組み込む (入力の準備)
  5. 実行

1. 解析用のデータベース/テーブルを作成する

まずは、どんな情報が欲しいか考えるところから始まりますが、今回はページアクセスランキングを作るとしましょう。 ページID、アクセスした時間 の情報があれば、ミニマムなランキングが出来そうです。

# mysql に接続してデータベース/テーブルを作っていきます
mysql -u user_name -p

# データベース作る
> create database access_log;

# テーブルを作る
> CREATE TABLE `page` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `page_id` int(10) NOT NULL,
  `access_time` int(10) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

これで準備完了。

2. fluentd のインストール

これもあっさり終わると思います。

$ gem install fluentd
$ source ~/.zshrc

これだけで本体はインストールできて、パスが通ってるはずです。

3. fluent-plugin-mysql をインストール (出力の準備)

fluent-plugin-mysql を入れる

gem install fluent-plugin-mysql

これだけで終わりです。楽ちん。

Rails の DB 情報に設定した情報を追記

1 で設定したデータベースを設定する

$ cd /home/user/app/pageapp
$ vi ./config/database.yml

database.yml にデータベースの情報を追記(username,password,socketあたりは変更してください)

accesslog:
  adapter: mysql2
  encoding: utf8
  database: access_log
  pool: 5
  username: mysqlname
  password: mysqlpass
  socket: /var/lib/mysql/mysql.sock

fluent 用の conf ファイルを作成。

$ cd /home/user/app/pageapp
$ mkdir ./config/fluent

# fluent の config ファイルを作成する
$ fluentd --setup ./config/fluent

conf の修正

$ vi ./config/fluent/fluent.conf

fluent.conf の最下部に 以下の情報を追記します(database, keynames, sql, username, password あたりは変更してください)

<match page.access>
  type mysql
  host localhost
  database access_log
  key_names page_id,access_time
  sql INSERT INTO page (page_id,access_time) VALUES (?,?)
  username mysqlname
  password mysqlpass
  flush_interval 10s
</match>

これで Mysql へのアクセス準備が出来ました。

4. fluent-logger を Rails に組み込む (入力の準備)

インストール

fluent-logger は Rails のアプリの該当の箇所にアクセスされた場合に 「 fluentd にログを送って」、っていう指示を出すために必要になります。

rails の アプリケーションの場所まで飛んでください

$ cd /home/user/app/pageapp
$ vi Gemfile

vim 等で以下の 2行を Gemfile の適当な位置に追記します

# for access log by fluentd
gem 'fluent-logger'

インストールする

$ bundle install

これで fluent-logger が Rails アプリケーション内で使えるはずです。

具体的に コントローラーから呼び出して使ってみる

モデルを以下のように作成

$ cd /home/user/app/pageapp
$ vi app/models/access.rb
class Access < ActiveRecord::Base
  establish_connection(:accesslog)
  self.table_name = "page"
  self.primary_key = "id"
end

これで Rails から該当の DB を呼び出す準備が完了

一例としてページの情報を処理するコントローラーから 呼び出してみます

$ vi app/controllers/pages_controller.rb

例えば、 pages#view が呼ばれた時だけ fluentpost するようにすれば、id と時間が db に格納されます。

class PagesController < ApplicationController
  
  Fluent::Logger::FluentLogger.open(nil, :host=>'localhost', :port=>24224)
  before_action -> { fluentpost(params[:id]) }, only: [:view]
  
  # routes.rb で params[:id] 受け取るようにしてます
  def fluentpost(id)
    Fluent::Logger.post("page.access",{
      "page_id" => id,
      "access_time" => Time.current.to_i
    })
  end

実行

$ cd /home/user/app/pageapp
$ fluentd -c ./config/fluent/fluent.conf -vv &

問題なければ、これで実行されます。この状態で 各ページにアクセスしてみてください。

ページアクセス後、 MySQL から確認してみます。

$ mysql -u mysqlname -p -D access_log
mysql> select * from page;
+-----+---------+-------------+
| id  | page_id | access_time |
+-----+---------+-------------+
| 343 |       1 |  1413172076 |
| 344 |       5 |  1413172429 |
| 345 |       1 |  1413172438 |
| 346 |       1 |  1413172483 |
| 347 |       1 |  1413172735 |
| 348 |       3 |  1413172800 |
| 349 |       5 |  1413172803 |
| 350 |       5 |  1413173082 |
| 351 |       1 |  1413173177 |
| 352 |       3 |  1413173216 |
| 353 |       7 |  1413177045 |
| 354 |       1 |  1413177811 |
+-----+---------+-------------+
12 rows in set (0.00 sec)

問題があるときは、ログが挿入されていない / ログに赤字でエラーが出ると思います。

色々設定して間違ったのは、

  • fluent.conf ファイルの keynames がきちんと設定されていない
  • fluent.conf の sql 文が間違っている
  • fluent.conf のソケットが間違っている
  • mysql の username, password が間違っている
  • コントローラー内のパラメーターが存在していないので nil になっていて insert できない

などでした。

アクセスログから、ランキングを作る

ランキングを呼び出したいコントローラーから以下のような ActiveRecord を書けば呼び出せます。 リストだけ取り出すこともできるんですが、ランキングの順番に取り出すためのコードは以下の様に field(id, 1,5,3,7,....) のようにしてあげる必要があります。

# select page_id, count from page order by count(page.page_id) desc
# 1,5,3,7
ranked_id_list = Access.group(:page_id).order('count(page.page_id) desc').count.keys
sorted_rank = ActiveRecord::Base.send(:sanitize_sql_array, ["field(id ,?)", ranked_id_list])
# select * from page order by field(id, 1, 5, 3, 7) 
@popular_pages = Page.order(sorted_rank)

これで終わり!

実際に該当の アクションにアクセスしてみて確認してみてください。

その他

本来、こういう処理をする場合、 MySQL をランキングとして使うのは負荷がかかりあまりよくないですが、アクセス数がそこまであるわけじゃないし、MySQL じゃないものの学習コストをそこまで取っていられないっていう場合だったので今回これを選択しました。

Fluentd を使うことによって、こういった選択も MySQL ではない DB への移行だとか、ログ収集用のサーバーの変更がしやすくなるはずです。

前日分のアクセス数は不要なのでDBから削除してテーブル肥大化を防ぎましょう。また集計の方法も、 アクセスした時間で where 句追記するといったこともすれば、時間ごとのランキング等も作れそうですね。

さらには、テーブルを日毎にシャーディングしたり等も今後のサイトの発展次第では設定しなくてはならない気がしますが、今はミニマムな実装だけで終わることにします。

参考

Fluentd | Open Source Data Collector

tagomoris/fluent-plugin-mysql · GitHub

fluent/fluent-logger-ruby · GitHub

Fluentdで、RailsのログをS3に保存する方法 - Qiita

【Ruby】gemでinstallできたはずが読み込んでいない - うみやま亭

special thanks

@hase1031 hase1031のブログ