2012年2月12日日曜日

プロパティのバインドに失敗するときのエラーメッセージをカスタム

stackoverflow.com。 今でもたまに書き込んでます。

c# - Regular Expression stop crashing - Stack Overflow

随分前にも似たようなガッカリ感を味わったんだけど、今回もまた似たような気分を味わってます。とはいえ、質問の真意を履き違えての結果の可能性は否めない。

この質問、必須項目にしたDateTime型のプロパティへのバインドに失敗するから、正規表現でチェックしたんだけど、”0000/00/00 00:00:00”って入力するとエラーになるんだよね。なんで?って言う感じでしょーか。

質問者のモデルを見るとPostedは必須項目。DateTimeには有効な日時しか入れられないっていう条件をあわせて考えると、不正な文字列の時にエラーメッセージを表示できれば要件は満たす。かな。正規表現がどうのこうのっていう問題解決の仕方ではなく、プロパティにセット出来なかった時のエラーメッセージがカスタム出来ればいいかもね、と思ったんですねー。

private static string GetValueInvalidResource(ControllerContext controllerContext) {
	return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;
}

private static string GetValueRequiredResource(ControllerContext controllerContext) {
	return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;
}

↑このようにMVCのDefaultModelBinderではそんな時のエラーメッセージをプロパティ”個別”にカスタムはできません。どんなプロパティにも共通のメッセージ。

で考えた結果、投稿したコードが↓これ。

public class CustomModelBinder : DefaultModelBinder
{
  protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
  {
    base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);

    var propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
    var invalidMessage = propertyMetadata.AdditionalValues.ContainsKey("PropertyValueInvalid")
                 ? (string)propertyMetadata.AdditionalValues["PropertyValueInvalid"]
                 : string.Empty;
    if (string.IsNullOrEmpty(invalidMessage))
    {
      return;
    }

    // code from DefaultModelBinder
    string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
    if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
    {
      return;
    }
    ModelState modelState = bindingContext.ModelState[fullPropertyKey];
    foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList())
    {
      for (Exception exception = error.Exception; exception != null; exception = exception.InnerException)
      {
        if (exception is FormatException)
        {
          string displayName = propertyMetadata.GetDisplayName();
          string errorMessageTemplate = invalidMessage;
          string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate,
                            modelState.Value.AttemptedValue, displayName);
          modelState.Errors.Remove(error);
          modelState.Errors.Add(errorMessage);
          break;
        }
      }
    }
  }
}

これを利用するためにモデル定義を以下のように変えてしまいましょう。

[Required]
[AdditionalMetadata(
 "PropertyValueInvalid",
 "Wrong Syntax Entered, Needed:day/Month/Year Hour:Minutes:Seconds")]
public DateTime? Posted { get; set; }

AdditionalMetadataにカスタムメッセージを指定して、プロパティのバインドがエラーになったらそのメッセージを表示する。素敵だと思うんだけどなー。

で、コメントが「this code is like giving a baby a gun」。ぎゃふん。

2012年1月29日日曜日

効率のいいリクエスト処理

Webサイトの運用、大変ですよね。お疲れ様デスよ。まったく。

普通にネットワーク構成するとハードウェアロードバランサの下にWebサーバー並べて負荷分散する感じになるでしょう。処理が追いつかなかったらWebサーバー追加だぜ!とか、富豪富豪。ふぇっふぇっふぇ。でも、そんな富豪ばかりじゃない。

Application Request Routing | IIS 拡張機能 | TechNet

略してARR。NLBみたいにネットワークの負荷分散じゃなくてHTTPでのリクエスト負荷分散。ソフトウェアLB。

ARRが単なるリバースプロキシだと思ったら大間違いでした。この子かなり優秀。

そもそも、SSLオフロードさせようと思って、パフォーマンステストをしてたんですけど、思いの外Request / Secが高くなる。変だなーと思って、HTTPSじゃなくてHTTPでも試してみたら、それでもRequest / Secが高い。なんで!?と思いパフォーマンスモニターでいろいろ見てたら、どーもノードサーバーの処理が重い時にはARRがリクエストのルーティングをちょっと保留してるくさい。レスポンスタイムを見て、重くなってきたのかどーかを判断してるのかなー。それ以外に指標がないと思うので、そーじゃないかという予想。

arr1

arr2

VS のロードテスト(今はまだUltimate使えるからね)でチェックしてみました。10~250ユーザーまで徐々に増やしていく設定でWebアプリケーションの動きをシミュレートさせる。下のグラフはパフォーマンスモニタ。

黄色がCPU、青がWeb Service:Current Connections、緑がW3SVC_W3WP:Request / Sec、赤がASP.NET v2.0:Request Queued。

まずは、ARRを通さずに直接リクエストした状態です。CPUは概ねフルに使いつつ、コネクション数が段階的に増え、リクエストキューに処理待ちリクエストが溜まっていく様子です。直感的にそーなるなー、っていう動きがそのまま再現できてますね。

