C# ASP.NETのRESTフレームワークパフォーマンス比較大全

LightNode(という私の作ってるOwinで動くMicro REST Framework)の0.2出しました。でも皆さんあんま興味ないと思うので(!)、先にベンチマークの話をしましょふ。0.1を出した時にもグラフを出したのですが、よく見るまでもなく詐欺グラフで非常に良くなかったので載せ直し&NancyとかWCF RESTとか他のフレームワークも追加しました。そして今回測りなおしてみると、そもそも前回のものは致命的に計測のための環境作りにミスッていたので、まるっきりナシでした、すびばせん。

パフォーマンステスト

各ソースコードはLightNode/Performanceに置いてあるので再現できます。数字はrequest per secondで、Apache Benchで叩いているだけです。実行環境は「Windows 8.1/CPU Core i7-3770K(3.5GHz)/Memory 32GB」という、私の開発環境のデスクトップPC上で動かしてます。ホスト先はIIS ExpressじゃなくてローカルIIS。

オレンジと緑はふつーのIISでホストしてるもの。グレーはちょっと別枠ということで色分けてます。

結果に納得いきます?イメージとちょっと違う結果に?まず、WCFのRestが一番遅いです。これはどうでもいいですはい。次点がASP.NET MVC、そこからちょっと僅差でASP.NET Web API。これはJSONシリアライザの問題もあるかなってところ(MVCのデフォルトシリアライザはJavaScriptSerializerで、あれは遅い)かどうかは知りませんけれど、ともかくWeb APIは言われるほど遅い遅いなんてことはない、ってとこでしょーか。安心して使っていきましょう。そこから別に大きく差がついてってことなくNancy、ちゃんと定評通りにLightweightで速そうです、偉い。ServiceStackは、一応謳い文句どおりちゃんとFastestでした。

生OwinHandler(Async)とHttpTaskAsyncHandlerはほとんど変わらない。といったところから、Owin(Katana)でラップされることは、そんなにパフォーマンスロスは発生しない、と考えられるでしょう。この事実はかなり安心できて嬉しい。で、LightNodeがそれらよりもグッと高速。そう、LightNodeはちゃんと最速フレームワークなのです。で、しかし生OwinHandler(Sync)と差がちょっと付いちゃってるので、ここはもう少し縮められないかなあ、といったところ(実際のところ、LightNodeのデフォルトは出力をバッファするようになってて、そのオプションをオフにすればもっと迫れますが、実用的にはオンにせざるを得ないので、ここはオン時で)。そして最速はSyncのHttpHandler。生ハンドラ強い。

SelfHostとHeliosはちょっと別枠。SelfHostは一節によるとSloooooooooowって話もあったのですけれど、手元でやると普通に速いのですよねえ。そのSloooowってコード見たら、なんかそもそも他のテストと出力物が全然違ったので、その人のミスなのかなぁ?って思ってますがどうなのでしょうね。ともあれ、私の環境でやってみた限りではSelfHostはかなり速いです。

Helios(Microsoft.Owin.Host.IIS)は、System.Webを通さずにIISネイティブをペシペシ叩くことで超最速を引き出すとかいう代物で、まだ0.1.2-preでプロダクション環境に使える代物ではないとはいえ、とにかく速い。すぎょい。これだけ違うとニヨニヨしちゃうねぇ。

Sync vs Async

ベンチ結果の突っ込みどころは二点ほどあるかな、と。ひとつはHttpHandler(Async)がHttpHandler(Sync)に比べて険しく遅いこと。もう一つは、RawOwinHandler(ASync)とRawOwinHandler(Sync)ってなんだよハゲ、と。RawOwinHandlerはこんなコードになっています。

