nginx+luaで動作するルーティングライブラリ lua-resty-r3 を書きました

## lua-resty-r3

https://github.com/toritori0318/lua-resty-r3
nginx+lua上で動作するURLルーティングライブラリです。
lua+ffiでr3をバインディングしています。

## r3(libr3)について

リポジトリはこちら。
https://github.com/c9s/r3

高速なルーターライブラリということを謳っているようです。
perl/python/ruby/node.js/Haskell などのバインディングが存在しています。

## 書いた動機

- nginx + lua でいい感じのルーティングモジュールが欲しかった [^1]
- より高速なルーティングがしたかった
- ffi モジュール書いてみたかった

## 実装方針

libr3には様々なAPIがありますが、今回はroutingに特化したAPIのみ対応しています [^2]
また、マッチした時の戻り値は functionブロックのみ対応しています。

さらにlibr3は比較的低レイヤー（matchする箇所など）を担当しているため
そのままラップしただけのものでは使いづらいものになってしまいます。
そのためlua-resty-r3のインタフェースは自分でわりと好きに定義してみました。

## 使い方

ほぼこちらそのままですが。。
https://github.com/toritori0318/lua-resty-r3#synopsys

```lua
-- マッチした時に実行されるハンドラ例
function foo(tokens, params)
  ngx.say("fooooooooooooooooooooooo")
end


-- r3router
local r3router = require "resty.r3";

-------------------------------------------------------------
-- route登録パターン１：newした後で明示的に get / post などを指定
-------------------------------------------------------------
local r = r3router.new()
-- routing
r.get("/", function(tokens, params)
  ngx.say("hello r3!")
end)
r.get("/foo", foo)
r.get("/foo/{id}/{name}", foo)
r.post("/foo/{id}/{name}", foo)
-- パターン１の時はcompileを忘れずに！
r.compile()

-------------------------------------------------------------
-- route登録パターン２：初期化時にルーティング直接指定しちゃう
--           こっちがオススメかも
-------------------------------------------------------------
local r = r3router.new({
    {"GET",          "/",                function(t, p) ngx.say("hello r3!") end },
    {"GET",          "/foo",             foo},
    {{"GET","POST"}, "/foo/{id}/{name}", foo},
})

-------------------------------------------------------------
-- dispatch_ngx() ngx変数から自動でそれっぽく処理してくれる
-------------------------------------------------------------
local ok = r:dispatch_ngx()

-------------------------------------------------------------
-- dispatch() 自分で明示的に渡すのもOK
-------------------------------------------------------------
local ok = r.dispatch(ngx.var.request_method, ngx.var.uri, ngx.req.get_uri_args(), ngx.req.get_post_args())

if not ok then
  ngx.status = 404
  ngx.print("Not found")
end
```

## ベンチマーク

スクリプトは[こちら](https://github.com/toritori0318/lua-resty-r3/tree/master/benchmark)に置いてます。
比較対象は [router.lua](https://github.com/APItools/router.lua) です。

### 環境

EC2のc4.largeで検証しました。

### dispatchのみ

resty-cli を利用し以下の様に実行しました。短いほど良い結果です。
dispatchを1000万回ループしています。

``resty test_lua_resty_r3.lua``

項目| get / | get /foo/bar/baz/hoge/fuga/piyo | get /foo/?/?
---|-------|---------------|---------
lua-resty-r3 | 1.35 sec | 2.87 sec | 5.19 sec
apitools-router | 3.46 sec | 19.46 sec | 14.85 sec

r3速いですね！

### HTTP経由

wrk を利用しHTTP経由で以下の様に実行しました。大きいほど良い結果です。

``wrk -t10 -c200 -d15s http://127.0.0.1:80/...``

項目| get / | get /foo/bar/baz/hoge/fuga/piyo | get /foo/?/?
---|-------|---------------|---------
nginx | 45375 req/sec | 41675 req/sec | 44270 req/sec
lua-resty-r3 | 36382 req/sec | 34591 req/sec | 32827 req/sec
apitools-router | 41918 req/sec | 36967 req/sec | 38844 req/sec

(; ・∀・) あ、あれ、遅くなってる… 
試しに１リクエスト内で複数dispatchループすると逆転するので、
おそらくリクエスト受けた時の処理が重いんでしょうねー。
また当初クラス化してないバージョンで書いてたのですが、試しにそのバージョンで動かしてみたらもっと高速に動作しました。
もしかしたらクラスオブジェクト化するところが重いのかも。。

## まとめ

nginx + lua 上で高速に動作する r3バインディングを書いてみました。
http経由だと遅いという残念な結果になってしまいましたが
改修してリベンジしたいと思います…
またffiで書いたの初めてなので変なところがあればツッコミお願いします:)

[^1]: Luaで同じようなモジュールだと、APItoolsの[router.lua](https://github.com/APItools/router.lua)があります。

[^2]: 本当はinsert_pathやmatch_pathにも対応してたんですが、使わなさそうなので消しちゃいました :)