たごもりすメモ

コードとかその他の話とか。

Fluentd out_forward における最適化パラメータいくつかの話

Fluentdのデータをネットワーク経由で転送するための組み込みプラグイン out_forward には最適化のための設定がいろいろあるが、内部構造への理解がないとなかなか意味がわからなかったりするものも多い。ので、あんまりいじってる人はいないんじゃないかという気がする。
最近複数の転送先へのロードバランスを out_roundrobin ベースの方法から out_forward の機能を使った方法に切り替えてみたので、ついでにそのあたりについて書いてみる。

(おまけ) out_roundrobin と out_forward(のロードバランス)の違い

out_roundrobin は event stream の emit つまりFluentd内部における最小の配送処理単位ごとに配送先プラグイン(のインスタンス)を切り替える。可能な限り細かい単位で配送先をバラけさせたいときはこちらを使うとよい。0.10.25 時点では単純な均等ラウンドロビンしかできないが、設定毎に重みをつけるオプションが既に commit されているので次のリリースで多分含まれる。

out_forward で配送先 server を複数指定するとそれらを使用したロードバランスが行える。out_forward は内部バッファのflush時にそのバッファチャンクをどこに送るかを探すが、その際にロードバランス設定されたノードからひとつを選んで送る、というような動作となる。つまりロードバランスの単位は out_forward のbuffer chunk単位となる。

以前は*1なにか致命的に機能が足りないか何かで out_forward のロードバランス単位が大きすぎるから忌避していたんだけど、最近考えなおしてみたら特に問題がなさそうな状況だった、ので out_forward のロードバランスに依存するように変えてみた、という状況。設定もシンプルになることだし。

ノード数とスレッド数

num_threads

out_forward は複数スレッド化に対応したプラグイン*2で、そのスレッド数を指定するのが num_threads になる。これを増やすとI/O待ちについては並列処理してくれるので、配送データ量が多く、かつ複数ノードへのロードバランスが前提になる out_forward においては設定を考えたほうが良い。
num_threads を増やすデメリットは当然CPUを使う(コンテキストスイッチはそれなりに重い処理)のでCPUがかつかつなFluentdでは試さないほうがよい、が、今のところ自分の手元ではCPUよりもI/Oが詰まる方が怖いなーという感じ。*3

I/Oの並列化のみに効果があるので num_threads を配送先ノード数より多い数にしても意味はない。配送先ノード数と同じ数にするのは簡単だが、それはいくらなんでもやりすぎ。全ノードに一刻も休まず全力でデータを送り続けないといけないってことでしょ。

自分の手元でどうするかは考えたが、とりあえず配送先ノード数の半分にしてみた。これでもおそらく大分余裕がある数だが、CPUがきつくないなら多少以上多めにしておいても問題あるまい。おそらくノード数の 1/4 くらいでも大丈夫だとは思う(完全に勘)。

ということでこんな状態にした:

  • 配送ノード数: 56
  • num_threads: 28

このあと配送ノード数を 80 に増やしたが num_threads は 28 のままで、なにも問題なく動いてる。ノード数の 1/3 ってところか。

  • 配送ノード数: 80
  • num_threads: 28

buffer chunkあれこれ

out_forward によるラウンドロビンを考えるとき、このあたりのパラメータに注意する必要がある。極端な話 buffer chunk のflushが1時間に1回、1GB単位でしか行われなかったとしたら、ロードバランス用にいくらノード数を増やしていたとしても結局ひとつのノードに全部まとめてflushされて負荷が集中することになり、ロードバランスの意味がまったくない。

buffer_chunk_limit

buffer_chunk_limit は out_forward が内部で保持する buffer chunk の最大サイズを指定する。デフォルトは 8MB。
chunk flushは常にbuffer chunkまるごとに対して行われるため、ここで指定するサイズが out_forward がロードバランス先ノードに送信する単位の最大値となる。
たとえば1秒に1回くらいは各ノードに対してデータを送信したい、という場合、この buffer_chunk_limit が1秒間に送信できる*4サイズを超えていたら全体の動作が破綻する。送信先ノードの性能やノード間ネットワークの速度などをきちんと考えて指定する必要がある。

buffer_queue_limit

