2013/12/18

fluent-plugin-redis-publishのご紹介

このエントリはFluentd Advent Calendar 2013の18日目のエントリです。社内引きこもり系のものぐさエンジニアがパブリックなAdvent Calendarに記事を書くなんてまぁめずらしい!

さて、今日は何の話かというと、fluent-plugin-redis-publishというプラグインの話です。作者はtypesterなんですが、rubygems.orgにアップロードしたのはhisaichi5518です。なんでそんなことになっているかというと、話は1ヶ月半ほど前に遡ります。もともとこのプラグインは社内で生まれて1年前くらいから普通にバリバリと使っていました。

hisaichi5518: fluent-plugin-redis-publish ってrubygemsにないんですか
acidlemon: ない

言われるまで気付かなかったけど、1年以上使ってるのにgithub止まりgemだったとは!

acidlemon: typester: fluent-plugin-redis-publish をrubygemsに上げるのまだなのかな…ってひさいちくんが泣いてる
hisaichi5518: まだなのかな………
typester: こないだやろうとしたけど、gemの作り方わすれてたので、あきらめた
typester: ひさいち氏やって良いよ

いつもの展開だった。まぁそんなわけで先月ようやくrubygems.orgに上がりました。というわけで、これの使い方とかどのように使っているかというお話です。

Q. なんで自分の作ったモジュールじゃないのに紹介記事を書くの?
A. たぶんぼくが一番便利に使っているからです。自分が便利だと思ったものは(記事書くのは面倒だけど)紹介していきたい。

モジュールの特徴

FluentdのOutput Pluginとして、入ってきたレコードをRedisのPub/Sub機能へPUBLISHコマンドで送ります。その際、以下のような動きをします。

  • レコードにtimeというキーで現在時刻(Unix Epoch)を追加
  • RedisへPUBLISHするチャンネルはレコードのtagを使用する

この記事を書く前におもむろにrubygems.orgを検索したらfluent-plugin-redis-pubsubというのを見つけたのですが、これとの違いはredis-pubsubプラグインは設定でチャンネルを指定するのに対し、redis-publishプラグインはtagがチャンネルになるという点のようです。あとredis-pubsubプラグインはInput PluginとしてSUBSCRIBEする機能もあるみたいですが、redis-publishプラグインにはそれはありません。

Redisのchannelがタグごとに分けるという仕様がよいのか悪いのかは使う人次第なのでその点は使う人が適宜選択すればよいかと思います。

チャンネルがタグごとになっているメリットとしては、fluentdの設定のmatchは正規表現でパターンマッチしてまとめて送るようにしてしまいたいが、subscribeする側では余計なものを受け取りたくないみたいなときに楽という感じです。デメリットは複数のタグを1個のSubscriberで拾うことができないことですが、その点はPSUBSCRIBEコマンドでパターン指定することである程度回避できます。

Webサービスへのデイリーユニークユーザ数、どうやって数える?

さて、ここからは簡単な利用例の話です。ニーズとして一番わかりやすそうな、1日ごとにログインしたユニークユーザを数えたいときにこうやって構成しますという例を示します。

DBに格納してSQLで引っ張って算出するとなると、なんかログインログのテーブルを用意して、ログイン発生ごとにuser_id, login_atみたいなカラムでデータを突っ込んでいく…という感じでしょうか。で、ログイン数がほしくなったらlogin_atで範囲を指定してCOUNT(DISTINCT user_id)するんですかねー。

MySQLならlogin_atDATETIMEじゃなくてDATEにして(user_id, login_at)をUNIQUE制約かけてINSERT IGNOREしながら挿入していけばCOUNTするときはDISTINCTじゃなくてよくなる感じになりそうです。ただデータを丸めてINSERT IGNOREしてしまうともはやログインログという位置づけじゃなくなるやんという話もあります。

とりあえずDBでやろうとするとそんな感じになると思うんですけども、ぼくはMySQLのInnoDBのCOUNTに時間かかるじゃないですか! みたいな件が気になります(気にしすぎ?)。テーブルスキャンとかインデックススキャンにならない、定数時間でユニークユーザ数が出せる仕組みをNoSQLを活用して作ってみましょう。ちなみにInnoDBのCOUNTに時間がかかるから使い物にならねーぜとかそういうことを言いたいのではありませんのでご了承ください。その辺は漢のコンピューター道を参考にすると十分実用的な速度を出せると思います。

で、NoSQLでどうやって定数時間でそれを実現するかというと、みんな大好きRedisのSetを使います。RedisのSetは同じデータを何回入れても1つとしてカウントされるので、login:20131218みたいなキーのSetにuser_idの値を入れて、SCARDでSetに入っている要素数をカウントすればO(1)でユニークユーザ数を出すことが出来ます。

なーんだじゃあログイン処理の最後にSADDすればいいね! って事になるわけですが、サービスの規模がデカくなってくるとログイン処理とは非同期にそのSADDをやりたくなってくるわけです。ユニークユーザ数の集計のようなサービス内容と直接関係しないKPI集約系の機能でレスポンスタイムが悪化するのは避けたいですからね。

じゃあどうするかというと、私はFluentのログを拾ってきてそこでSADDしましょう...となっています。あと、容量とかセキュリティとかの運用面でプロダクト用のRedisと統計解析用のRedisは分けたいというのもあり、そのような理由で生まれたのがredis-publishプラグインです。

fluent-plugin-redis-publishを使ってやってみる

具体的には、Webサービスへのログインが成功したタイミングでFluentdへapp.loginというタグでログを書き出すようにし、JSONにuser_idを含めるようにします。で、以下のような設定を書きます。

<match app.**>
  type copy
  <store>
    type redis_publish
    host 127.0.0.1
    port 6379
  </store>
  <store>
    type file
    path /path/to/logdir/app
  </store>
</match>

これでFluentdが稼働しているサーバのlocalhostに立っているredis-serverへapp.ではじまるタグのレコードがじゃんじゃんバリバリとPUBLISHされていきます。

で、RedisをSubscribeするクライアントではapp.loginをSubscribeするようにします。すると、app.loginとかapp.purchaseとかapp.uploadとかそういういろんなレコードのうちapp.loginだけをSubscribeすることが出来ますので、そのレコードを解析してuser_idやtimeを取り出してRedisにSADDします。

これで、Webサービスのログイン処理とは非同期に、Fluentdを使ったログ集約のついでにWebサービスのKPI集計といった作業を行うことができます。便利ですね! 仕組みと必要性を理解するまで時間がかかり(もしかしたら必要に迫られないかもしれない)、構築するのもちょっとめんどくさい仕組みなんでもうちょっとソースコードのサンプルとかを出せたらよかったのですが、今日の所は時間切れです。

ただ、KPIの集計みたいなWebサービスのサービス内容に直接関係ない処理はなるべく非同期で(別のところでやる)というポリシーをお持ちの方なら、Fluentdにとりあえずログを投げてRedisのPub/Sub機能を用いてイベントドリブンに集計を行っていくという方法には共感いただけたのではないでしょうか。

うされもん @acidlemonについて

|'-')/ acidlemonです。鎌倉市在住で、鎌倉で働く普通のITエンジニアです。

30年弱住んだ北海道を離れ、鎌倉でまったりぽわぽわしています。

外部サイト情報

  • twitter
  • github
  • facebook
  • instagram
  • work on kayac