mrubyでHTTP/2のサーバとクライアントを書く

以前のエントリ「mrubyでSPDYやHTTP2通信」で、mrubyを使ってHTTP/2通信を行うクライアントを作りましたが、機能が最低限でした。

そこで、今回はmrubyで簡単なHTTP/2のサーバを動かせるようにしてみました。

mruby-http2の導入

mrubyでHTTP/2を動かすには、mrubyのbuild_config.rbにmruby-http2の行を追加してrakeするだけで簡単に導入できます。

[program lang=’ruby’ escaped=’true’]

MRuby::Build.new do ¦conf¦

  # ... (snip) ...

  conf.gem :github => 'matsumoto-r/mruby-http2'
end

[/program]

その後にmrubyのディレクトリ内で、

[program lang=’ruby’ escaped=’true’]

rake

[/program]

を実行すると、HTTP/2のサーバとクライアント機能を持ったmrubyバイナリが出来上がります。

mrubyでHTTP/2サーバを動かす

では、実際にmrubyでHTTP/2サーバを動かしてみましょう。まずは、安定性と性能を重視してコア部分を実装したので機能は少ないですが、そこそこの安定性と性能が得られたので今後は各種Webサーバ関連の設定機能を追加していきたいと思っています。

サーバは以下のように書くだけです。

[program lang=’ruby’ escaped=’true’]

root_dir = "/usr/local/trusterd"

s = HTTP2::Server.new({
  :port           => 8080,
  :key            => "#{root_dir}/ssl/server.key",
  :crt            => "#{root_dir}/ssl/server.crt",
  :document_root  => "#{root_dir}/htdocs",
  :server_name    => "mruby-http2 server",

  #
  # optional config
  #

  # debug default: false
  # :debug  =>  true,

  # tls default: true
  # :tls => false,

  # damone default: false
  # :daemon => true,
})

s.run

[/program]

このように書いて、mrubyバイナリでプログラムを実行するとHTTP/2サーバが起動します。

[program lang=’bash’ escaped=’true’]

./bin/mruby http2_server.rb

[/program]

上記で指定したドキュメントルートにコンテンツを置きます。今回は以下のようなindex.htmlを作りました。

[program lang=’bash’ escaped=’true’]

$ cat index.html
hello mruby-http2 world.

[/program]

mrubyで書いたHTTP/2クライアントでアクセスする

次は、上記で起動させたHTTP/2サーバに対して、同様にmrubyで書いたHTTP/2クライアントでアクセスしてみましょう。クライアントは以下のように書きます。

[program lang=’ruby’ escaped=’true’]

test_site = 'https://localhost:8080/index.html'
r = HTTP2::Client.get test_site

p r.response
p r.body
p r.request_headers
p r.response_headers
p r.status
p r.body
p r.body_length
p r.stream_id

[/program]

そして、mrubyバイナリでこのプログラムを実行すると以下のようにレスポンスが返って来ます。

[program lang=’ruby’ escaped=’true’]

{:body=>"hello mruby-http2 world.¥n", :body_length=>25, :recieve_bytes=>25.0, :response_headers=>{"server"=>"mruby-http2 server", ":status"=>"200"}, :frame_send_header_goway=>true, :request_headers=>{"user-agent"=>"mruby-http2/0.0.1", "accept"=>"*/*", ":authority"=>"localhost:8080", ":scheme"=>"https", "accept-encoding"=>"gzip", ":method"=>"GET", ":path"=>"/index.html"}, :stream_id=>1}
"hello mruby-http2 world.¥n"
{"user-agent"=>"mruby-http2/0.0.1", "accept"=>"*/*", ":authority"=>"localhost:8080", ":scheme"=>"https", "accept-encoding"=>"gzip", ":method"=>"GET", ":path"=>"/index.html"}
{"server"=>"mruby-http2 server", ":status"=>"200"}
200
"hello mruby-http2 world.¥n"
25
1

[/program]

きちんとレスポンスが返って来ますね。

続いて、クライアント側で各frameの処理フェーズでblockをコールバックしてみましょう。例えば以下のように書きます。

[program lang=’ruby’ escaped=’true’]

test_site = 'https://localhost:8080/index.html'

s = HTTP2::Client.new
s.uri = test_site
s.on_header_callback {
  p "header callback"
}
s.send_callback {
  p "send_callback"
}
s.recv_callback {
  p "recv_callback"
}
s.before_frame_send_callback {
  p "before_frame_send_callback"
}
s.on_frame_send_callback {
  p "on_frame_send_callback"
}
s.on_frame_recv_callback {
  p "on_frame_recv_callback"
}
s.on_stream_close_callback {
  p "on_stream_close_callback"
}
s.on_data_chunk_recv_callback {
  p "on_data_chunk_recv_callback"
}
r = s.get
p r.response