buffer_queue_limit は未送信の buffer chunk を out_forward 内で最大いくつまで保持するかの指定。デフォルトは64。
配送先ノードへの転送に時間がかかる場合は未送信buffer chunkがout_forward内に滞留することになる。out_forward はbuffer chunkをメモリ上に保持するため、無制限に保持しようとすればメモリが溢れて全体の動作がおかしくなる。それを抑止するため、万一の場合の保険としてこの最大値を指定する必要がある。
基本的に buffer chunk が滞留することはない*5はずなのであまり大きくする必要はない。が、あまり小さくしてもバースト的に転送量が増えたときに全く対応できなくなってそれはそれで問題がある。サーバ全体のメモリ量を圧迫しない程度に適当に指定しておけばよい。
唯一、buffer_chunk_limit と buffer_queue_limit をかけた総メモリ使用量がマシン全体で使えるメモリ量よりも小さくなるようにすること、という点のみ気をつけよう。

flush_interval

flush_interval はbuffer chunkをどのような時間間隔で flush するかの設定。buffer_chunk_limit に達していない程度のデータ量しか buffer chunk に入っていなくても、この時間が経過したら強制的に flush する。デフォルトは60秒。
ロードバランスを考えるときはできるだけ配送先ノードへのデータ送信の粒度が小さいほうがよい。その方が各ノードへの負荷配分が細かくなり、ノード間で負荷の偏りが発生しにくくなる。ということで、ロードバランス設定で out_forward を使う場合はこの数値は1秒で固定でしょう。

まとめ

out_forward でロードバランスを行う場合には buffer chunk がどのように溜まってどのようにflushされるかを理解しないと性能問題が出た場合などに対処が難しい。また処理するデータ量がどのくらいかということにも注意を払う必要がある。
あまり適当に設定すると低い頻度で巨大なデータが送信されたりしてロードバランスの役に立たないことがあるので、そのようなことが起きないように設定して快適なFluentdライフを楽しみましょう。

*1:あんまりよく覚えてないんだけど

*2:実はそういう機能をサポートする仕組み、というかインターフェイスがFluentd本体にある。対応しているプラグインはほとんど無いと思うけど。Ruby 1.9のマルチスレッドはCPUバウンドな処理を並列に走らせてはくれないので、I/Oがよっぽどヘビーにかかるところ以外では意味がない。

*3:ただしこれは配送サーバに限る。

*4:そして配送先ノードが処理できる

*5:そのような状況が慢性的に起きるということは全体の処理能力が足りてないので即座に改善するべき

fluent-plugin-notifier と fluent-plugin-ikachan の話

リリースしてはいたものの手元でちゃんと使っていなかったプラグインふたつ、をいよいよちゃんと使いはじめた。そのついで(?)にちゃんとテストを書いたりREADMEを書いたりしたので、せっかくだからここにも書いておく。

fluent-plugin-notifier

Fluentdで流通するメッセージを相手に数値範囲や文字列検査(正規表現)により異常を検出しアラート(を示すメッセージ)を出してやろう、というプラグイン

tagomoris/fluent-plugin-notifier · GitHub
fluent-plugin-notifier | RubyGems.org | your community gem host

使いかたはたぶん例を見ればわかる……んじゃないかなー。こんな。

<match apache.log.**>
  type notifier
  <def>
    pattern apache_duration
    check numeric_upward
    warn_threshold  800000
    crit_threshold 1000000
    target_keys duration
  </def>
  <def>
    pattern status_500
    check string_find
    warn_regexp 5\d\d
    crit_regexp 500
    target_key_pattern ^status.*$
    exclude_key_pattern ^status_ignore_.*$  # key name not to notify about...
  </def>
</match>

'numeric_upward' は閾値(warn/critそれぞれについて指定する)を超えたらアラートメッセージを出す。'numeric_downward'はその逆。'string_find' は正規表現にマッチするか検査してアラートを出す。検査の定義は任意個数書けるので match 節でがばっと多めのメッセージを対象にしておいて def 内での target_keys *1で相手を絞る、というようにしてもいいと思う。
上の例には出してないけど、連続する同じアラートメッセージ*2は任意の間隔・回数で抑制する方法を持っていて、デフォルトは以下のようになっている。

  • 1分毎に5回まで送出
  • 5分毎に5回まで送出
  • 以後は30分毎