app.Run(context =>
{
    // 中略

    if (context.Request.Query.Get("sync") == "true")
    {
        context.Response.Body.Write(enc, 0, enc.Length);
        return EmptyTask; // Task.FromResult<object>(null)
    }
    else
    {
        return context.Response.Body.WriteAsync(enc, 0, enc.Length);
    }
}

違いはWriteしてるかWriteAsyncしてるか。で、これだけで1000rpsも変わってしまうんですねえ、お、おぅ……。HttpHandler(Async)とHttpHandler(Sync)も同じ話です。これねえ、困った話です。しかも、Heliosだと遅くならなかったりするので、原因はSystem.Webのネットワークストリームに対するWriteAsyncに何らかの欠陥があるのかなあ、と。それ以外にとりあえず考えつくのは、そもそもレスポンスサイズが小さいとかlocalhost同士だから、とかなくもないので(所詮マイクロベンチですから)、厳密にどーこうというのは言いづらくてまだ要調査ってところ。でもHeliosだと遅くならなかったりするのでもうHeliosでいいよ(投げやり)

まぁネットワーク離したりサイズ大きくしたりとかは、そのうちやりませうか。そのうち。多分やらない(面倒くさいの!)

ベンチ実行環境の注意

Windows Defenderのリアルタイム保護が有効ならば、無効にしましょう。これ、有効か無効かでめちゃくちゃ結果変わります。3000rpsぐらい変わるしHeliosにしても別に大して差が出ないとか、もう根源的に結果が変わります。Defenderがオンの時に測った結果とかクソの役にも立たないゴミデータなので投げ捨てましょう。ネットにある計測しました、とかってのもそれの可能性があるので見なかったことにしましょう。ファイアウォールとかもとりあえず切っといたほうがいいんじゃないでしょーか。

IISかIIS Expressかは、傾向としてそこまで大きく違うってこともないですけれど、IISのほうが成績は良好なので、一応測るのだったらIISでやったほうが良いかと思います。以前にやってた時は横着してIIS Expressでやってたのですけれど、反省ということで。そこまでやるならWindows Serverでー、とか無限に要求は加速しますが、フレームワーク同士の相対的な比較なので、そこまでやる必要はないかな?

LightNode 0.2

ここからタイトル詐欺の抱合せ商法。じゃなくて、最初はこっち本題で書いてたんですが思ったよりパフォーマンス比較が厚くなったので上に持ってきただけなんですよ……。ということでLightNode 0.2出しました。LightNode自体は0.1の解説LightNode - Owinで構築するMicro RPC/REST Frameworkを読んで欲しいのですけれど、0.1はOwinへの理解度が足りなかった成果、安定性に難があったり、細部が詰められてなかったりしました。けれど、今回はかなり完成度上がってます。このバージョンからは普通に投下しちゃって問題ないレベルに達しているかな、と。ベンチマークを細かく取って検証しているとおり、パフォーマンスについてもよりシビアに詰めています。

変更点は割といっぱいあります。

Enumバインディングの高速化
Enumパースの厳密化
T4クライアントコード生成内容の変更、アセンブリロック回避
ContentFormatterのコンストラクタ修正
IContentFormatterにEncodingインターフェイス
Extensionを|区切りで受け付けるように
void/Task時は204を返す
ReturnStatusCodeExceptionを投げることで任意のステータスコードで返せる
Optionでstringはデフォルトではnull非許可に
IgnoreOperationAttribute追加
フィルター追加

こんなとこで。あとGitHub Pages -LightNode立てたのでトップページがちょっとオサレに。

Enum高速化・判定厳密化

C#のEnum自体は凄く好きなんですよ、プリミティブとほとんど変わらない、という、それがいい。Javaみたいにゴテゴテついてると逆に使い勝手悪かったりパフォーマンス上の問題で使われない(Android!)とかって羽目になってたりしますし、これはこれで良いかな、って思ってます。拡張メソッドによってちょっとしたメソッドは足すことが可能になりましたしね。ただ、静的メソッドが頂けない。リフレクションの塊なので速くない、なんか色々使い勝手悪い、などなどビミョー感半端ない。

しょうがないので、LightNodeではEnum専用のインフラ層を構築して回避しました。高速化しただけじゃなくて、値の判定を厳密化しています。Enumの値が1,10,100の時に5を突っ込んだらダメ、って感じです。更にビットフラグに対しても厳密な判定がされるように加えているので([Flags]属性がついてるかどうかを見ます)、1,2,4,8の時に100が来たら死亡、7ならOK、って感じですにぇ。ASP.NET MVCなどでも、Enumでゆるふわな値が渡ってくるのはかなり嫌だったので、良いんじゃないかと思います。他、デフォルトでstringもnull非許可、配列の場合は空配列になるので、デフォではnullや範囲外の値というのは完全排除しています。

なお、このEnumインフラストラクチャは、後日、もう少し機能を追加して専用ライブラリとして切り出そうと思っています。使い道はかなり多いんじゃないかなー、と思いますのでお楽しみに。

例外によるステータスコード変更

ASP.NET Web API 2 で追加された機能について見てて、いいですよねー、ということで。実際、戻り値の型をシンプルなオブジェクトで指定した場合って、例外でグローバルに吹っ飛ばすしか手段ないですしね。

public class Hoge : LightNodeContract
{
    public int HugaHuga()
    {
        throw new ReturnStatusCodeException(HttpStatusCode.NotImplemented);
    }
}

単純明快でいいと思います。IHttpActionResultなんて作りゃあそりゃ最大の柔軟性ですがResponseType属性とかは、まぁ、やっぱあんまりだな、って思いますよ、ほんと……。

さて、実際のとこWeb APIはやっぱり一番参考にしていて、機能眺めながら、どうするか考えてます。ルーティングや認証は他のMiddlewareがやればいい、Request Batchingは入れたい、ODataはOData自体がイラネ、フィルターオーバライドはうーん?とか。色々。色々。

Middleware vs Filter

そして、フィルター入れました。最初から当然入れる気ではあったのですが実装時間的に0.1では間に合わずで。まぁ、あと、0.1の時点ではミドルウェアパイプラインとフィルターパイプラインの違いを言語化出来なかったり、実装方法というかインターフェイスの提供方法についても全然考えが固まっていなかったので、無理ではあった。今はそれらはしっかり固まってます。

というわけで、こんなインターフェイス。

public class SampleFilterAttribute : LightNodeFilterAttribute
{
    public override async Task Invoke(OperationContext operationContext, Func<Task> next)
    {
        try
        {
            // OnBeforeAction

            await next(); // next filter or operation handler

            // OnAfterAction
        }
        catch
        {
            // OnExeception
        }
        finally
        {
            // OnFinally
        }
    }
}

ASP.NET MVCとかの提供するOnActionExecuting/Executedとか、アレ、私は「大嫌い」でした。挙動が不明だから。Resultに突っ込むと何が起こるの?Executingで投げた例外はExecutedに届くの?(届かない)、などなど、分かりやすいとは言い難くて、LightNodeではその方式は採用したくなかった。

かわりにシンプルなパイプラインを採用しています。OWINのInvokeパイプラインとほぼ同等の。

// app.Use
Func<IOwinContext, Func<Task>, Task> handler
// LightNode Filter
Func<OperationContext, Func<Task>, Task> invoke
// インターフェイスでは
public abstract Task Invoke(OperationContext operationContext, Func<Task> next);

というか一緒です。見た目もやってることも一緒なミドルウェアとフィルターですが、違いは当然幾つかあります。第一に、ミドルウェアだとほとんどグローバルに適用されますが、フィルターはグローバル・クラス・メソッド単位の3つが選べます。そして、最大の違いは実行コンテキストを知っていること。フィルターが実行されるのはパラメータバインディングの後なので、実行されるメソッドが何かを知っています。どのAttributeが適用されているかを知っています。ここが、大きな違い。

フィルタはGlobal/Contract/Operation単位で設定可能ですが、順番は全て設定されたOrderに従います。AuthenticationとかActionとかの順序パイプラインはなし。だって、あの細かい分け方、必要です?認証先にしたけりゃOrderを-int.MaxValueにでもすりゃあいいんです。というわけで、そのへんは取っ払って、Orderだけ、ただしOrderのレンジは-int.MaxValueからint.MaxValueまで、かつデフォルトはint.MaxValue(一番最後に発火される)。です。ASP.NET MVCのデフォが-1で最優先されるってOrderも意味不明で好きじゃないんですよねえ……。

OperationContextはIsAttributeDefinedなど、属性のチェックや取り出しが容易になるメソッドが幾つか用意されてますにゃ。もし実行をキャンセルしたければ、nextを呼ばなければいい。その上で大きく結果を変えたければ、Owin Environmentを弄れば好きなようにStatusCodeでもResponseでもなんでも設定できます。十分十二分。

まとめ

パフォーマンスは、まぁ自分で測らないと納得しにゃいところもきっとあると思いますので自分で測るのが一番いーですね、そりゃそうかそりゃそうだ。生ハンドラに比べるとロスは少なくないなぁというのは現実かもしれませんねぇ(LightNodeは除く)。Owinを被せることのロスは少なめってのが確認できたのは安心できて良いですね、さぁ積極的に使っていきましょう。

LightNodeは超簡単。超速い。十分なカスタマイズ性。というわけで、真面目に実用的です!使うべき!さぁ今すぐ!あと、意図的に既存のフレームワークの常識と崩しているところも含めて、全ての挙動の裏には多くの考えが含まれています。全ての挙動は明確に選択しています。そーいうのも読み取ってもらえると嬉しいですね。

また、ちょっとした縁があって2014/2/8に北海道のCLR/Hにてセッションを一つ持ちます。北海道!あんまり東京から出ない私なのですが、こないだは大阪に行きましたし、ちょっとだけたまにはお外にも出ていこうかな、なんて思っていなくもないです。北陸とかにもそのうち行きたいですね、色々なのと重ならなければ……。

セッションタイトルは「LightNode Demystified - How to Make Extreme Fast Owin Framework」を予定しています。ネタは色々あるんですが、LINQはこないだ大阪でやったし、Real World Hyper Performance ASP.NET Architecture(Internal謎社)は、もう少し後でいいかなぁ(?)だし、前2つがうぇぶけー(Windows Azure 最新アップデート/最新Web アプリケーションパターンと .NET)なので、合わせて見るのが良いかにゃ、と。

LightNode Demystified、ですけれど、LightNodeを作ることを通してOWINとはどのようなものなか、どのような未来が開けるのか、というのを伝えられれば良いと思っています。私自身、実際に作ることによる発見がかなり多かったし、作ってみないと分からないことというのはかなり多いと思いますので、そうして開けた視野をシェアできれば嬉しいですね。

また、1/17には弊社で開催するめとべや東京#3にてLTでデモをするので、そちらのほうも都合がつく方は是非是非どうぞ。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive