nginx-lua から fluentd へログを送信する

[nginx Advent Calendar 2015](http://qiita.com/advent-calendar/2015/nginx) 7日目 兼 [fluentd Advent Calendar 2015](http://qiita.com/advent-calendar/2015/fluentd) 6日目のエントリです。
nginx-lua (https://github.com/openresty/lua-nginx-module) から fluentd にログを送信する方法を紹介します。

Lua から fluentd へログを送信するライブラリとして [fluent-logger-lua](https://github.com/united-adstir/fluent-logger-lua) というものがありますが、これは [LuaSocket](http://luaforge.net/projects/luasocket/) ライブラリを使用しているため nginx-lua では使いづらいので、[ngx.socket.tcp](https://github.com/openresty/lua-nginx-module#ngxsockettcp) を使用して自前で送信してみましょう。

fluentd の forward protocol は3パターンの入力フォーマットがあるのですが、一番単純なフォーマットは `[tag, time, event]` の形式で、この形に MessagePack のバイナリを組み立てて TCP でそのまま流すだけでログを送信することができます。

例として「リクエストされた URL引数、User-Agent、アクセス元IPアドレスを fluentd へ送信する」というアプリケーションを書いてみます。

```lua
msgpack = require("msgpack")

local sock = ngx.socket.tcp()
sock:settimeout(5000)

-- URL引数を取得
local args, err = ngx.req.get_uri_args()
if not args then
   return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

-- 127.0.0.1:24224 の fluentd に接続
local ok, err = sock:connect("127.0.0.1", 24224)
if not ok then
   return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end

args["user_agent"] = ngx.var.http_user_agent
args["remote_addr"] = ngx.var.remote_addr

-- forward 形式のメッセージを組み立てて投げる
local bytes, err = sock:send( msgpack.pack({"access", ngx.time(), args}) )
if err then
   return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
sock:setkeepalive(10000) -- keepalive 10 sec.

-- レスポンスを返す
ngx.header.content_type = "text/plain"
ngx.say("ok")
```

MessagePack のライブラリとして [lua-MessagePack](https://github.com/fperrad/lua-MessagePack) を使用しますので、あらかじめ `lua_package_path` の場所に配置してください。

これで、適当に URL 引数を付けてリクエストすると fluentd に送信するアプリケーションができました。Webビーコン的なものですね。

ポイントは以下です。

- 時刻取得に `os.time()` ではなく `ngx.time()` を使用する
  - システムコールによるオーバーヘッドを避けることができます
- `sock:setkeepalive()` で TCP 接続を使い回す
  - 指定した時間、socket を close せず別のリクエストに使い回すことができます

### ベンチマーク

- Macbook Pro 13 (Early 2015)
- fluentd-0.12.16
- ruby 2.2.2p95
- openresty/1.9.3.1
- wrk 4.0.0

上記環境で、ローカルマシンで簡易的なベンチマークを取ってみました。
fluentd は [flowcounter-simple](https://github.com/sonots/fluent-plugin-flowcounter-simple) を使用して、流れてくるログの流量を集計します。

```
# fluentd.conf
<source>
  type forward
  port 24224
</source>
<match access.**>
  type flowcounter_simple
  unit second
</match>
```

結果

```
$ wrk -c 50 -d 10 -t 4 "http://localhost:8000/?foo=bar&bar=baz"
Running 10s test @ http://localhost:8000/?foo=bar&bar=baz
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.20ms   13.79ms 247.88ms   92.33%
    Req/Sec     7.97k     2.16k   14.03k    67.94%
  270322 requests in 10.08s, 44.84MB read
Requests/sec:  26809.95
Transfer/sec:      4.45MB
```

```
2015-12-07 15:09:36 +0900 [info]: plugin:out_flowcounter_simple	count:8571	indicator:num	unit:second
2015-12-07 15:09:37 +0900 [info]: plugin:out_flowcounter_simple	count:30995	indicator:num	unit:second
2015-12-07 15:09:38 +0900 [info]: plugin:out_flowcounter_simple	count:33546	indicator:num	unit:second
2015-12-07 15:09:39 +0900 [info]: plugin:out_flowcounter_simple	count:72508	indicator:num	unit:second
2015-12-07 15:09:40 +0900 [info]: plugin:out_flowcounter_simple	count:24412	indicator:num	unit:second
2015-12-07 15:09:41 +0900 [info]: plugin:out_flowcounter_simple	count:30495	indicator:num	unit:second
2015-12-07 15:09:42 +0900 [info]: plugin:out_flowcounter_simple	count:32792	indicator:num	unit:second
2015-12-07 15:09:43 +0900 [info]: plugin:out_flowcounter_simple	count:35525	indicator:num	unit:second
2015-12-07 15:09:44 +0900 [info]: plugin:out_flowcounter_simple	count:32710	indicator:num	unit:second
2015-12-07 15:09:45 +0900 [info]: plugin:out_flowcounter_simple	count:2314	indicator:num	unit:second
```

2コア4スレッドのマシンで nginx `worker_processes 4` で実行したところ、26000req/sec 程度の性能でした。
wrk, nginx, fluentd がそれぞれ CPU を食い合っている状態なのであくまで参考程度ですが、たいていのアプリケーションには十分すぎる性能が出ていると思います。

## 注意点など

上記サンプルアプリケーションでは、接続先の fluentd が落ちていたりしてログが送信できない場合、503 を返すだけになっています。可用性を高めるためには、送信できない場合に別の fluentd に送るなどの対処が必要でしょう。

また、各種 fluentd の logger には送信できなかったログはメモリ上にバッファリングして次回接続時に再送するなどの機能があるものがありますが、nginx上でリクエストをまたいでメモリ上に確保するのは [ngx.shared.DICT](https://github.com/openresty/lua-nginx-module#ngxshareddict) を使用するなどの工夫が必要そうです。