この間隔(intervals)と回数(repetitions)は全体でも def 毎でも指定できるので、細かく指定したい人にも不満はあまりないんじゃないかなー。あったらぜひ教えてほしい。

fluent-plugin-ikachan

全世界のWebサービス開発が依存しているという噂のIRCはもちろんみんな使っていると思うが、IRCの任意のチャンネルにあちこちのシステムから発言をポストするのは意外に面倒で、そのためのモジュールみたいなのは各言語にあるんだけど、それをいちいち組み込んでまわるのはできれば避けたい。
そのように当然思う人達のためにYappoさんが書いた ikachan というツールがある。要はHTTPで簡単に発言をポストできる IRC Proxy Server ですね。今やCPANにもあります。

ikachan - IRC message delivery by HTTP - metacpan.org

で fluent-plugin-notifier でアラートメッセージを生成したらどこに投げるかというと当然IRCに投げたいので ikachan 経由になるわけですよ。そのようなプラグインも作りました。……だいぶ前に。

tagomoris/fluent-plugin-ikachan · GitHub
fluent-plugin-ikachan | RubyGems.org | your community gem host

ikachanのサーバ、対象チャンネル、出力するフィールド名のリスト、およびどうフォーマットするかを以下のように与えてやれば、目的のチャンネルに notice でポストしてくれます。

<match message.*>
  type ikachan
  host ikachan.local
  port 4979
  channel watching
  out_keys target_host,num_value,message
  message %s(%s): %s
</match>

このようにしておけば {'target_host' => 'hoge.local', 'num_value' => 100, 'message' => 'fluentd message\' message...'} というメッセージを受け取ったとき、以下のようにIRCに出力されるわけですね。

11:48 ikachan: hoge.local(100): fluentd message's message...

便利!

組合せて実現するFluentdでの監視システム

たとえば以下のようにします。

  • fluent-plugin-datacounter でHTTPステータス 5xx の数および全リクエスト中のパーセンテージをカウント
    • 後述する処理の他に fluent-plugin-growthforecast でグラフにも出力しておく
  • fluent-plugin-notifier で 5xx_percentage の数値を検査、10以上でwarn、30以上でcritとする
  • notifier の出力を fluent-plugin-ikachan で受けIRCにポスト

こうするだけで、いい感じに頻度調整も行われつつ、5xxステータスの量が跳ね上がった場合に以下のようなメッセージがIRCにポストされるわけですよ。

11:48 ikachan: HTTP Status warn [2012-07-20 11:48:04 +0900] serviceX_5xx_percentage: 10.0 (threshold 10.0) http://gf.devops.local/view_graph/accesslog/httpstatus/serviceX_5xx_percentage

我々はIRCにポストされてくるGrowthForecastのURLをぽちっとクリックするだけで、時系列での5xxステータスコードの割合の推移を確認し、これが即座に対処すべきなのか、アプリケーション担当者をつついてみるべきなのか、とりあえず放っておいていいものなのか、そういったことを考えることができるようになるというわけですね!

ちなみに上記メッセージは以下のような fluent-plugin-ikachan の設定で出してます。fluent-plugin-notifier はイベントを検出した日時を message_time というフィールドに可読文字列で入れるため、それを出力しておけばIRCに出てくるというわけですね。検出閾値も threshold というフィールドに入っているため、いちいち設定を確認する必要はありません。
GrowthForecastのグラフURLは fluent-plugin-growthforecast の設定と合わせておけばよいでしょう。

<match notification.*>
  type ikachan
  host ikachan.devops.local
  channel foobar
  out_keys level,message_time,target_key,value,threshold,target_key
  message HTTP Status %s [%s] %s: %s (threshold %s) http://gf.devops.local/view_graph/accesslog/httpstatus/%s
</match>

どうよこれ! べんり! 改めて見てみるとぽんぽん細かくエラー率が跳ね上がってるサービスあるもんですね!

……と思ったんだけど、リクエスト数の少ないサービスだと特に夜間にGooglebotなどの襲来により4xxが跳ね上がって検出してしまうなど、ちょっと微妙な点が見えてないこともないです。どうしよっかなー。

*1:もしくは target_key_pattern + exclude_key_pattern