[/program]

今回は単純に各frameやsessionの処理フェーズでフェーズ名を出力するだけにしました。これで同様に上記のサーバにアクセスすると、

[program lang=’ruby’ escaped=’true’]

"recv_callback"
"on_frame_recv_callback"
"recv_callback"
"before_frame_send_callback"
"send_callback"
"on_frame_send_callback"
"before_frame_send_callback"
"send_callback"
"on_frame_send_callback"
"recv_callback"
"header callback"
"on_frame_recv_callback"
"recv_callback"
"on_data_chunk_recv_callback"
"on_frame_recv_callback"
"recv_callback"
"on_frame_recv_callback"
"on_stream_close_callback"
"recv_callback"
"before_frame_send_callback"
"send_callback"
"on_frame_send_callback"
{:body=>"hello mruby-http2 world.¥n", :body_length=>25, :recieve_bytes=>25.0, :response_headers=>{"server"=>"mruby-http2 server", ":status"=>"200"}, :frame_send_header_goway=>true, :request_headers=>{"user-agent"=>"mruby-http2/0.0.1", "accept"=>"*/*", ":authority"=>"localhost:8080", ":scheme"=>"https", "accept-encoding"=>"gzip", ":method"=>"GET", ":path"=>"/index.html"}, :stream_id=>1}

[/program]

このように各フェーズでコールバックされている事がわかります。今後はこのコールバック時に、内部の処理から値を取ってきたり、値を設定したりできるようにしようと考えています。また、同様の機能はより必要になってくるサーバにも実装する予定です。

また、HTTP/2のテストサイトとして、

[program lang=’text’ escaped=’true’]

https://http2.matsumoto-r.jp:58080/index.html

[/program]

に上記のmrubyで書いたHTTP/2サーバを起動させていますので、クライアントだけ試したい人などは是非活用してみてください。

簡単なベンチマーク

サーバの機能を実装したということで、ベンチマークをかけてみてどの程度の安定性と性能が出るのかを簡単に見てみたいと思います。

検証環境はVM上でCPU2コアでメモリ8GBです。といってもVM環境なのでピンとこないということで、この環境でnginxを動かした場合の性能との相対比較をしてみようと思います。

それぞれ、nginxにはweighttpで同時接続数100、総接続数10万、keepalive有りでworker processを1で動かしました。というのも、mruby-http2で動かすサーバはマルチworkerには対応していないため、シングルworkerで比較してみたかったからです。

mruby-http2で動かしたTLS有りと無しのHTTP/2サーバには、h2loadによって同時接続数100、1セッション当たりの並行ストリーム数100、総接続数10万でベンチマークをかけました。

リクエストのコンテンツは25バイトのhello worldレベルのテキストです。

以下、nginxの設定です。

[program lang=’apache’ escaped=’true’]

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile on;
    tcp_nopush on;
    access_log off;

    keepalive_requests 100000;
    keepalive_timeout 65;

    server {
        listen       81;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

[/program]

シングルworkerなのでaccept_mutex等の設定は省いています。

ではベンチマーク結果です。

mruby-http2 mruby-http2(TLS) nginx
request/sec 297178 45893 42929

このように、nginxよりもmruby-http2で書いたTLS有りのHTTP/2サーバの方がこの条件下においては性能が出ました。また、TLSを使っていない場合は297178req/secと軽量なファイルだと非常に性能がでました。この辺りの実験ログは引き続き行いgistにも結果をいくつか上げているので御覧ください。少し早すぎるので追試して頂けると非常に嬉しいです。

それなりに同時接続数を増やしたり大量にアクセスしても今の所落ちたりしていないので安定性も最低限あるように思います。Valgrindのメモリリーク等もありませんでした。

(実はここに至るまでには並行でリクエストをかけると落ちてしまうバグに悩まされて1日以上かけてそのバグを取り除いたのですが…)

まとめ

以上のように、mrubyで書いたHTTP/2サーバでもそれなりの性能と安定性が得られる事が分かりました。mruby-http2のコアのベース実装はこのような実装にしておいて、今後は各種設定機能を追加していきたいと思っています。

今後やりたいこととして、

  • サーバの各種frameやsession処理でのコールバック対応
  • Locationの設定等の各種Webサーバ設定対応
  • マルチworker対応

等を地道にやっていこうと思います。

それにしても、HTTP/2の難しい所をライブラリとして提供してくれているnghttp2は本当に素晴らしいです。