八発白中

はてなブログに引越しました。

Day 15: Woo

これは fukamachi products advent calendar 2016 の15日目の記事です。

今日はWooについて話します。

これがあらすじです。

twitter.com

Wookieの高速化

Wookieのボトルネックを解消してもNode.jsのパフォーマンスには今一歩届かない。そこで使っているイベントライブラリのlibevent2が遅いのではないか?という疑念が生まれました。libevent2がボトルネックならば、libevや、Node.jsの使っているlibuvに切り替えることでNode.jsと対等に戦えます。

するとWookieの作者のAndrewが、libuvに切り替える予定だ、というリプライをくれました。

twitter.com

その頃、libuvとlibevent2の有効なベンチマークは知る限りありませんでした。しかし、Node.jsがlibevから乗り換えたことを考えるとlibuvもlibev程度は高速であることが見込めます。そしてベンチマークによってはlibevはlibevent2より2倍近く高速ということがわかっていました。

つまり、黙って見てるだけでも2倍近く高速になるかもしれないという状況です。

Wookieのスリム化

で、黙ってみているのもつまらない。その間にできることはやっておきましょう。

WookieにはWebサーバーとしては不要の機能がいくつかついていました。URLディスパッチャやプラグイン機構などがそうです。これらがプロファイリングには出てこずとも積み重なって有意な差を生んでいるかもしれません。

それを検証するために、余分な機能を削ぎ落としたバージョンを作ろうと考えました。そのサーバーがClack-compatibleなAPIを持つならばClackハンドラのボトルネックもありません。

そして完成したのが「Woo」でした。削ぎ落としたと言ってもフォークではなくスクラッチから書き直したのですが、2日程度で完成しました。

twitter.com twitter.com

ここで実験して得た知見をWookieにフィードバックしたいという程度のおもちゃのようなプロダクトでしたが、最初のバージョンでもはや少しNode.jsより高速になりました。

URIパースの高速化

また、何度かのWooのプロファイリングでURIのパース部分に時間がかかっていることがわかってきました。当時広く使われていたPURIというライブラリは遅くはなくとも十分に高速と言えるものではありません。

そこで「QURI」という高速なURIパーサーを書き、これをPURIと置き換えました。

twitter.com

QURIについては明日のアドベントカレンダー16日目で詳しく書く予定なので今回は割愛します。

サムライトに入社

河西くんに声をかけられたのはこの頃でした。彼は当時サムライトのCTOをやっており、Node.jsの広告配信サーバーを開発していました。個人的にはCommon Lispが好きだったにも関わらず、Common Lispが遅かったため、やむなくNode.jsで開発を行っていたのです。不幸な境遇です。

しかし、僕がCommon Lispは速いということをNode.jsとWooの比較で実証しました。1.2倍程度の差ではありますが、言語として比較してもJavaScriptより抽象度の高く開発効率も良いCommon Lispで書かない理由はもはやありません。

最初はCommon Lispへの移行を手伝う程度に考えていたのですが、話を聞いているうちに入社したほうがよかろうと思い、サムライトに入社することになりました。

サムライトに入社しても僕の仕事が大きく変わるわけでもありません。引き続きWooの高速化と安定化、他のWeb系ライブラリの開発、不具合や質問があれば迅速に対応するといったことが僕の仕事でした。

とはいえ、のんきにはしていられません。

サムライトは広告配信システムを開発しています。同時に大規模なリクエストがサーバーに飛んできます。つまりは思っていたよりも早くWooのパフォーマンスを試せる環境を得たわけで、これが対外的にCommon Lispの優位性をアピールできる舞台でもあるわけです。

このチャンスを逃すまいと、僕は一層Wooの高速化と安定化に励みました。

Wookieの脱落

そうこうしているうちにWookieの使っているcl-asyncのlibuv化が終わりました。

twitter.com

けれど、これは失敗でした。libuvにすることでWookieのパフォーマンスはがくっと下がり、もはやNode.jsどころかHunchentootにも負けるような状態になりました。

同じバックエンドを使っていたWooも当然パフォーマンスの劣化が見られました。けれど、それまでの十分なパフォーマンス向上により、なんとかNode.jsより1.3倍高速というラインを保っていました。とはいえここにきてのパフォーマンスの大幅な劣化は気分が沈みます。

僕は、Andrewがパフォーマンスが落ちるということを知りながら早急にmasterにマージしたのは失策だと思います。

当時の僕はcl-asyncのuvブランチの開発状況を確認しつつ、ベンチマークを取ってIssueでフィードバックしていました。そしていくつもの改善も虚しく結果は芳しくありませんでした。なのでパフォーマンスが落ちるということを知らなかったはずがありません。

libuvのほうが開発が活発だからその未来に賭けたいと思ったのかもしれません。けれど、パフォーマンスが重要なWebサーバーというプロダクトで、現状数十パーセントもパフォーマンスが劣化するならばそのコードはお蔵入りで当然でしょう。

せっかく苦労して書いたものを捨てるのは勿体ないと思ったのかもしれませんね。けれど、パフォーマンス・チューニングなんてそもそもそんなものです。速くなるかどうかはある程度コードを書いて動かしてベンチマークを取らなければわからない。結局遅ければ全部捨てて他の可能性を試す、ということの繰り返しです。

Wooの爆速化

Node.jsを圧倒的に引き離すにはもはや細かいチューニングではダメということは自明でした。数十パーセントの変化は設計上や基幹ライブラリなどを大きく変えるしかありません。

