人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

ngx_mruby v2のHTTPクライアントをv1よりも最大90倍高速にした

写真のような感じでRubyKaigi2018で登壇し、RubyKaigiを経て、ようやくngx_mrubyのv2をリリースしました。基本的にv1と互換性がありますので、今後はv2を開発していくことになります。

github.com

ngx_mruby v2の目玉機能としては、Rubyスクリプトからノンブロッキングのsleepとhttp[s]クライアントを使えるようになったことです。実装的には、nginxのsub requestという機能をうまく使って、ノンブロッキングのhttp[s]クライアントを汎用的なsub_requestメソッドとして実現しています。

では、本エントリではそのノンブロッキングhttpクライアントがどの程度高速処理可能になったかを実験してみましょう。また最後には、RubyKaigi2018の感想も述べます。

実験

実験では、ngx_mrubyで構築したproxyとapiサーバを用意します。その上で、proxyもapiもworkerプロセス1個で起動させます。そして、proxyのngx_mrubyからv1で従来使っていたmruby-httprequestを利用して、apiサーバに情報をhttpで取りに行って、そのレスポンスを利用して何らかの処理をするようなRubyスクリプトを想定します。今回はシンプルに、apiサーバから返ってきたレスポンスbodyを利用して、その内容を単にproxyのレスポンスとしてクライアントに返す処理にします。

次にngx_mrubyのv2を使って、同様の処理をノンブロッキングのhttpクライアント(sub request)を使って処理をします。これらをnginx及びngx_mrubyの設定にすると以下のようになります。

proxyサーバのblockingとnon-blockingのhttpクライアントの設定

worker_processes  1;
events {
    worker_connections  1024;
}

daemon off;
master_process off;
error_log logs/error.log warn;

http {
    include       mime.types;

    server {
        listen       58080;
        server_name  localhost;
        root /home/ubuntu/DEV/ngx_mruby/build/nginx/html/;

        location /sub_req_proxy_pass {
            proxy_pass http://127.0.0.1:48080/mruby;
            mruby_output_body_filter_code '
              a = 1
            ';
        }

        # non-blocking http client
        location /async_http_sub_request_with_proxy_pass {
            mruby_rewrite_handler_code '
              Nginx::Async::HTTP.sub_request "/sub_req_proxy_pass"
              res = Nginx::Async::HTTP.last_response
              Nginx.rputs res.body
            ';
        }

        # blocking http client
        location /http_request {
            mruby_rewrite_handler_code '
              r = HttpRequest.new
              Nginx.rputs r.get("http://127.0.0.1:48080/mruby")["body"]
            ';
        }
    }
}

/async_http_sub_request_with_proxy_pass locationがv2のノンブロッキングクライアントhttpクライアント機能により、一旦別のlocationにリクエストを投げてからlocation経由でapiサーバのレスポンスを取得しにいきます。一方、v1でやっていた方法はhttp_request locationで、mruby-httprequestからapiのURLを直接つつきにいき、レスポンスを取得して、それをクライアントに返します。

apiサーバの設定

次にapi側の設定は以下です。

worker_processes  1;
events {
    worker_connections  1024;
}

daemon off;
master_process off;
error_log logs/error.log warn;

http {
    include       mime.types;

    server {
        listen       48080;
        server_name  localhost;
        root /home/ubuntu/DEV/ngx_mruby/build/nginx/html/;

        # test for hello world and cache option
        location /mruby {
            mruby_rewrite_handler_code '
              Nginx::Async.sleep 10
              Nginx.echo "done"
            ';
        }
    }
}

/mruby locationにリクエストがあったら、任意のsleep(msec)をした後で、レスポンスを返します。これによって、レスポンスタイムを仮想的に変更させながら、ある程度実用的なレスポンスタイムを作りだし、ブロッキングとノンブロッキングhttpクライアントの性能比較を行います。この設定でも、ノンブロッキングsleepを使っていることから、workerプロセスは1つしか起動していませんが、リクエスト単位でsleepを行うことができます。従来のsleepはプロセス自体をブロックするため、同時に処理している全てのリクエストを止めてしまいますが、ノンブロッキングsleepは単一のプロセスであってもsleepが実行されたリクエストを止めるのみで、同時に処理している他のリクエストを邪魔しないので非常に効率が良いです。

では実験してみましょう。この実験環境は、Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHzを2コア、メモリを8GB割り当てたvagrant環境にLinux ubuntu-xenial 4.4.0-124-genericをインストールしてngx_mrubyを起動します。クライアントはabコマンドを利用しました。

実験結果

以下の二種類の実験を行います。

  1. api側のレスポンスタイムを1msecから50msecまで変化させながら、同時接続数100でベンチマークをかけてRequest/secを計測する
  2. api側のレスポンスタイムを10msecに固定し、同時接続数を1から100に変化させてRequest/secを計測する

このような条件でブロッキングhttpクライアント(v1)とノンブロッキングhttpクライアント(v2)の性能比較を行いました。

レスポンスタイムを変化させたベンチマーク

まず、実験1のレスポンスタイムの変化に応じたベンチマーク結果は以下のようになりました。

f:id:matsumoto_r:20180606022214p:plain

