Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

【進撃の巨大データ】RealTimeLog集計を目的としたRedisの活用


Log集計の設計を再考

【進撃の巨大データ】Log集計用DBとシステム構成の美しい設計を考える - Yuta.Kikuchiの日記 はてなブックマーク - 【進撃の巨大データ】Log集計用DBとシステム構成の美しい設計を考える - Yuta.Kikuchiの日記
人生を前向きに楽しむことを心に誓った@yutakikuchi_です。最近はこのブログで【進撃の巨大データ】というタイトルで何回かBigDataに関する記事を書いています。前回はLog集計用DBとシステム構成の美しい設計を考えるという題でInnoDB、InfiniDBを使ったLog集計のmerit/demerit、SystemPerformanceについて記述しました。それから時間をおいて再考し、InnoDBを使う場合のメリット/デメリットと注意事項が不足している事に気づいたのでここで追記します。更に集計の緊急度に合わせて使用するDBを変えます。リアルタイムではRedis、定期処理ではMysqlを使って集計することを試してみたいと思います。

Log集計方法のmerit/demerit

4のRedisを使うは今回新しく追加した項目です。

No Method merit demerit
1 InnoDBを使う。格納時点でデータを集計。
1行中の集計用カラムを都度Update。
特定のデータ集計を1行で管理可能。
行数が膨らまない。
集計SQLのPerformanceを出す事ができる。
書き込み口が複数あると行ロックが心配なので、
サーバ構成を工夫しなければ行けない。
格納時にデータを集約してしまうので、
シンプルで特定の集計しかできない。
【追記】データの書き込みや集計に失敗するとリカバリが大変。
2 InnoDBを使う。格納時点でデータを1行ずつ書き込む。
集計時にSQLで行数をSUMる。
行ロックの心配が無くなる。
【追記】処理ミスの影響範囲が行レベルに収まる。
行数が膨大になる。
集計用SQLも重くなるし、GROUP BYにも限界がある。
行数が膨らまないように
定期的に古いデータは削除するなどの処理が必要。
3 InfiniDBを使う。格納時点でデータを1行ずつ書き込む。
集計時にSQLで行数をSUMる。
列指向なので集計が速い。 InfiniDBは制限が多くて使いづらい。
行数が膨大になる。
INSERTに時間がかかる。
行数が膨らまないように
定期的に古いデータは削除するなどの処理が必要。
4 Redisを使う。格納時点でデータを都度集計する。
RedisのCountKeyをUpdate。
インメモリDBなので書き込み/読み込みが高速。RealTime処理向き。 データ保証性や信頼性がMysqlに比べて劣る。
RealTime処理のみRedisを利用する等使い分けを考慮する。
前回の記事で書いた設計:LogAggregatorの注意点

[:W640]
前回の記事で書いた「美しいはずだった」システム構成図です(笑)。実はこのシステム構成のままLogAggregatorに進撃の巨大データが突っ込まれると、1台のLogAggregator=>1台のMysql Masterで書き込みの待ち状態が発生し、結果として集計結果取得が遅延してしまう可能性があります。これでは超大型巨大データを駆逐することは出来ません。そこで集計の緊急度に合わせて使用するDBを使い分ける設計を今回考えます。RealTime集計はインメモリDB、定期集計はMysqlを利用という感じです。ちなみに以下が前回のInnoDB/InfiniDBへのWrite/ReadのPerformance実績です。InnoDBでもかなりのWrite時間、InfiniDBは論外でした。

No 方法 50万件 INSERT時間 50万件 SELECT時間
1 InnoDBを使う。格納時点でデータを集計。
1行中の集計用カラムを都度Update。
0.6 hour 0.00 sec
2 InnoDBを使う。格納時点でデータを1行ずつ書き込む。
集計時にSQLで行数をSUMる。
2.15 hour 6.34 sec
3 InfiniDBを使う。格納時点でデータを1行ずつ書き込む。
集計時にSQLで行数をSUMる。
39 hour 0.75 sec
RealTimeの集計はRedis、定期集計はInnoDB
新しい設計図

修正点はRealTime集計用途でLogAggregator Master => Redis Master、定期集計用途でLogAggregator Slave => Mysql Masterと定義しました。この構成でInserの待機が回避できるかを試します。

なぜRealTime集計でRedisを使うか

Redisを使う理由はいかにあります。

  • インメモリDBだから処理の高速化が期待できる。
  • 特定のKeyのCountUpならばそれほどメモリ容量を食わないから、高速処理に期待ができる。
  • 定期的にdiskに書き出す永続か機能があるから、一般的なインメモリDBより信頼がある。

MysqlにもインメモリDBがあると思うので、後日そちらも使ってみる予定です。

RealTime集計をどのような方針でやるか

RedisへのWrite/Read処理を効率化させるために集計カウント用のRedisKeyをCountUpする方針で行います。InnoDBの集計カラムを都度Updateするケースでのdemeritにも記載した内容と同じですが、これらの方針だと一度集計結果がズレるとその状況が分かりづらく、またリカバリが困難です。よって処理の影響範囲がDaily以下で収まるようなDBの設計と、集計した結果もズレているリスクがある(確立は低いですが)という事を承諾した上で利用するぐらいのつもりが良いと思います。当然RealTimeでの集計はズレが生じるケースがあると思うので、そこをカバーするために定期処理では安定のMysqlを利用して正確な集計結果を出すようにしましょう。

fluent-plugin-redis-counter

kbinani/fluent-plugin-redis-counter はてなブックマーク - kbinani/fluent-plugin-redis-counter
LogAggregator Master => Redis Masterへデータを書き込むためにfluent-plugin-redis-counterを利用します。fluent-plugin-redis-counterは名前の通り、指定したRedisKeyのCountUpを行うPluginです


gemでinstallした後に/etc/td-agent/td-agent.confに対してredis_counterの設定を行います。ここで記述している設定はLogAggregatorのMasterでの設定ファイルになりますので注意してください。またWebApplicationとしてNginxを使う事を前提にしています。


設定についてRedisに書き込むタグだけ説明します。type tailのsourceタグで指定した/var/log/nginx/access.logにデータがが書き込まれるとFluentdはJSON形式へデータを変換します。次にtag nginx.realtimeaccessで指定された命令タグを読み込みます。の中ではredis-serverのhost名(host)、port番号(port)、db名(db_number)を選択し、でredis内部でCountUpの条件を設定します。条件の指定はmatch_*のようにアスタリスクの箇所にLog項目の内容を記載します。例えばNginxの場合、HttpStatusをcodeという項目で扱うのでmatch_codeURLをpathという項目で扱うのでmatch_pathのような条件指定となります。Redisに書き込むKeyの設定もできます。Log集計用のKeyになるので日付を含めてここではYYYY-MM-DD:URLというKey設定にします。Keyの形式指定はcount_key_formatで指定し、日付のデータは%Y-%m-%d:foo.htmlのようにすると自動で展開してくれます。


以下は実際の設定内容です。またその次に記述しているのはNginxのLogをJSON形式で表現したものです。設定内容がちゃんと有効化されているかどうかをNginxに対してアクセスをして、redisにリアルタイムでデータが書き込まれたかを確認します。

$ sudo /usr/lib64/fluent/ruby/bin/gem install fluent-plugin-redis-counter

$ sudo vim /etc/td-agent/td-agent.conf

<source>
    type forward
    port 24224
  </source>

  <match nginx.access>
    type file
    path /var/log/nginx/access.log
  </match>

  <source>
    type tail
    format apache
    path /var/log/nginx/access.log
    tag nginx.realtimeaccess
    pos_file /var/log/td-agent/nginx.pos
  </source>

  <match nginx.realtimeaccess>
    type redis_counter
    host localhost
    port 6379
    db_number 0
    <pattern>
      match_code ^2[0-9][0-9]$
      match_path ^/foo.html$
      count_key_format %Y-%m-%d:foo.html
    </pattern>
    flush_interval 10s
  </match>

$ sudo /etc/init.d/td-agent restart
2013-07-17T00:55:39+09:00   nginx.realtimeaccess    {"host":"127.0.0.1","user":"-","method":"GET","path":"/foo.html","code":"200","size":"0","referer":"-","agent":"curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2"}
$ redis-cli
redis 127.0.0.1:6379> FLUSHALL
OK
redis 127.0.0.1:6379> keys *
(empty list or set)
redis 127.0.0.1:6379> exit

$ curl http://localhost/foo.html
$ curl http://localhost/foo.html
$ curl http://localhost/foo.html

$ redis-cli
redis 127.0.0.1:6379> keys *
1) "2013-07-17:foo.html"
redis 127.0.0.1:6379> get 2013-07-17:foo.html
"3"

ちゃんと3回curlでアクセスしたらRedisの2013-07-17:foo.htmlというkeyに対して3というカウントがされるようになりました。後はこれを集計ツールで読み込むようなプログラムを作ればRealTImeLog集計がRedisを使う事で可能になります

Performance測定

RedisのWrite/ReadのPerformance測定をします。Fluentd経由ではなくredisphpにて50万件のLogデータの書き込みを模擬します。以下はredisphpの設定と書き込みプログラムのサンプルです。結果として50万件のデータの書き込みは1分で終わりました。

$ git clone https://github.com/nicolasff/phpredis.git
$ cd phpredis
$ phpize
$ make && sudo make install

$ sudo vim /etc/php.ini
;以下を追記
[redis]
extension=redis.so
<?php
$start = time();
$redis = new Redis();
$redis->pconnect('127.0.0.1', 6379);
$redis->set('2013-07-17:foo.html',1);
for( $i=1; $i<500000; $i++ ){
   $count = $redis->get('2013-07-17:foo.html');
   $redis->set( '2013-07-17:foo.html', $count+1 );
}
$end = time();
echo $end - $start . "\n";
$ php redis-write.php 
60

RedisのPerformanceをMysqlの結果にマージすると以下のようになります。書き込みはRedisのKeyをCountUpするのが断然速くなりました。最初に注意点として挙げたMysqlのRealTimeLogのInsert待ちはRedisを活用すると回避可能な事がこの実験を通して分かりました。なんたって書き込みが0.6hourから1minに縮まったので36倍のPerformanceが出せた事になります。

No 方法 50万件 INSERT時間 50万件 SELECT時間
1 InnoDBを使う。格納時点でデータを集計。
1行中の集計用カラムを都度Update。
0.6 hour 0.00 sec
2 InnoDBを使う。格納時点でデータを1行ずつ書き込む。
集計時にSQLで行数をSUMる。
2.15 hour 6.34 sec
3 InfiniDBを使う。格納時点でデータを1行ずつ書き込む。
集計時にSQLで行数をSUMる。
39 hour 0.75 sec
4 Redisを使う。格納時点でデータを集計。
集計用のKeyを都度Update。
1min 0.00sec
実験の結果と今後
  • RedisへのRealTimeLog書き込みはfluent-plugin-redis-counterを使うと簡単に実現できる。
  • Redisを利用すると巨大データのInsert待機を回避できる。
  • RedisへのInsertはInnoDBの36倍Performanceを出す事ができた。
  • 次回はMysqlのインメモリDBを検証してみたい。