*2:検査対象の tag + key_name と警告レベル(warn/crit)の組合せで判定

Fluentdでparser用の正規表現を書く・試す

こんなエントリを目にしたので、なんか書こうかなと思った。

fluentdのformat(正規表現)の作り方について試行錯誤中 #fluentd - Glide Note - グライドノート

Fluentd の in_tail や拙作 fluent-plugin-parser ではログのparse用の正規表現を指定することになるが、確かにこれを設定してログを流して試して直して、というのはいささか効率が悪い。ので、簡単に試す方法を書いてみる。

なお irb を使う手順。ruby 1.9系がインストールされていればたぶん入っているけれど td-agent だとちょっとどうだっけ。適当にがんばっていただきたい。

とりあえず以下のようなログをparseしたいとする。

Jul 16 00:45:47 BJA login[58879]: USER_PROCESS: 58879 ttys002

irbを起動するが、td-agent の場合はまずLOAD_PATHにfluentdのライブラリを加える必要がある。rvm(やrubiesなど)で gem install fluentd している場合は最初の LOAD_PATH の手当ては不要。

事前にlibのパスを確認する必要がある。 /usr/lib64/fluent/ruby/lib/ruby/gems/1.9.1/gems/fluentd-0.10.XX/lib あたりにあるはず。

$ irb
:001 > $LOAD_PATH.push /usr/lib64/fluent/ruby/lib/ruby/gems/1.9.1/gems/fluentd-0.10.xx/lib

そしたら以下のようにして確認する。

require 'fluent/config'
require 'fluent/parser'
require 'fluent/engine'

log = 'Jul 16 00:45:47 BJA login[58879]: USER_PROCESS: 58879 ttys002'

regexp = /^(?<date>\w{3} \d\d (\d{2}:){2}\d{2}) (?<host>\w+) (?<role>\w+)\[(?<pid>\d+)\]: (?<event>\w+): (?<info>\d+) (?<tty>.+)$/

parser = Fluent::TextParser::RegexpParser.new(regexp)

parser.call(log)
# => [1342367563, {"date"=>"Jul 16 00:45:47", "host"=>"BJA", "role"=>"login", "pid"=>"58879",
#  "event"=>"USER_PROCESS", "info"=>"58879", "ttyname"=>"ttys002"}] 

うまくいかなければ regexp を直して parser にセットしなおして parser.call(log) しなおす、というのを繰り返せばいい。

なお in_tail などの日時parseも試したければ以下のようにする。日時としてparseするフィールドを 'time' にしておくのがポイント。

require 'fluent/config'
require 'fluent/parser'
require 'fluent/engine'

require 'time'

log = 'Jul 16 00:45:47 BJA login[58879]: USER_PROCESS: 58879 ttys002'

regexp = /^(?<time>\w{3} \d\d (\d{2}:){2}\d{2}) (?<host>\w+) (?<role>\w+)\[(?<pid>\d+)\]: (?<event>\w+): (?<info>\d+) (?<tty>.+)$/

parser = Fluent::TextParser::RegexpParser.new(regexp)

parser.time_format = '%b %d %T'

parser.call(log)
# => [1342367147, {"host"=>"BJA", "role"=>"login", "pid"=>"58879", "event"=>"USER_PROCESS", "info"=>"58879", "ttyname"=>"ttys002"}] 

Time.at(1342367147)
# => '2012-07-16 00:45:47 +0900'

まあこんな感じじゃないすかね。

fluent-plugin-numeric-monitor v0.1.0 and fluent-plugin-numeric-counter v0.1.0 released!

Fluentdメッセージ中の数値の統計・集計をとるためのプラグインをふたつリリースしたのでその話。numeric-monitorは前に 0.0.x 系のバージョンでいちおう出してたけど、自分の手元でもちゃんと使い始めたので v0.1.0 として改めてリリースした。

fluent-plugin-numeric-counter

fluent-plugin-numeric-counter | RubyGems.org | your community gem host
tagomoris/fluent-plugin-numeric-counter · GitHub