レスポンスタイムが短い時は、リクエストのブロッキングの影響がでにくいためブロッキングhttpクライアントでも数千のrequest/secが出ていますが、10msecとレスポンスタイムが増えてくると、それに応じてmrubyの実行がブロッキングとなりシーケンシャルな処理になるため、request/secが格段に遅くなっていきます。一方で、ノンブロックhttpクライアントの場合は、同時にリクエストを処理できるため、高い性能が出ていることがわかります。

同時接続数を変化させたベンチマーク

次にレスポンスタイムが10msecで、同時接続数を変更させていった場合のベンチマーク結果を見てみましょう。

f:id:matsumoto_r:20180606022237p:plain

こちらの結果でも、同時接続数を上げるたびに、ノンブロックhttpクライアントは並行処理可能なため、処理可能なrequest/secがどんどん増加していき高い性能を示しています。一方で、ブロッキングhttpクライアントは、同時接続数を上げても結局1つのリクエスト処理中のmrubyがブロッキング処理を行ってしますため、シーケンシャルな処理となってしまい性能がほとんど変わりません。便利ですね!

結果として、実験1においてapiサーバのレスポンスタイムが5msecぐらいかかる状況で同時接続数が100本あった場合に、v1のブロッキングhttpクライアントでは107req/sec出ていたのに対し、v2のノンブロッキングhttpクライアントでは6252req/sec出ていたことからv2のhttpクライアントは60倍高速であり、レスポンスタイムが45秒かかる場合で同時接続数が100本だった場合に、v1のhttpクライアントでは21req/sec、v2では1875req/secであったことなどから、この実験においては最大90倍程度v2のhttpクライアントの方が速くなりました。また、同時接続数がもっと増加したり、apiサーバのレスポンスが遅い状況でも、各リクエストが別のリクエストの処理に影響をうけることなくノンブロックに処理できるようになります。

まとめとRubyKaigi2018の感想

以上のようにngx_mrubyのv2のノンブロッキングhttpクライアントが以下に効率良く処理できるかを評価しました。これによって、proxy用途で動作しているngx_mrubyのRubyスクリプト内でどこかのapiにHTTPリクエスト投げたレスポンスから動作を動的に変える処理を実装した場合に、従来のv1では性能面で問題がありましたが、v2では気兼ねなくそのような処理が可能になります。

h2oやlua-nginx-moduleからみたときにはだいぶリリースが遅れましたが、nginxとRubyでブロッキングっぽく書きながらノンブロッキングに処理できるので、いろいろ触ってみていただけたらなと思います。ぜひご活用ください。

最後に、v2のリリースに向けて、RubyKaigi中も最後の最後まで実装を手伝ってくださった @pyama86 id:pyamax に感謝します。

このようなノンブロッキングの内部処理については、先日RubyKaigi2018で発表してきましたので、以下のスライドを御覧ください。

speakerdeck.com

この発表後に会場に来てくださっていた id:kazuhooku さんから、fiber yiledはRuby側でやるべきという指摘を頂き、そのように変更することで改めてCとRubyの境界をFiberで超える問題をmrubyの仕様内におさめることができました。kazuhoさんとは3年前のちょうどh2oがリリースされた後のRubyKaigiで色々お話ししようと思っていたけど、自分の体調不良で登壇をキャンセルして、せっかく会場まで来て頂いていたのにお会い出来なかったことをずっと悔やんでいたので、ようやくお会いして色々お話しできてよかったです。しかもなぜか名刺交換をしたりしてワイワイしていました。rack-based APIの話や、socketのノンブロックI/Fの話もできてこれからまたhttp+mrubyは面白くなっていきそうです。

kazuho++

こんな感じでRubyKaigiとかに登壇すると、twitter上でいろいろお話したことがあっても実際にお会いしたことがない人と直接いろいろ議論できるので、ほんと素晴らしいなと思いました。一度喋っていると、その人の雰囲気とかもわかるので、テキストのやり取りもやりやすくなるように思いますし、論文書くだけでなく定期的にこういうテックカンファレンスに参加したいなと思いました。

RubyKaigi++

RubyKaigiのなかで最も心に残った発表は、身内びいきなどではなくやっぱり@hsbtさんの発表で、Rubyのcommitterも開発者もhsbtさんのような人が周辺ツールやエコシステム、互換性などをいい感じに全部まとめて見てくれるから、快適に、そして何より楽しくプログラミングに集中できるのだなぁと思って、そうやってDeveloper Experienceを向上させていく取り組みに心打たれたのでした。こういう人たちにあらゆる開発者やソフトウェア、ひいてはサービスが支えられているのだなぁと。同時に柴田さんは柴田さんとして、自分の考えるエンジニアリングの道を高いレベルで突き進んでいるのだなぁと改めて思わされました。

hsbt++

あとは仙台という街が最高でしたね。街も綺麗で人もそれほど多くないし、飯もうまい。なんとなく福岡にも似ている気がして住みやすそうだなぁと思いました。飯についてはひたすら山のように毎日あらゆるタイプの牛タンを食べ続けました。そして最後に京都の新福菜館で修行したとするラーメン屋で締めるべく、うまいラーメンと炒飯も頂きました。うーむ最高です。牛タンを食べながら未来大の松原先生とOS周りの話が出来たりしてとても有意義でした。

ということで、皆様次はRubyKaigi2019@福岡でお会いしましょう!それまでに福岡をやばいところにしておきますね。