読者です 読者をやめる 読者になる 読者になる

ハウテレビジョン開発者ブログ

『外資就活ドットコム』を日夜開発している技術陣がプログラミングネタ・業務改善ネタ・よしなしごとについて記していきます。

fluentdでnginxのログをElasticsearchとBigQueryに保存するお話

こんにちは。夏休みに長野に行って居酒屋で馬刺しをたらふく食べていたら 地元のおっさん人生の大先輩の絡み酒に付き合わされた祖山です。

4月に入社して以降、サーバサイドのWeb開発やスクラムの導入、サイト内検索の改善など様々な業務に 取り組んでいますが、最近の大きな案件としては、アクセスログ解析基盤の整備がありました。

nginxのアクセスログを分析しやすい環境を作るため、ElasticsearchとBigQueryにログを蓄積し始めたのですが、 その際に一番のキモとなるのは、みんな大好きfluentdです。

今回は、我々ハウテレビジョンがどのようにアクセスログを収集、保存しているのかについて、fluentdの設定を中心にご紹介します。

アクセスログ収集の目的

現在の我々のサービス環境を考慮すると、アクセスログの収集には下記2つの目的が存在します。

  1. アクセス情報をもとにユーザーの行動を解析
    • 閲覧履歴を用いたレコメンデーション等により、サービスをパーソナライズしたい
  2. 問題発生時の調査
    • 時間、IPアドレス、パスなどで目的のログをすぐに探し出し、調査を容易にしたい

どのツール、サービスを使うのか

1の目的を達成するためのツール、サービスは複数考えられますが、価格、性能の両面を考慮するとやはりBigQeuryが圧倒的です。 非常に安価かつ高速にログを蓄積、検索できます。 また用途の必要上、複雑なクエリを発行することも多いため、慣れ親しんでいるSQLライクなクエリが書けること、 クラウドサービスのため運用工数が抑えられることも大きなメリットです。

一方、BigQueryはクエリを発行する度に課金されるため、探索的、アドホックな検索には向いていません。 そのため2の目的にBigQueryを用いることは不適切です。

他方、検索条件は時間、IPアドレスなど単純であり、パフォーマンスもさほど要求されないため、 クラウド上の自社サーバにElasticsearch + Kibanaを自らインストールして運用しても、さほど運用工数はかからないでしょう。

上記の理由から、ユーザーの行動解析にはBigQuery、問題発生時の調査にはElasticsearch + Kibanaを採用しました。

どうやっているのか

f:id:fuyumi3:20140807145303p:plain

一応お絵かきしてみたものの、わざわざ図にするまでもないシンプルな構成です。

Webサーバ(nginx)からfluentdで直接それぞれにログを送っています。 Webサーバ1台で運用しているため、仲介サーバは設けていません。

td-agent.confの中身

各種ツール個別のセットアップ方法については既存の情報が豊富なため、ここでは省略します。

fluentdとElasticsearchあるいはBigQueryの連携についても探せばたくさん見つかるでしょう。 fluentdからログを単純に流すだけであれば、困難に突き当たることはないでしょう。

しかしながら我々の要件や、各ツール、サービスの使用上の制約から、気をつけなければならない点がいくつかあります。

  • BigQueryはストレージ課金+クエリ課金(走査したデータの分だけ課金される)ため、保存するデータは厳選したい
    • 静的ファイル等のユーザーの行動解析とは関係ないログはフィルタリングしたい
  • 同じ理由で、ログの保存先テーブルは1つではなく、時期によって分けることが望ましい
    • WHERE句で取得範囲を限定する場合でも、テーブル全体を走査することは変わりない
    • あらかじめ日毎、月毎などでテーブルを分けておけば課金を抑えられる
  • 障害調査のためには全てのログが欲しい
    • Elasticsearchに保存するログはフィルタリングをしない

このようにかなり複雑な要件が求められますが、幸いなことにfluentdには各種様々なプラグインが開発されており、 これらを上手く組み合わせることで全て実現が可能です。