このプラグインは数値の上下範囲にマッチするメッセージをカウントする。数値版の datacounter みたいなものだと思えばよい。出力も datacounter の出力内容とまったく同じ。
Fluentd Casualのときの @fujiwara さんの話にあったレスポンスタイム毎の割合のグラフ化はやってなかったので真似ようと思ったんだけど、datacounter で正規表現を使って無理矢理数値を評価するというのはちょっとアレな気がする、と思ってたので、ついでにさくっと作った。

機能としては簡単、パターン毎に数値をふたつ(x, y)指定しておくと x <= N < y に該当する数値をもっているメッセージの数をカウントする。最後のパターンに限っては上限値を指定しないこともできる*1。文字列でも数値化して検査するし、小数点が含まれていてもOK。負数もいちおう大丈夫(のはず)。バイト数などを検査する場合には 10k のような指定もできる(基数は1024)。

fluent-plugin-numeric-monitor

fluent-plugin-numeric-monitor | RubyGems.org | your community gem host
tagomoris/fluent-plugin-numeric-monitor · GitHub

指定したフィールド(の数値)について、min/max/avg および指定したパーセンタイル値を算出する。パーセンタイル値は複数指定できるので、たとえば 90, 95, 98, 99 パーセンタイル値をそれぞれ出力したりできる。(また単純に検査対象になったメッセージ数 num も出力する pullreq もmergeした。thx @kentaro !)

これにやはりレスポンスタイムの数値を食わせてやったりすると、時系列でWebサービスのレスポンスがどのように推移するかがばっちりわかる。95パーセンタイル値はたいして変わってないけど98パーセンタイル値が極端に悪化しててなんだこれ、みたいなのが見えてきて割と面白い。

レスポンスタイムの集計・統計と可視化

これらのプラグインを使って以下のように設定することで、サービス毎にレスポンスタイムの推移をがっつり可視化することができるわけですね!

<match accesslog.**>
  type copy
  <store>
    type numeric_monitor
    input_tag_remove_prefix accesslog
    unit minute
    tag monitor.responsetime
    aggregate tag
    monitor_key duration
    percentiles 50,90,95,98,99
  </store>
  <store>
    type numeric_counter
    input_tag_remove_prefix accesslog
    unit minute
    tag numcount.responsetime
    aggregate tag
    count_key duration
    pattern1 u100ms      0  100000
    pattern2 u500ms 100000  500000
    pattern3 u1s    500000 1000000
    pattern4 u3s   1000000 3000000
    pattern5 long  3000000
  </store>
</match>

<match {monitor.responsetime,numcount.responsetime}>
  type forest
  subtype growthforecast
  <template>
    gfapi_url http://growthforecast.local/api/
    service accesslog
    tag_for section
  </template>
  <case monitor.responsetime>
    remove_prefix monitor
    name_key_pattern .*_(min|max|avg|percentile_\d+)$
  </case>
  <case numcount.responsetime>
    remove_prefix numcount
    name_key_pattern .*_percentage$
  </case>
</match>

これだけでこんな感じのグラフがサービスごとに作成されてやっほう! というように大変お仕事が捗ります。

500ms以上かかってるリクエストがけっこうあるようですね! あと2〜4時のあいだに何か起きてるように見える。というようなことが一目瞭然ですね!!!!!

どうぞお使いください。

*1:x <= N に該当するものすべてがカウントされる

Fluentd Casual Talks 開催してきた&しゃべってきた

なんとなく思い付いたら各所の協力を得られましたので、そのまま開催してしまいました。
勉強会を主催するのは初めて*1だったのですが、会場をお貸しいただいた株式会社ディー・エヌ・エー様、ならびに当日運営をまるっとお手伝いいただいた @riywo さんをはじめDeNA社員の皆様のおかげで全く問題なく、すばらしいイベントになりました。本当にありがとうございました。
またUstream中継については @ixixi さんにご協力をいただきました。たいへんな作業だと思いますが、本当にありがとうございました。

Fluentd Casual Talks : ATND