この頃から、Wooの安定化と並行して、libevへの移行を行いました。

libevならばlibevent2とのベンチマークも公式で出ており、2倍近くのパフォーマンス向上が見込めます。もはやライバルはNode.jsではなくGoに移りつつありました。

そしてcl-asyncからlibevバックエンドへの移行が完了しました。

twitter.com

移行により予想通りパフォーマンスは跳ね上がり、Node.jsの約1.9倍のパフォーマンスが出ており、Goに迫る勢いです。詳しくは当時のエントリーを御覧ください。

高速なCommon LispのWebサーバ「Woo」を作りました - 八発白中

ちなみに最新のベンチマーク結果では2.3倍のパフォーマンスが出ています。

f:id:nitro_idiot:20161215145356p:plain

反響1: 実アプリケーションでは遅いのでは

Common LispのWebサーバーがNode.jsより2倍近く高速で、かつGoに迫る勢いだ、という主張がベンチマークもついて公開されたというのはインパクトがあったのかもしれません。

Reddit*1HackerNews にも投稿され、それなりの反響がありました。

反響の中には驚きや賞賛も多かったですが、否定的なものも目立ちました。

その中でもよく目についたのが、「こんなHello, Worldを返すだけでは実際のアプリケーションとは言えない (からこのベンチマークは無効だ)」というものでした。

仰る通り。けれど、Wooは「Hello, Worldベンチマーク」に最適化したWebサーバーではありません。実環境でも当然ながらNode.jsよりは高速です。

これは僕の空想ではありません。その後サムライトでは1ヶ月を費やして広告配信システムをCommon Lispに書き換えて運用を始めました。

早期導入を優先したため、SQLの最適化などWebアプリケーション側のチューニングは十分に行わなかったにも関わらず、以前のNode.jsより1.6倍のパフォーマンスが出ています。

以下はEuropean Lisp Symbopium 2015で僕が発表したスライドの一つです。このスライドで会場から拍手をいただいたときは、やっぱりうれしかったですね。

f:id:nitro_idiot:20161215161421p:plain

Woo: Writing a fast web server @ ELS2015

反響2: 本当に最速?

もう一つあったのが、「teepeedee2を知ってるか?」というものでした。

teepeedee2とはJohn Fremlinの作った「10k requests / secを超えられる」という触れ込みのCommon LispのWebサーバーです。公開された2009年当時ではC10k問題への解決策としてかなり反響があったはずです。

John Fremlin's blog: teepeedee2 achieves 10k requests/second (キャッシュ)

ベンチマークグラフにはnginxも並んでいますが、そのnginxよりも1.5倍良いパフォーマンスを出しており、C++で書かれたWebサーバーに迫っています。

もちろんteepeedee2は知っています。けれども言ってしまえば、「teepeedee2はプロジェクトが死んでる」。

twitter.com

Quicklispには入っていますが、最新のSBCLではLinuxでもビルドできない状態になっています。

動かないものとベンチマーク比較しろって無理じゃないですか。

twitter.com

河西くんが調べてくれたところによると、CFFIのAPI変更によるものだろうということでした。そして親切なことに、彼は手元で動くように簡単に直してベンチマークを取ってくれました。その結果は残念ながら、Node.jsより少し遅い程度だったという平凡なものでした。

IOLibも高速だとよく聞きます。実はcl-asyncを一度IOLibで置き換えたこともありました。しかし、パフォーマンスは下がり、Node.jsより20%も遅かったです。

この辺りで「Common Lispは速い」と言っている人の中に盲信者が含まれていることに気づきました。実際のところ誰も何がどれくらい速いかなんてわかっていなかったのです。teepeedee2はリリース当時は確かに速かったのかもしれませんが、現在では他に追い抜かれて朽ち果てたプロジェクトとなっています。

Goに勝てる余地はあるのか

最近は「Goと比較してWooは少し遅いようだが、導入メリットはあるか」という質問をよくされます。はい、あります。

理由は利用言語の違いです。Common LispとGoを比較すればCommon Lispのほうが抽象度が高く、機能も豊富です。たとえばCLOSやコンディションシステムなどです。これは開発効率にも影響しますし、長期的にそれなりの規模のWebアプリケーションを運用していくならメリットは十分にあります。

今は、最近Goを使い始めたばかりという会社ばかりなのでチーム開発や長期運用面でのつらさなどは表に出てきていませんが、2年後、5年後、10年後にどうなっているのか少し楽しみです。

パフォーマンス面でWooはGoのサーバーに勝てるのか、という話であれば、まだわかりません。ただ打てる策はあります。libevをやめるということ。libevではなくpoll, epoll, kqueueのバインディングをそれぞれ作ってそれを呼び出せばまだ5%程度なら速くなる余地があります。

これをやらない理由は、現状でそこまでのパフォーマンスを求められていないということと、時間がないからという消極的な理由です。興味がある方はPull Requestは歓迎します。

おわりに

WooはGitHubで公開されており、Starは475です。これは僕のプロダクトの中ではClackに次ぐ2番目の評価です

明日のアドベントカレンダーは16日目のQURIについてです。お楽しみに。

*1:はてなブログRedditのURLが貼れないので見たい人はこちら。 ttps://www.reddit.com/r/programming/comments/2qio4m/woo_blazing_fast_http_server_in_common_lisp/