それではお待ちかね、我々が使っているtd-agent.confをここで公開します。

(実際にはログのフォーマットをアレンジしているのでformatはapacheではなかったり、 フィルタリング条件ももう少し複雑なのですが、話が複雑になるので実際のファイルから少々アレンジしています)

<source>
  type tail
  format apache
  time_format %d/%b/%Y:%T %z
  path /var/log/nginx/access.log
  pos_file /var/log/td-agent/nginx.access.pos
  tag nginx.access
</source>

<match nginx.access>
  type rewrite_tag_filter
  rewriterule1 path   ^/(files|img|js|css)/ ${tag}.clear
  rewriterule2 path   ^/favicon\.ico/       ${tag}.clear
  rewriterule3 path   (.+)                  ${tag}.accept
</match>

<match nginx.access.accept>
  type record_reformer
  enable_ruby true
  tag ${tag}.${time.strftime('%Y%m')}
</match>

<match nginx.access.**>
  type forest
  subtype copy
  remove_prefix nginx.access

  <template>
    <store>
      type elasticsearch
      host 192.168.100.1
      port 9200
      type_name access_log
      logstash_format true
      logstash_prefix service_front_access
      logstash_dateformat %Y%m

      buffer_type memory
      buffer_chunk_limit 10m
      buffer_queue_limit 10
      flush_interval 1s
      retry_limit 16
      retry_wait 1s
    </store>
  </template>

  <case accept.*>
    <store>
      type bigquery
      method insert

      auth_method private_key
      email XXXXXXXXXX@developer.gserviceaccount.com
      private_key_path /etc/td-agent/XXXXXXXXXX.p12

      project XXXXXXXXXX
      dataset XXXXXXXXXX
      table nginx_${tag_parts[-1]}

      time_format %s
      time_field time
      schema_path /etc/td-agent/schema.json
    </store>
  </case>
</match>

順を追って解説していきます。

sourceディレクティブ

<source>
  type tail
  format apache
  time_format %d/%b/%Y:%T %z
  path /var/log/nginx/access.log
  pos_file /var/log/td-agent/nginx.access.pos
  tag nginx.access
</source>

sourceディレクティブについては特に説明する必要はないでしょう。 標準のtailプラグインでnginxのログをトラックしてnginx.accessというタグを付与するだけです。

ログのフィルタリング

<match nginx.access>
  type rewrite_tag_filter
  rewriterule1 path   ^/(files|img|js|css)/ ${tag}.clear
  rewriterule2 path   ^/favicon\.ico       ${tag}.clear
  rewriterule3 path   (.+)                  ${tag}.accept
</match>

1つ目のmatchディレクティブでは、ログのフィルタリングを行っています。 fluent-plugin-rewrite-tag-filterという、正規表現をもとにタグを付け替えられるプラグインを使います。

見ていてだければ大体の想像はつくかと思いますが、 pathが/files/, /img/, /favicon.icoなどで始まる場合(つまり静的ファイル)にはタグの末尾にclearを、 その他(ユーザーの行動に関係のあるログ)の場合にはacceptを追加します。

これにより、BigQueryに保存すべきログはnginx.access.accept、 除外するログはnginx.access.clearとなって次のmatchに渡されます。

月毎に動的にタグを変化させる

<match nginx.access.accept>
  type record_reformer
  enable_ruby true
  tag ${tag}.${time.strftime('%Y%m')}
</match>

先に挙げた理由から、BigQueryの保存先テーブルを月毎に変化させる必要があります。 例えば2014年8月のログなら保存先テーブルはnginx_201408、9月ならnginx_201409といった次第です。

この目的を達成するためには、月によってログに付与するタグを動的に変えてやる必要があるのですが、 そのためにfluent-plugin-record-reformer というプラグインを使います。 このプラグインを使うとログのフィールドやタグを自由に書き換えることができるのですが、 上記のようにenable_rubyをtrueにセットすると、任意のRubyのコードを使うことができます。

