H2Oにmruby拡張の提案を行いmergeされてから幾つかのPRを経て、少しずつ実用的になってきました。
今日は簡単にその使い方を紹介しようと思います。
h2o_mrubyを有効化したh2oをビルド
h2o_mrubyを有効化してビルドするのは簡単で、OSのライブラリ環境(/usr/lib/
以下とか)にlibmruby.a
等のmrubyライブラリがある状態で、
cmake -DWITH_MRUBY=ON . make h2o
するだけで、h2o_mrubyが有効化されたh2oバイナリがカレントにビルドされます。簡単ですね。
またこの記事も参考にすると良いかもしれません。
使えるメソッド
mod_mrubyやngx_mruby程メソッドはまだ充実していませんが、少しずつ使えるものを実装しています。また、mod_mrubyやngx_mrubyとの互換性を考慮したクラス・メソッド設計にしています。
H2Oクラス
H2Oクラスは以下のメソッドを持ちます。
H2O.return
このメソッドは、任意のステータスコードとレスポンスを返したり、またmrubyハンドラ自身でレスポンスを返さずに、次のハンドラに処理を渡す(DECLINED
)事ができます。
例えば、クライアントに404
を返したい場合は、
H2O.return 404, "not found", "Your request file was not found on this server"
とかけます。
次のハンドラに処理を渡す場合は、単に、
H2O.return H2O::DECLINED
と書くだけで良いです。この意味については後述します。
H2O::Requestクラス
主にクライアントからのリクエストを受けて処理する間に得られるパラメータを操作するメソッドを定義しています。少し数が多いので、最初にコードの一例を紹介します。
# curl http://127.0.0.1:58080/index.html?a=1 -H User-Agent:curl-dayo r = H2O::Request.new r.hostname #=> "127.0.0.1" r.authority #=> "127.0.0.1:8080" r.uri #=> "/index.html?a=1" r.path #=> "/index.html?a=1" r.method #=> "GET" r.qeury #=> "?a=1" # If not found query, r.query return nil # request headers r.headers_in["User-Agent"] #=> "curl-dayo" r.headers_in["User-Agent"] = "new-#{r.headers_in["User-Agent"]}" # response headers r.headers_out["hoge"] #=> nil r.headers_out["hoge"] = "fuga" r.log_error "Accept request to #{r.uri}" H2O.return H2O::DECLINED
H2O::Request#hostname
リクエストのあったhostnameを得られます。ポートを除いたホスト名です。
H2O::Request#authority
リクエストのあったauthorityを得られます。ポートも含んだホスト情報になります。
H2O::Request#uri
リクエストのURLが得られます。この値はunparsedなURLでqueryも込みのURLになります。
H2O::Request#path
H2O::Request#uri
と同じ処理です。
H2O::Request#method
GETやPOSTといったHTTPのメソッドを得られます。
H2O::Request#query
H2O::Request#uri
における?
以降のqueryパートを得られます。
H2O::Request#{headers_in,headers_in=}
リクエストヘッダの値を取得したり生成・上書きすることができます。上記例では、例としてUser-Agent
を上書きしています。これによって、例えばアクセスログのUser-Agent
箇所は新しいnew-
prefixの付いた値になっているはずです。
H2O::Request#{headers_ouot,headers_out=}
レスポンスヘッダの値を取得したり生成・上書きすることができます。H2O.return H2O::DECLINED
と組み合わせることでレスポンスヘッダを追加したレスポンスを返す事ができます。
ざっと紹介すると以上のようなメソッドになります。これだけでも、アクセス制御のような処理は簡単に実装できそうなのが想像できたでしょうか。
その他細かい仕様
- Rubyコードの戻り値が
nil
だった場合は次のハンドラに処理を渡す - Rubyコードの戻り値が文字列だった場合は200ステータスコードと共にその文字列をレスポンスとして返す
- ただし、
H2O.return
メソッドが書かれていた場合はその処理が優先される
という仕様です。明示的にH2O.return
を書いておくと混乱しないと思います。
アクセス制御の実装例
ということで、最後はよりイメージしやすくするためにアクセス制御の実装例を紹介します。
やりたい事としては、
warui-ua
というUser-Agentからアクセスがあったら403を返す- それ以外は普通にレスポンスを返す
という処理だったとします。
ではコードと設定例を紹介します。
- Rubyコード(warui.rb)
r = H2O::Request.new if r.headers_in["User-Agent"].to_s == "warui-ua" r.log_error "denied #{r.uri}" H2O.return 403, "Forbidden", "your user-agent is warui\n" else H2O.return H2O::DECLINED end
- h2o.conf
listen: 8080 hosts: "127.0.0.1.xip.io:8080": paths: /: file.dir: examples/doc_root mruby.handler_path: examples/h2o_mruby/warui.rb access-log: /dev/stdout
上記のようになります。
この状態で、以下のようにアクセスすると、
$ curl 127.0.0.1:8080/ -v * About to connect() to 127.0.0.1 port 8080 (#0) * Trying 127.0.0.1... connected * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.19.7 > Host: 127.0.0.1:8080 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 22 Jul 2015 00:05:44 GMT < Server: h2o/1.4.0-alpha1 < Connection: keep-alive < Content-Length: 177 < content-type: text/html < last-modified: Tue, 28 Apr 2015 05:21:29 GMT < etag: "553f18d9-b1" < accept-ranges: bytes < <!DOCTYPE html> <html> <head> <title>Welcome to H2O</title> </head> <body> <p>Welcome to H2O - an optimized HTTP server</p> <p>It works!</p> </body> </html> * Connection #0 to host 127.0.0.1 left intact * Closing connection #0
悪いUser-Agentじゃないので、普通にindex.htmlのレスポンスが帰ってきます。これはmrubyハンドラがレスポンスを返すのではなく、H2O.return H2O::DECLINED
によって処理を次のハンドラに渡しているからです。
つまり、headers_out
によって新しいレスポンスヘッダを追加していると、このindex.htmlのレスポンスのレスポンスヘッダに任意のヘッダを追加できたりします。
一方で、以下のようにアクセスすると、
$ curl 127.0.0.1:8080/ -H User-Agent:warui-ua -v * About to connect() to 127.0.0.1 port 8080 (#0) * Trying 127.0.0.1... connected * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > Host: 127.0.0.1:8080 > Accept: */* > User-Agent:warui-ua > < HTTP/1.1 403 Forbidden < Date: Wed, 22 Jul 2015 00:05:27 GMT < Server: h2o/1.4.0-alpha1 < Connection: keep-alive < Content-Length: 25 < content-type: text/plain; charset=utf-8 < your user-agent is warui * Connection #0 to host 127.0.0.1 left intact * Closing connection #0
というように、謎メッセージと共に403がきちんと返って来ました。ちゃんとアクセス制御できていますね。
ログにも以下のように出力されていました。
127.0.0.1 - - [21/Jul/2015:11:41:19 +0900] "GET / HTTP/1.1" 200 177 "-" "curl/7.37.1" [h2o_mruby] in request:/:denied / 127.0.0.1 - - [21/Jul/2015:11:41:35 +0900] "GET / HTTP/1.1" 403 25 "-" "warui-ua”
うまく動いてそうですね。
ベンチマーク
さて、最後にこのアクセス制御がどの程度オーバーヘッドがあるかを計測してみましょう。今回はHTTP/1.xとHTTP/2両方で評価したいと思います。設定と使用するコードは上記の設定からログ出力を除いたものにします。また、ベンチマーク環境は、IIJGIOのV240を使いました。
アクセス制御の処理は常に実行されるので、通常のUser-Agentでアクセスしindex.htmlが返ってくる処理に対してベンチマークを行い、それに対して、まったくmrubyの処理が実行されずにindex.htmlが返ってくる場合と比較しました。
- HTTP/1.xのベンチマークコマンド
ab -k -c 100 -n 100000 http://127.0.0.1:8080/
- HTTP/2のベンチマークコマンド
h2load -m 100 -c 100 -n 1000000 http://127.0.0.1:8080/
以下結果です。(値はrequests/secです)
\ | HTTP/1.x | HTTP/2 |
---|---|---|
h2o_mruby | 40441.10 | 212225 |
h2o | 40495.10 | 214328 |
上記のように、大きな性能劣化は見られませんでした。むしろ、何度かベンチマークをかけているとmruby側が性能速かったりと、基本的にはこの上記の性能差は誤差の範囲でありほぼ同等の性能といえるでしょう。mrubyはH2O起動時に既にコンパイルされ、各スレッド単位でバイトコードの状態で保持しているので、HTTPサーバの処理から見るとほとんどオーバーヘッドにはならない処理のように思えます。とはいえ、Rubyの実装の仕方や規模の増加に伴って、この辺りは左右されていく事は言うまでもありません。
ですが、index.html程度の軽量な処理に対して、mrubyハンドラを処理しているにも関わらずほとんどオーバーヘッドにならない結果は、比較的良い結果といえるでしょう。
まとめ
ということで、h2oのコアに取り込まれているh2o_mruby機能の実用的な使い方について紹介しました。また、性能面においてもそれほど気にしなくても良いレベルでの実装になっていると思います。
是非一度、h2oを触ってみるついでにh2o_mruby機能も有効にして色々と試して頂けるとおもしろいと思います。
今後はH2O::Server
クラスやH2O::Connection
クラスを実装して、H2Oの設定を取得したりコネクション情報を元に制御できるようにしていく予定です。
後は、プロキシまわりのサポートやそれにともなってfilterモジュールの実装も進むとより実用性が増しそうですね!