金曜夜に100人を超える参加者が見事に集まりまして*2、Fluentdを実際に使ってみた系の人々が大いにしゃべってました。個人的にはあれやこれやと面白い話がたくさん聞けたので、それだけでもう満足。SDのFluentd特集との内容のカブりについて話もありましたけど、自分が思うに、どう使っているかという生の話は雑誌記事ではどうしても得られない実感に繋がると思うので、やっぱりスピーカーが直接話す、というのは大事ですね。
MySQL Casual Talks 主催の @myfinder さんが「自分が聞きたい話があるからやる」というのを聞いたことがありますが、あれはいい言葉だなーと思う。自分も今回実感しました。やってよかった。参加者の方々もそう思ってもらえていると良いですね。内容がカジュアルだとかガチュアルだとか(むしろ主催だけがガチュアルだとか)言われましたが、内容はともかく開催と参加がカジュアルであることが大事ですね。

運営という面では、本当にタイムスケジュール詰めすぎ、という話に尽きます。最初からあの分量で 19:00-21:00 って書いて休憩を間に挟むべきだった。人間はトイレタイム無しでは生きていけないのです。まあカジュアルな勉強会なので、ああいうときにはカジュアルに良きにはからうのがよいですね。

個別に感想を書きますと、とにかく先頭の @oranie さんが期待していたことをきっちり話してくれたのがすばらしかった。なんでFluentdを使うのが嬉しいのか、まだ使ってない人達への絶妙な内容になったんじゃないだろうかと思う。そしてドキュメントについての熱い思いを語っていたので、きっと世界のFluentd界におけるドキュメント化の先頭を突っ走ってくれるに違いありませんね。

飲みの席にて @oranie 語る

@fujiwara さんのFluentdの使いかたには、自分もやってなくて、かつ即座に真似ようと思うところもあったりして、やっぱり話していただけて最高に正解でした。すばらしい。やっぱり運用をきちんとやっている話を聞くとみんなのお仕事が即座に捗るようになるなあ。レスポンスタイム別のパーセンテージ表示は真似るぞー。*3

@kzk_mover さんは西海岸から偶然たまたま帰国しているタイミングでした。まさにあの人にしかしゃべれないことをあの場所でしゃべってもらって最高でした。あとで聞いたら当日は40度近く熱があった中をやってたらしいです。どんだけ熱意あるの……。Fluentdのロゴが無事決まったことが全世界で初公開されるなど、盛りだくさんの内容で大満足でしたね。……ちょっと俺がdisられまくりなことを除けば!

LTはどの内容も使ってみた系の話やプラグインの話を中心に、やっぱり実践の内容ばかりでたいへんすばらしゅうございました。しかもみなさん5分の枠にきっちり収まっててすばらしいというか、みんなもうちょっとルーズでもいいんだぜ(自分のLT資料のページ数を見つつ)。いや、でもそのおかげで遅くなりすぎない時間に終わりました。感謝しております。

しゃべってきた

で、自分もLTでカジュアルにしゃべってきた。

自分の書いた(リリース済みの)プラグインについて、それぞれの機能を簡単にご紹介、という内容。あと他の言う場所もなかったので、2月の Fluentd Meetup の後でFluentdクラスタがどういう状況になったかも簡単に数字を並べた。
内容は見てもらえば、と思うけど、小さいプラグインを繋げていってナイスに使うというのはコマンドのパイプラインとも通じるところで、たいへん応用の効く方法だと思う。それぞれのプラグインは小さい限定的な機能しかなくてもいい、というところも作りやすくてナイス。みんなああいう感じであれこれ工夫しまくると世界が良い方向に行くのでどんどんやるといいと思う。

あと One more thing とか言っていた fluent-plugin-webhdfs はその後の日曜に無事 リリースしました 。ただ性能試験もロングランもやってないしテストもドキュメントもほとんど書いてないので、いま突撃すると色々なことが起きて大変面白いと思います。ぜひどうぞ。

雑感

たいへん面白かったので、またその気になったらやりたい。また、他の人がやりたくなったらそれはそれで Vol.2 でも別名イベントでもやればいいと思う。とりあえず、やってよかった。
かじゅある!

*1:isuconというイベントで主催側の一員だったことがあるけど、あれはなんていうか、だいぶ違う

*2:ちゃんと数えてなかったんだけど、DeNA側に記録あるのかな

*3:でもdatacounterでやるのはちょっと無理がある気がするので、数値範囲でパーセンテージをカウントするプラグインを作るつもり

fluent-plugin-mysql released!

FluentdからMySQLにデータを挿入するためのプラグイン fluent-plugin-mysql を公開しました。