そこで、time.strftime('%Y%m')の結果をログの末尾に追加することで、例えば2014年8月に処理されたログであれば、 nginx.access.accept.201408とタグを書き換えることができます。

こうして末尾に付与した年月によって、次のmatchディレクティブで保存するテーブルを変えていきます。

<match nginx.access.accept>としているため、BigQueryから除外するログ(nginx.access.clear)についてはここでは処理が行われません。

ElascticsearchとBigQueryにログを送る

<match nginx.access.**>
  type forest
  subtype copy
  remove_prefix nginx.access

  <template>
    <store>
      type elasticsearch
      host 192.168.100.1
      port 9200
      type_name access_log
      logstash_format true
      logstash_prefix service_front_access
      logstash_dateformat %Y%m

      buffer_type memory
      buffer_chunk_limit 10m
      buffer_queue_limit 10
      flush_interval 1s
      retry_limit 16
      retry_wait 1s
    </store>
  </template>

  <case accept.*>
    <store>
      type bigquery
      method insert

      auth_method private_key
      email XXXXXXXXXX@developer.gserviceaccount.com
      private_key_path /etc/td-agent/XXXXXXXXXX.p12

      project XXXXXXXXXX
      dataset XXXXXXXXXX
      table service_nginx_${tag_parts[-1]}

      time_format %s
      time_field time
      schema_path /etc/td-agent/schema.json
    </store>
  </case>
</match>

ようやく最後のmatchディレクティブに来ました。ここでは

  • すべてのログをElasticsearchに流す
  • タグにacceptがついたログだけはBigQueryに流す
  • タグの末尾の値(201408など)に応じて、BigQueryの保存先テーブルを変化させる

という、少々複雑な操作が必要となります。

複数の宛先へログを送るには、標準のcopyプラグインを使います。 タグの値によって動的に保存先テーブルを変化させるのには、fluent-plugin-forestを利用します。

  <template>
    <store>
      type elasticsearch
      host 192.168.100.1
      port 9200
      type_name access_log
      ...略
    </store>
  </template>

templateセクションにはマッチした全てのログに対する共通の処理を記述します。 Elasticsearchには全てのログを保存しておきたいため、ここにはElasiticsearchに保存する処理を記述します。

  <case accept.*>
    <store>
      type bigquery
      method insert

      auth_method private_key
      email XXXXXXXXXX@developer.gserviceaccount.com
      private_key_path /etc/td-agent/XXXXXXXXXX.p12

      project XXXXXXXXXX
      dataset XXXXXXXXXX
      table nginx_${tag_parts[-1]}

      time_format %s
      time_field time
      schema_path /etc/td-agent/schema.json
    </store>
  </case>

caseセクションは一般的なプログラミング言語のswitch-case文と同じく、その条件にマッチするログに対してのみ行う処理を記述します。 今回の場合、BigQueryに保存するのはnginx.access.accept.201408, nginx.access.accept.201409, ...等のタグがついたログですので、accept.*で条件を指定します (nginx.accessの部分はremove_prefixで除去済みです)。

table service_nginx_${tag_parts[-1]}

最後に、この部分がキモとなります。${tag_parts[-1]}とすることで、タグの末尾(accept.201408なら201408)の値を埋め込むことができます。 そのため、2014年8月のログならばnginx_201408, 9月のログならばnginx_201409のテーブルといった次第に格納先のテーブルを変化させることができるのです。

まとめ

以上、fluentdを使ってnginxのログをElasticsearchとBigQueryに保存する方法についてご紹介しました。

fluentdはここ1, 2年の間に大流行し、早くも定番となった感のあるプロダクトですが、 なるほど使えば使うほど、その柔軟性、設定の容易さ、エコシステムの広さに驚嘆するばかりです。

おかげでログを収集、保存することが容易になった一方で、ただ貯めておくだけでは何の価値も生み出しません。 我々の解析基盤は出来たてのホヤホヤであり、これからどんどんデータが蓄積されていきます。 ある程度データが溜まった段階で様々な施策に活かしていきますので、その過程や結果について、またここでご紹介できればと思います。