自作でイベント駆動型サーバ作るのツライ問題とlua-nginx-module

Posted by Nakamura Narihiro on March 23, 2016 · 1 min read

何の因果かわかりませんが、お仕事でちょっと賢いリバースプロキシサーバ(以降、RPサーバ)を作る機会が2回ありました。
HTTPヘッダの内容によってプロキシ先のサーバを動的に切り替えるようなものです。

ちょっと賢いリバースプロキシ

この要件を満たすため、RPサーバには以下のようなプログラムが必要になります。

  • HTTPヘッダの内容を知るためにHTTPリクエストをパース
  • プロキシ先のサーバへHTTPリクエストをプロキシ
  • プロキシ先のサーバはRedisから取得
  • レスポンスをクライアントへ返す
  • 大量のリクエストも捌ける

1回目はRubyとI/O多重化のライブラリを使ってイベント駆動型のRPサーバを自作してみました。
が、振り返ってみるとこれは失敗でした。

なぜ失敗だったのか?

Rubyでイベント駆動型のサーバを書こうとすると様々なものが途端に大変になります。

イベント駆動型サーバではほとんどの箇所でブロックする処理を書けません。
たとえばPostgreSQLにアクセスしようと思ったら、使い慣れたruby-pgは使わないほうが好ましいでしょう。
ruby-pgが使うソケットはブロッキングI/Oのためプロセス全体を止めてしまう恐れがあるからです。
では何を使うかというと、em-pg-clientなどのノンブロッキングなライブラリを利用することになります。

実際にやってみるとわかるんですが、ブロックする処理を書けないというのはRubyではけっこうしんどいです。
色んな所で気を使わないといけないですし。

また、「em-xxx」というgemが乱立していることでなんとなく雰囲気がわかると思うんですが、これらのライブラリ群には未成熟なものが結構あります。
前述した自作RPサーバでは要件を満たすために、ライブラリの細かいところに手を入れる羽目になり、かなり大変でした。

二度目の正直

また同じようなものを作る機会があったので、前回の失敗の反省をいかせないかと悩んでいました。
ある程度リクエストを捌けて、プロキシできて、HTTPが喋れて、そんなものがあったら…となったときに思いついたのがnginxです。

nginxを利用する際にネックとなるのがリバースプロキシを「ちょっと賢く」する部分です。
プロキシ先のサーバをRedisに問い合わせる部分をどうにかしないといけません。
そのためだけにnginxの拡張モジュールを書くのもめんどいな…と思っていた時に見つけたのがlua-nginx-moduleでした。

lua-nginx-module と openresty

lua-nginx-moduleとはLuaで簡単にnginxの拡張が書けてしまうライブラリです。
これでRedisへ接続する部分を書くことができそうです。

また、nginxとlua-nginx-moduleなどのライブラリ群を簡単にインストールできるようにパッケージングしたopenrestyというものがあり、実際のインストールはopenrestyを使って行うこととなります。

lua-nginx-module VS ngx_mruby

同じようにnginxの拡張をRubyで書けるngx_mrubyというものがあります。
ここで両者の性能を比較してみましょう。
公式サイトのベンチマークによると単純な文字列を生成するWebサーバではngx_mrubyの方が速いとのことでした。

今回は、Redisを利用する簡単なRPサーバを作成し、そのスループット (処理したリクエスト数の秒平均値) をabで計測します。
利用したコード類はこちらです。

var options = {
  width: 800,
  height: 400,
  hAxis: {
    title: 'リクエスト数(n=1000固定),クライアント数(c)',
    titleTextStyle: { color: '#333' },
  },
  vAxis: {
    title: '[#/sec]',
    titleTextStyle: { color: '#333' },
    maxValue: 1800
  }
};
var chart = new google.visualization.ColumnChart(document.getElementById('ngx_lua_vs_mruby'));
chart.draw(data, options);

}
google.charts.setOnLoadCallback(drawNgxLuaVsMrubyChart);

lua-nginx-moduleの方がスループットがでており、クライアント数が増えるに連れてその差は顕著になります。
なぜこのような結果になったのか考察してみましょう。

ノンブロッキングI/Oの差

実はここにも前述した「自作イベント駆動型サーバ作るのツライ」と同じ種類の問題があります。

nginxはイベント駆動型のサーバです。各プロセスはI/O多重化により複数のHTTPリクエストを受け取り、同一のプロセスで処理します。
そのため、その拡張のmrubyやluaのコードの中でブロックする処理を書いてしまうと、処理中のプロセス全体がストップしてしまうことになります。
その場合は他のHTTPコネクションに迷惑をかけたり、リクエストを受け付けられなかったりする可能性があります。

ngx_mrubyの場合はRedisに接続しているソケットがブロッキングI/Oであり、Redisにデータを問い合わせる際のIO待ちなどで処理がブロックしてしまいます。
そのため、効率よく複数のリクエストを捌けない場面がでてきます。

一方、 lua-nginx-moduleではノンブロッキングI/Oのソケットライブラリが提供されており、これを利用してRedisクライアントが実装されています。
そのため、Redisにデータを問い合わせてもIO待ちなどでブロックする恐れがありません。
結果的に効率よくリクエストを捌くことができ、サーバの性能が引き出すことができます。

lua-nginx-moduleはLuaで拡張が書けるだけではない

lua-nginx-moduleの偉いところはこの「ノンブロッキングI/Oの提供」と「それを利用したライブラリ群の豊富さ」にあると筆者は思います。
単にLuaで拡張が書けるわけではなく、サーバの性能を最大限に引き出せるようなお膳立てもしてくれると言う点がngx_mrubyと大きく異なります。
もちろんngx_mrubyでも同様の機能を提供し、新しくngx_mruby用にRedis用ライブラリとかを作りなおせば同じことはできますが、それはなかなか大変そうです。

lua-nginx-module付近のライブラリ群の多さはopenrestyのgithubページを見ればわかります。
正直、最初に見た時は「うわー、がんばってるな!」と思いました。
と、同時に「em-xxx」の悪夢も脳裏をよぎりましたが、処理の核となる部分はnginxがやっているし大丈夫…かもしれません。
少なくとも筆者の用途だと、Rubyの自作RPサーバよりコード量が1/10程度になったので格段にメンテナンスはしやすくなりました。

また、今回のケースではRedisを利用したためにこのような結果になりましたが、ブロックしない処理を書く場面であればngx_mrubyを利用してもよいでしょう。
Rubyでnginxの拡張が書けるという選択肢を与えてくれるngx_mruby自体はとても素晴らしいものだと考えています。

まとめ

今回の記事をまとめると以下の三行となります。

  • openresty(lua-nginx-module)はスゴイよ
  • イベント駆動型サーバは作るとツライよ
  • 関係ないけどgolangもスゴイよ

(こういうのはgolangで書くのも手だと思います)