arr3

どんどん負荷を上げる(ユーザー数を増やす)と、キューにたまる数が増えて、グラフ上Request / Secを上回ります。これはつまり同時処理性能を超えたリクエストがWebアプリケーションサーバーに来てるっていう状態ですね。さばけないからキューイング。ASP.NETのキューイング。

最終的にすべて終了すると↓こうなって、何もかも0に戻る。

arr4

ロードテスト結果。

arr5

Request / Secが281でページ/秒の平均が26.0です。

今度は全く同じ事をARR経由させて実行します。

arr6

arr7

この段階ですでに挙動が違うんですよね。下のグラフはWebアプリケーションサーバーの状態なんですけど、コネクション数の増え方が違う。段階的なんだけど、伸びかたが明らかに違う。この時のARRの同じグラフを見てみるとどーなってるか。

arr8

なるほどね。こっちにはユーザー数と同じ増え幅でコネクション数が増えてます。

ARRからノードサーバー(Webアプリケーションサーバー)へはTCP接続を使いまわしてる?コネクションプーリング。

arr9

ユーザー数をどんどん増やしていくと、リクエストキューにたまるのが直接接続時の挙動でしたが、ARRを経由した場合、キュー(赤)がたまるのに合わせてコネクション数(青)が増えてる。関連があるような挙動ですね。

で、観測を続けると、キューの数が減少に転じてます。不思議ですね。ユーザー数は同じでリクエスト数も同じように発生してるのに、Webアプリケーションサーバーではキューがたまりません。

その時のロードテストの状態。

arr10

Request / Secは298で落ちてない。若干上がってるくらい。

arr11

最終的にテスト実行完了時点でCPUやRequest/Sec、キューの数は0に落ちるのに、コネクション数(青)はまだ0にならない。しばらく高止まり。これはつまりARRがコネクションをすぐに切ってないということですね。

arr12

Request / Secが347でページ/秒の平均が32.4.0。ページの応答時間平均にいたっては6.45から4.20ですよ!約1.5倍。これはどういうことなのかというと、Webアプリケーションサーバーの同時処理性能が下がるようなリクエストをルーティングせず、しばらくARRでキューイングしておくことで、Webアプリケーションサーバーがベストパフォーマンスを出せるように調整してるから、じゃないでしょうか。実際にWebアプリケーションサーバーのキューの数はARRを経由させたほうが少ない状態を維持し、コネクション数も少ない。何度か試した結果コネクション数が半分の時もあるのに、ページの応答時間平均は常に概ね1.5倍早い。

無理に同時実行させないことで早くレスポンスを返す。結果的にRequest/Secも上がる。SSLオフロードでWebアプリケーションサーバーのCPU利用を実処理に専念させようとしただけなのに、この結果。恐るべしARR!

ちなみにARRとWebアプリケーションサーバーが同居した場合にはまるで性能向上しませんでした。してもいいかと思ったんだけど、そういう訳でもないんですね。IISとASP.NETの双方でのCPU利用(ネットワークも思いのほかCPU利用するのかもね)を最適化するならARRは別サーバーで用意しましょう。

ARR賢い。単なるリバースプロキシじゃない。ノードサーバーはIISに限定するものじゃないので、性能判断の指標はレスポンスタイムしか無いと思うけど、その辺の情報は見つけることが出来ませんでした。

今回はARR1台、ノードサーバー(Webアプリケーションサーバー)1台なので、こういう結果でしたが、ノードサーバーの台数が増えた場合にはどのように効率化されるのかは、環境次第だと思うので、自身の環境で試してみたらどーでしょー。

もし、仮に同じような性能向上をするならWebアプリケーションサーバーが4台以上ある場合、1台をARRにしたほうがレスポンス性能がいいということになるかもよー。

目指せ、続ハイパフォーマンスWebサイト!

2012年1月2日月曜日

HttpBench on GitHub

年明け早々です!お正月生まれのツール。

HttpBench – GitHub

ApacheBenchにはいつもお世話になってます。でも、同じようにHTTPSも計測したいです。absめ!

localhostに対してab –n 100 –c 10 http://localhost/

hb3

localhostに対してhb –n 100 –c 10 http://localhost

hb4

それっぽいー。けど、手抜きすぎー。

せっかくなのでIIS ExpressでHTTPSへのリクエストを確認。

hb5 hb6

左がHTTPで右がHTTPS(IIS Expressの勝手証明書)。ab(abs)だとうまくできないですよね。

abだと、すごくたくさんオプション指定できるし、計測も細かい。流石です。が、hbはまだ全然。とりあえず実行回数/コンカレント数/BASIC認証、HTTP GETだけ。

あ。バイナリ配布ってどうしよ...。いつかすると思う。今はそんな段階でもないからいいよね。

コンカレントをMaxDegreeOfParallelismじゃなくて、Threadを作って処理してます。普通にTaskでもよかったのかなー。

hb7

↑-c 200指定。

目指せハイパフォーマンスWebサイト!

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。