tagomoris/fluent-plugin-mysql · GitHub
fluent-plugin-mysql | RubyGems.org | your community gem host

機能としては以下のどちらかを指定してDBにデータを突っ込む、というようなものになっております。

  • 指定したフィールドの値を指定したカラムに挿入(INSERT)
  • メッセージ全体をJSONにして指定したカラムに挿入

JSONで突っ込む場合には奥一穂さんの mysql_json を使うとたいへん便利になると思います。
基本的に現在は性能面のことは(ほとんど)考えておらず、そのまま大量のデータをこれを使ってmysqlに入れようとすると間違いなく酷いことになると思います。どうしても頑張りたい場合には blackhole engine などを使ったハックに血道を上げると良いのではないでしょうか。さすがにオススメできませんが。

また fluent-plugin-mongo がサポートする tag_mapped などの機能も現時点では持っていません。メッセージのタグ/時刻を保存対象にする方法も現時点ではありません。このあたりはそのうちどうにかしたいなーというか、pullreqをお待ちしていますというか、そういう状態です。

MySQLへの抑えられない思いを存分に表現したいという方は使ってみるといいんではないかと思います。

fluent-plugin-forest released!

現状のfluentdでは、タグを動的に扱う方法がいまいち無い。具体的に言うと設定項目にタグに応じて変化するような指定をしたい場合、タグごとに分けて書くしかない。例えば out_file で出力先ファイル名をタグに応じてつけたい場合、タグの数だけ match 節を書く必要がある。

<match hoge>
  type file
  path /var/log/hoge.log
</match>
<match pos>
  type pos
  path /var/log/pos.log
</match>
# 以下いっぱい

これには極めて簡単にわかる範囲で、ふたつの大きな問題がある。

  • 多数のタグを扱う場合、設定ファイル全体のボリュームが肥大化して管理コストが増大する(品質が低下する)
  • 新しく扱うタグが増える場合、設定ファイルの更新と適用が必要となり、管理コストが増大する

既に手元でこの問題に悩まされていて、HoopServerに書き込みに行くfluentd*1の設定ファイルの行数が既に18タグ分、417行にわたっていてかなり末期的な症状を呈していた。大部分コピペなんだけどところどころ違うのがたちが悪い。

ぶっちゃけ、このように書いたらあとは適当にfluentdがやってくれたりすると嬉しい。

<match *>
  type file
  path /var/log/__TAG__.log
</match>

無いのなら、作ってしまえ、プラグイン

fluent-plugin-forest

ということで fluent-plugin-forest を作った。とりあえずまる1日動かしたところ問題なさそうなのでrubygems.orgにリリースし、手元では全台に配布済。

tagomoris/fluent-plugin-forest · GitHub
fluent-plugin-forest | RubyGems.org | your community gem host

これを使うことで417行あった設定ファイルが55行まで圧縮され、しかもタグが増えても再起動も要らず、タグごとの設定の違いもたいへん分かりやすいという天国的な状況に! ヤッタネ!

ちなみに上の単純な out_file のパターンだとこのようになる。

<match *>
  type forest
  subtype file
  <template>
    path /var/log/__TAG__.log
  </template>
</match>
どう使えるか

fluent-plugin-forestは以下のように動作する。

  • タグひとつにつき output plugin のインスタンスをひとつ生成する
    • これは設定ファイルに のセクションをひとつ増やすことに(おおまかには)対応する*2
    • タグのパターンごと、ではなく、タグ全体の文字列ひとつに対しインスタンスひとつ、であることに注意*3
  • 設定中の任意の場所で __TAG__ というプレースホルダを使用できる
    • この部分はタグ文字列に置き換えられた設定として output plugin に渡される
    • なおここには remove_prefix / add_prefix が評価された後のタグが入る
  • 設定では template セクションと case セクションを使用できる
    • template セクションは全タグ文字列に対して適用する設定内容
    • case セクションは case hoge.** のようにパターンを記述でき、そのパターンにマッチする場合だけ適用する設定内容
  • 現状 server セクションや store セクションなどを用いる output plugin について、そのサブセクション内だけを置き換えるような書き方はできない
    • つまり forward プラグインで送り先の1行だけを書き換える、みたいなやりかたは forest ではできない
    • そのうちやるつもり*4

このあたりを理解して、自分の手元の実例を挙げるとこうなる。まず従来の設定内容。sourceや最後のforwardまわりの細かい設定は省いてある。

# original configurations
<source>
  type forward
</source>

<match converted.serviceX>
  type copy
  <store> # Hoop server への書き込み、流量が多いのでノードごとにファイルを分けるもので NODENAME には実際にはfluentd動作ノード名がサーバごとに書かれる
    type hoop
    hoop_server hoop.server.local:14000
    path /hoop/log/%Y%m%d/serviceX-%Y%m%d-%H.NODENAME.log
    username hoopuser
    flush_interval 60s
    output_include_time false
    output_include_tag  false
    output_data_type attr:hhmmss,vhost,path,method,status,bytes,duration,referer,rhost,userlabel,agent,FLAG,status_redirection,status_errors,rhost_internal,suffix_miscfile,suffix_imagefile,agent_bot
    add_newline true
  </store>
  <store> # 監視用サンプリング設定
    type sampling_filter
    interval 100
    remove_prefix converted
    add_prefix sampled.100
  </store>
</match>

<match converted.serviceY> # 流量の少ないサービス、path指定NODENAMEがないこと、サンプリングレートが 10 であることのみ異なる
  type copy
  <store>
    type hoop
    hoop_server hoop.server.local:14000
    path /hoop/log/%Y%m%d/serviceY-%Y%m%d-%H.log
    username hoopuser
    flush_interval 60s
    output_include_time false
    output_include_tag  false
    output_data_type attr:hhmmss,vhost,path,method,status,bytes,duration,referer,rhost,userlabel,agent,FLAG,status_redirection,status_errors,rhost_internal,suffix_miscfile,suffix_imagefile,agent_bot
    add_newline true
  </store>
  <store>
    type sampling_filter
    interval 10
    remove_prefix converted
    add_prefix sampled.10
  </store>
</match>

# 以下16回(!)くりかえし

<match sampled.**>
  type forward
  <server>
    host watcher01.local
  </server>
  <server>
    host watcher02.local
  </server>
</match>

眺めるだけでアタマが痛いですね!!!!! これを fluent-plugin-forest を使うことによりこれだけに圧縮した。

<source>
  type forward
</source>

<match converted.*>
  type copy
  <store>
    type forest
    subtype hoop
    remove_prefix converted
    <template>
      hoop_server hoop.server.local:14000
      username edge-dev
      flush_interval 60s
      output_include_time false
      output_include_tag  false
      output_data_type attr:hhmmss,vhost,path,method,status,bytes,duration,referer,rhost,userlabel,agent,FLAG,status_redirection,status_errors,rhost_internal,suffix_miscfile,suffix_imagefile,agent_bot
      add_newline true
    </template>
    <case {serviceX,serviceZ}>
      path /hoop/log/%Y%m%d/__TAG__-%Y%m%d-%H.NODENAME.log
    </case>
    <case *>
      path /hoop/log/%Y%m%d/__TAG__-%Y%m%d-%H.log
    </case>
  </store>
  <store>
    type forest
    subtype sampling_filter
    remove_prefix converted
    <case {serviceA,serviceX}>
      interval 100
      add_prefix sampled.100
    </case>
    <case *>
      interval 10
      add_prefix sampled.10
    </case>
  </store>
</match>

<match sampled.**>
  type forward
  <server>
    host watcher01.local
  </server>
  <server>
    host watcher02.local
  </server>
</match>

これで全部。どうよこの画期的にわかりやすい(?)設定! これでタグが増えてもデフォルトルールに従うだけなら設定変更はいらないし、メンテナンスも極めて簡単になって言うことなしですね!!!!!

*1:ついでにサービス毎に特定のレートで監視用のサンプリングも行う

*2:メモリ使用量的にはこの通り。matchにかかるCPU資源の使いかたには少しだけ差があるが、ほとんど増えていないはず。output plugin自体が使うCPU資源には全く差は無い。

*3:もし時刻をタグに埋めているようなものをforestに食わせると大変なことになる

*4:設定オブジェクトのノードを丁寧にたどる実装を書く必要があって面倒。誰かパッチ書いてくんないかなw