kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

月別アーカイブ: 12月 2012

Visual Studio 2012 でのパフォーマンスプロファイリング


どーも、どーも、どーもです。

Visual Studio Advent Calender 2012 が中々枠が埋まり切らないという悲鳴が上がってたので、参加しましたです。

というわけで、表題通りパフォーマンスプロファイリングについて書かせてもらいます。

Visual Studio でのパフォーマンスプロファイリング機能は Visual Studio 2010 までは Premium 以上で有効、Visual Studio 2012 では Professional 以降で有効という事で、Visual Studio の上位エディション機能がやっと普通のエディションで使えるようになったという事になります。

というわけで、プロファイリングによってなんかしらプログラムのボトルネックが検出できるかという事で緩く募集したら msgpack-cli をプロファイリングしてみてほしいという事で試してみました。

https://twitter.com/kazuk/status/282008544600023040

Visual Studio でのパフォーマンスプロファイリングの開始方法

テストプロジェクトがあれば、テストの実行中にプロファイリングを行う事ができます。「テスト」メニュー「テストのプロファイル」ただし、プロファイリングがストアアプリのプロファイリングにまだ対応していないので msgpack-cli に対して即座に試す事はできませんでした。実際問題としてパフォーマンス問題を抱えたところをテストとして作りこむとCIサーバとかでスローテストになって邪魔になるのでお勧めしませんです。

image

コンソールアプリケーションを含む一般的な実行可能なものであればプロファイルは可能ですので、今回はテストの中で[Explicit]で除外されてる物から TestStringMedium を選び、コンソールアプリケーションに同等物をこさえてみました。

現物ソースはこちら。

https://gist.github.com/4357661

最初オリジナルのテストを数1000回実行するようにしたのですが、結果的にテスト側での StringBuilder での文字列構築がボトルネックとなったので、一回構築した文字列に1万回のシリアライズ、デシリアライズを行う様にしてみました。

このコンソールアプリケーションをスタートアッププロジェクトに指定して、パフォーマンスプロファイラを起動します。

「分析」メニューから「パフォーマンスウィザードの起動」でプロファイリング方法などの設定を行いながらプロファイリングを開始します。

imageimage

初期状態として「CPUサンプリング」がプロファイリングの方法として選択されています。このプロファイリングでは一定時間毎に実行スレッドを止めスタックトレースを取って何を実行しているかを記録する方法が使われます。なので相当に実行時間のかかる処理以外はプロファイリングできませんし、ほとんど実行時間を使わない部分については結果に現れません。

性能のボトルネックがどこなのかを把握するのに最も適したプロファイリング方法になります。次にプロファイル対象プロジェクトの選択になります。自分で用意したプロジェクトを指定して次へ進みます。

imageimage

完了時にプロファイルを開始するを指定していればパフォーマンステストが開始されます。

image

相当しつこく実行するようにしているのでプログラムは中々終わりません、定常実行状態になって1分ちょいでも実行すれば十分なサンプルが集まりますのでコンソールアプリケーションの実行ウィンドウを閉じて終了させます。

分析していますの表示後に以下のような分析結果が表示されます。

image

最初に表示されているのは CPU 使用率のグラフで、このテストの間 1 Core をほぼ占有してプログラムは動作していた事になります。次にホットパスとしてサンプリング結果が示していた関数が最も多く実行していた関数が表示されています。

92.4% のCPU時間が TestStringMedium で消費されているのでプロファイリングとしてはターゲットをちゃんと実行している事になります。TestString は実際に Pack/Unpack を行っていますが、実際にはPackの方が 59% を占有、Unpackは8%弱しか使っていない事が解ります。

imageimage

デシリアライズの性能改善が可能であるかという話なので、少ない側ですがUnpack に降りていきましょう。

imageimageimageimageimageimageimageimage

単純に呼び出し先が mscorlib とかマングリングされたネイティブメソッドになるまで降りた結果としては上記の流れになります。

8メソッドにわたって降りて行きましたが最終的には new byte[] と Stream.Read に処理時間が使われている事がこれでわかります。ここの時間消費を削るにはどんな方法があるでしょうか。 new byte[] を削るにはバッファ管理を入れて同一の長さのバッファを再利用する等の方策もあったりしますけど、上位の構造を大きくいじらない限りには無理でしょう。Stream.Read の処理時間を削る方法は多分無いと思います。メモリストリームからの読み出しですから単純にバイト列のコピーに時間を取られているはずです。

実際のところとして new byte[] は 0初期化を含んでいます、0で初期化したうえで Stream.Read で内容をすべて上書きするのはちょっと無駄と言えば無駄です、しかしこれをプログラミング的に避ける事はできません。

さて Stream からバイト列を読む方法にはもう一つあり BinaryReader.ReadBytes があります。これを使った場合どうなるでしょう。

image

結果的には若干のコスト増になってしまいました、BinaryReader を new しているところで 0.3%の増加が最も大きく響いています、before / after で見ると new byte[] と Stream.Read  で 8% だった物が BinaryReader.ReadBytes の呼び出しで 8.1% と若干の増加も見られます。失敗と見ていいでしょう。 mscorlib に対しては NGen で結構厳しく最適化かけているという噂もなかったりしますけど、 BinaryReader.ReadBytes にはその効果は及んでいないと見て良いでしょう。

imageimage

実際 BinaryReader.ReadBytes の IL を眺めてみても Stream.Read で上書きされる事が明らかな配列の初期化を省略するような確保方法は行われていませんから順当な結果です。

それでも自分は変質的なスピード狂だったりする

Stream.Read には改善の余地はなくとも、バッファの確保と0初期化を避けるためにできる事はいくらかあったりします。

というわけでバッファの管理と再利用を実装してバッファを使いまわしてあげましょう。

人様のライブラリに勝手に API を追加しちゃいます。 MessagePackObject に Release メソッドを追加

        public void Release()
        {
            if (this._handleOrTypeCode is MessagePackString)
            {
                var packString = this._handleOrTypeCode as MessagePackString;
                packString.Release();
            }

        }

MessagePackString に Release と Alloc を追加してバッファを再利用できるようにします。

        static Dictionary<int, Stack<byte[]>> _buffers = new Dictionary<int, Stack<byte[]>>();

        public void Release()
        {
            Stack<byte[]> bufferStack;
            if (_buffers.TryGetValue(this._encoded.Length,out bufferStack))
            {
                bufferStack.Push(this._encoded);
            }
            else
            {
                _buffers.Add(this._encoded.Length, new Stack<byte[]>( new[] { this._encoded }));
            }
        }

        public static byte[] Allocate(int length)
        {
            Stack<byte[]> bufferStack;
            return _buffers.TryGetValue(length, out bufferStack) 
                ? bufferStack.Pop() 
                : new byte[length];
        }

テストコード側から Release を呼ぶようにして、バッファの再利用を促すのと同時に読み込みコードで Allocate によってバッファを確保するようにしてプロファイルした結果は以下。

image

単純比較はできないかもしれませんが、3割強の性能改善が実現できました。(まぁ、バッファの管理系はシングルスレッドだから動いてるレベルの実装なのでマルチスレッドに対応するともっと性能は下がってしまうかもしれません。)

まとめ

無理やりチューニングすれば早くできるところは多少なりともあったりしますが API 変わってしまうとそれを使っている所は作り直しになってしまいます。だから早いうちから API の利用者が増えないうちに、常にプロファイリングを取り続けてチューニングしましょう。パフォーマンス問題がAPI変更に波及すると出来上がってからの性能改善、チューニングはまず無理です。

まぁ、アプリケーションの枝葉末端はアプリケーションへの性能影響は殆ど無いのも事実ですので、アプリケーションへの性能影響の無い末端はチューニングする必要もありませんので、このコードがシステムの性能を支えている物なのかどうなのか、そこを把握するためにもプロファイリングは行ってくださいね。

今回は CPU サンプリングによる CPU 時間を元にチューニングをしましたが、他のプロファイリング方法も重要といえば重要です。システムがメモリ枯渇に陥っているなど GC が頻繁に活動して性能が下がっている等状況に応じて使い分けていきましょう。

広告

とある鈍器と動的再生成- C# Advent Calender 2012


どーも、どーも、どーも。

 Kazuhiko Kikuchi

Kazuhiko Kikuchi @kazuk

C# er であり .NET erな人。(IL erという噂もある) ILDASMで殴る人

でございます。

まぁ、そんなわけで、ILDASMという鈍器を日夜振りかざして( ILDASM Hoge.dll /Out=Hoge.il して、Hoge.il を適当に書き換えて ILASM に食べさせてIL書き換えとか)いるのも結構疲れるわけでございまして、もうちょっと簡単にならないかな的な物をシコシコ作ってこさえましたので、Advent Calender という機会を使っての宣伝でございます。

どこにありますか?

https://github.com/kazuk/ProjectOosaki にソース一式あります。

実行環境は制限がありますか?

.NET Framework 4.5 が必要です。(現状コードは4.0でも動きますが、今後4.5で追加されたメソッドを使う予定なので動かなくなります。)

何ができるものですか?

ILメソッドの逆アセンブルと、動的アセンブリへの再アセンブルができます。

この逆アセンブルから再アセンブルの間にIL列にちょっかい出しをしたり、ILのトークン値の解決に割り込んでトークンの置き換え等ができます。また、再アセンブルをしないIL列の内容チェック等により静的解析で行われるようないくらかの処理ができます。

結果的にはオリジナルメソッドと全く同一ないしは意図的に加工した意図通りのILをもつメソッドが出来上がりますので、トークンの書き換えによってMock/Stubフレームワークでやるような DateTime.Now の呼び出しをほかのメソッドに振り向けるなどの細工ができます。(サンプルでもやってます

逆アセンブラの実行

https://github.com/kazuk/ProjectOosaki/blob/master/Oosaki.Msil.Samples/DisassemblerSamples.cs

逆アセンブルは MethodInfo から GetMethodBase したうえで GetILAsByteArray で取り出したIL命令を解析します。

逆アセンブル結果は内部的にはILバイト列そのものと、各命令のオフセット算出および分岐ターゲットラベル、例外フレームを重畳したint値の列からなります。(その他パラメータ等付随データがいくらかあります)

呼び出し命令の検出等のIL命令の検索等の静的解析はこの逆アセンブル結果に対する処理として実装することができます。

逆アセンブル結果のIL列の順序はオフセットの int 値の列によって定義されますので、必要であればIL列に命令を挿入する事もできますし、IL命令の順序を変える事もできます。

ILアセンブラの実行

https://github.com/kazuk/ProjectOosaki/blob/master/Oosaki.Msil.Samples/ReassembleAndTransformSamples.cs

ILアセンブラは逆アセンブル結果に Assemble メソッドとして実装されています。逆アセンブル結果はMethodInfo を返しますので、Invokeを呼び出せば実行する事ができます。

現状では内部実装に System.Reflection.Emit.ILGenerator を利用していますが、 System.dll の全メソッドの逆アセンブルと再アセンブルを試行した結果として意図通りのIL列を特に例外処理に関して正しく出力できない事が解っていますので完全に別実装に置き換え予定でいます。

ILの動的逆アセンブルと再アセンブルでできるようになる事

ILの動的逆アセンブルと再アセンブルでできるようになる事は結構多岐にわたります。

IL命令列に意図的な加工を施す事で、既存テストが Fail すればテストは正しいという判断を下す物はミューテーションテストと呼ばれます。この手法は結構以前にMSDNマガジンで紹介されているのですが、実際にやっているという人は見たことがありません。

ミューテーションの力: .NET Framework による単純なミューテーション テスト システムの作成

上記では ildasm / ilasm を利用してハーネスを駆動していますが、動的にオンメモリで実行可能であればIL列の書き換えや実行に要する時間を大幅に削減できます。

また、メソッド内の分岐グラフの節点にその節点が実行された事を通知するメソッド呼び出しを埋め込めみ実行する事でカバレッジを測定する事ができるでしょう。カバレッジ測定には CLR のプロファイリングAPIを利用するものもありますが、インストルメンテーションというIL解析から必要なマーカーポイントの打ち込みを行って実行アセンブリを再生成するプロセスによってこれを行うタイプのカバレッジツールと同様の手法となります。

非同期マルチスレッド処理の競合点の検出や競合のシミュレーションもやりようによっては可能です。競合点となる操作はフィールドへのアクセスですので、フィールドの参照操作の前後にロック確認用メソッドの呼び出しを挿入すれば、適切なロックをせずに実行されるフィールドアクセスが存在するかを実行確認する事が可能になります。(ラムダを利用すると、メソッドのローカル変数がクロージャークラスのフィールドに変換される事を知っていますか?そして、この変換されたフィールドへのアクセスがスレッド競合の危険に晒されていることを意識してメソッドを実装していますか?それってあなたのチームの全員が?)

というわけで

まだまだ作りかけの物です、Samples に実装されている内容は常に動くようにという方針でやっていますが再アセンブル処理のベースを完全に再実装しなおすつもり等タスクは結構残っています。んなわけで、開発者募集です。自分自身これで利益得てないので無報酬ですが。

github に晒してる物に pull request 等投げてもらえればマージするかもしれませんし、しないかもしれません(中身と気分に応じてやります宣言)、要望、不具合等 issue に書いてくれれば実現するかもしれませんししないかもしれません。

自分の物としてこちらを気にせず色々いじくりまわしたい人は fork して好き勝手にいじってくれていいです。NuGetにはもうちょっと完成度を上げてから流したいと思っていますのでforkの方から流すのはご遠慮ください。

pull request する際のコーディング規約等

あんまり無いです、緩いです。

  1. 他者の権利、ライセンスを侵さないソースでお願いします
  2. #region は使わないでください。
  3. public メソッドには Code Contracts による契約を入れてください
  4. public な要素については標準的な命名規則に従ってください
  5. 動作確認に使えるテストコードを含めてください
  6. 既存および追加されるテストの完走を確認ください(issue報告の為の不具合再現用テストコードは Fail している物をpull request してもらって構いません)
  7. 利用者に開放される機能の追加は Samples 配下にテストコードの形で機能の利用方法として参照できるコードを収めてください
  8. メソッド名、プロパティ名、パラメータ変数名等名前の類にはそこそこ気を使って下さい。スコープが5行未満のローカル変数については説明的である必要はありませんが、スコープがそれを超えるものについては何が格納されているのかを示す命名をしてください。

まとめではない事

私、菊池和彦は個人事業主として .NET Framework , ASP.NET , Windows , Windows Azure 上のシステムの設計開発、およびそれに関連するチューニング、コンサルティングやアドバイザリーサービスをやっております。お仕事のご用命等ありましたら電子メールにて kazuk.dll@kazuk.jp へお気軽にお願いします。決して安いとは言いませんが値段なりのお仕事をさせて頂きますのでよろしくお願いします。

500 – Internal Server Error – One ASP.NET Advent Calender


というわけで、二日続けての Advent Calender、今回は One ASP.NET です。

というわけで、タイトルの事についてゴニョゴニョ語ってみようと思います。タイトルからして嫌なものを題材にしてって感じですが、クリスマスに向けてホノボノし続けてるとたまにホラー映画とか見たくなるじゃないですか、そういうニーズにこたえる記事でございます。

ASP.NET での Web サイトやアプリケーションを皆さんはどのように監視したりしていますか?

  1. ping や一定期間ごとの get リクエストでの死活監視
    これは最低限の監視ですね、サーバやインフラが生きてないんじゃお話にならないでしょう。
  2. 定期的なWebアクセスログやシステムイベントログの収集と分析
    システムイベントログを取ってるとアプリケーションのエラーとかの情報はおおよそ良い感じに集まるので、これをベースに監視や管理を行うのはいいことですね。Web アクセスログはうまく使えば応答時間の変動等も監視できますね。
  3. そもそもやって無いとか
    レンタルサーバにおいていてレンタルサーバ屋さん任せ、自分は特に監視や管理をしないよ!これもありっちゃありな話です。
  4. ASP.NET の監視のAPI使ってるぜ!
    これは珍しいんじゃないかな?っていう事で今回の記事になります。

System.Web.Management 名前空間

ASP.NETなら当然に参照されている System.Web アセンブリに System.Web.Management 名前空間があります。この名前空間で用意されているものが今回の題材です。MSDN でのリファレンスはこちら

Internal Server Error !

さて、 Internal Server Error が発生したら何が起こるでしょう。 Page Error イベントハンドラが起動されたり Application オブジェクトの Error メソッドが呼び出されるとかはまぁご存じのことでしょう。

このエラーの発生時には様々な事をやりたいと思うでしょう、担当を叩き起こしたり、お客への詫び状を書いたり、再発の防止策をまとめたりはさておき、DBサーバは生きてるのか確認したりとか、いろんな確認をしてどこがどんな原因で落ちたのか「ほらバグだ、さあ直せ」「ほらバグだ、さあ直せ」「ほらバグだ、さあ直せ」と踊り狂う営業をどうやって棺桶に入れるかとか色々考えることや調べること、諸々があります。

さて、まっとうに商売しているWeb屋さんなら複数のアプリケーションをもっていたり、複数の顧客のサイトを運営していたりする事はよくある事でGlobal.asax.cs なんてふつうは顧客向けカスタムの巣窟だったりしますよね。

でも、監視管理は自分達の監視管理の方針に揃えたいですよね。

顧客カスタムの巣窟なGlobal.asax には会社として揃えたい物は入れたくない、そうその通り。かといって Application_Error でしか捕まえられないじゃない!

困った結果として HttpApplication と MvcApplication の間に挟まれる中間クラスを書いたりしようにもこの辺は各アプリケーションが自分たちの実装の為に入れ替えてるのがふつうだったりしてアプリケーションの用意するHttpApplication 実装の下には入りづらいけど上に入れるのは基底クラス違いでいくつも書かないとアレとか色々な問題が起こりすぎて泣けてくるのがふつうだったりするでしょう。

能書きはおいといて実装編

さて、Application_Error でない場所で Internal Server Error をトラップしてみましょう。

会社としての管理ポリシーを実装するためのクラスライブラリを作成します。クラスライブラリでは System.Web と System.Configuration を参照設定し、 WebEventProvider の派生クラスを作ります。

image

using System.Web.Management;

namespace EnterpriseManagementPolicy
{
    public class EnterpriseWebEventProvider : WebEventProvider
    {
        public override void ProcessEvent(WebBaseEvent raisedEvent)
        {
            if (System.Diagnostics.Debugger.IsAttached)
            {
                System.Diagnostics.Debugger.Break();
            }
        }

        public override void Shutdown()
        {
        }

        public override void Flush()
        {
        }
    }
}

Webアプリケーションからクラスライブラリを参照し、web.config の system.web 配下に以下を記述します。

  <system.web>
    <healthMonitoring>
      <providers>
        <add name="Enterprise" type="EnterpriseManagementPolicy.EnterpriseWebEventProvider"/>
      </providers>
      <rules>
        <add name="RouteErrorToEnterprise" eventName="All Errors" provider="Enterprise"/>
      </rules>
    </healthMonitoring>

単純に Home の Index で ApplicationException を throw してみましょう。

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "ASP.NET MVC アプリケーションを簡単に始めるには、このテンプレートを変更してください。";

            throw new ApplicationException("落ちてみるよ!");

            return View();
        }

WebEventProvider の実装コードではデバッガがアタッチしていればブレークするようにしているので、Visual Studio からデバッグ実行するとブレークポイント処理に飛び込みます。

imageimage

やったね!

クイックウォッチで raisedEvent の中身を表示させたのが以下、

image

WebRequestErrorEvent に as キャストしてしまえば例外そのものや、それにまつわるスタックトレース、RequestInformation 配下にはアクセスされたURLまで入っているし、発生日時等一通りの情報が含まれていますのが見て取れますね。

この実装では web.config の修正や参照設定は行いましたがアプリケーションの実装には一切の変更はありませんので、顧客ニーズに応じた処理な global.asax には一切の変更は要らないですし、GACに管理ポリシーを実装するアセンブリを入れて Machine.config/ApplicationHost.config で定義してしまえば実際には web.config の修正も要りません。

http://500error.local/ みたいなローカルイントラネットアプリに HTTP-POST で必要な情報を送り込めば既知バグか判断してくれたり、既知バグだった場合にプライオリティを上げたり、既知バグでなかったら TFS にバグを起票してくれたり、例外の種類がDBサーバ死んでる的なものは DB サーバのチェックコマンドを投げてくれたり色々と自動化のネタが思いつきますね。

既存のWebEventProvider

エラーをメール通知したりとか、一般的な事を何にも書かなかったのはワザとで、エラーのメール通知は標準で存在する WebEventProvider 派生に実装済みです。

メールの送信は以下

SimpleMailWebEventProvider

TemplatedMailWebEventProvider

SQL Server にイベントを書き込むのは以下

SqlWebEventProvider

SQL Server にイベントを書きこむためには aspnet_regsql コマンドで用意されたデータベーステーブルが必要です。

実際に SQL Server にWebEventを書きこむのはどうかと思っていたりしています。(DBがあっぷあっぷしてる時のエラーをDBに書けるかという難問を解けるかですね)

OS のイベントログに書き込むのは以下

EventLogWebEventProvider

エラー時等にOSのイベントログが書かれるのは実際にはこれが動いているからですね。

WMIイベントの発生もできます。

WmiWebEventProvider

そしてIISのトレースインフラに書き込むのが以下です。

IisTraceWebEventProvider

開発中等で見るIISの Failed Request Trace に出ているものはこれが中継しています。

そして、System.Diagnostics.Trace に渡すのが以下

TraceWebEventProvider

ASP.NET のトレースを有効にしていれば、ASP.NET の黄色いアレにイベントが出る事になります。(Trace.axd にも)

カスタムすればどこにでも何とでもなりますけど、標準実装でできる事の範囲内では標準使いましょう。

(Azureでインスタンス飛ぶと遅延転送されるログが消えるのがまずいって人は重要なものを選定してストレージに流せばいいんじゃないかな?)

カスタムイベント

さて、WebEventProvider はイベントをどっかに持っていく物だったりしますが、WebEvent そのものもカスタム定義をする事ができます。

アプリケーションでカスタムイベントを使う事はアプリケーションの要件によりますが、たとえばECサイトとかを作っている場合、お金と商品のやり取りに関するデータだけは絶対に失いたくない物だったりしますよね。こういう物をカスタムイベントを使って WebEvent を流して、 SqlWebEventProvider とカスタムななんかしらのストレージに流す仕組みを作ると、それだけで絶対に失いたくない物のロギングが二重化されます。そのログ2通りからDB状態の整合を取るツールを作っておけば、何か起こった時の命綱として安心感をもたらしてくれる事でしょう。

逆に商品の在庫切れのようなものをWebEventを流しておけば、標準のメール通知のWebEventProviderで管理者に通知する事や、カスタムのWebEventProviderによって商品に応じた担当者に向けた通知を発行するなんて事も可能な形になります。えてしてこういう便利機能をアプリケーションに積み込んでいくことが実際の顧客向けのオンライン性能の制約になったりしますのでWebEventの発行とWebEventProviderでのイベント処理でステージが分離される事のメリットは結構あったりします。

んで、カスタムな WebEvent の実装方法です。 WebBaseEvent を派生するクラスを作って必要なメソッドを実装するだけです。リファレンスの最後に実装例も載ってますね。

イベントの発行は該当クラスを new して Raise を呼ぶだけです、カスタム WebEvent の発行には信頼レベルが Midium が必要な点だけは注意が必要です。

F5アタックに対する考慮

カスタムなWebEventProvider 、WebEventを実装する場合や既存のWebEventを何らかのWebEventProviderに接続する前にはF5アタックのような性能飽和攻撃(サービス拒否攻撃)に対して考慮の必要があるでしょう。未認証ユーザーからのリクエスト等を無駄に詳細なロギングをしようとするとあっという間に性能が飽和してアプリケーションの目的とする動作が実行できなくなったりします。認証ユーザーに対してでも同時ユーザー数が十分に多い場合等はアプリケーションの目的サービスの妨げとならないように性能に気を配った実装をする必要があるかもしれません。

まとめ

アプリケーションの Error 処理で Global.asax.cs で Application_Error を書くのはあんまりお勧めできないです。

ロギングしたいなら既存の WebEventProvider でできますので web.config でルール設定すればいいんです。それ以外のカスタムエラー画面出すようにするとかも web.config での設定の範囲でできる事だったりします。それ以上のエラー処理ってあんまり無かったりしませんですか?ログの体裁をむにゃむにゃしたいとかならカスタムなWebEventProvider 作ればいいじゃないかとかとか、log4net に出したいとかも、log4net に出す為の WebEventProvider 作ればよろし。失って困る物は WebEvent 発行して複数の WebEventProviderでログすればまず困らない。

とかいう私も数年前まで Application_Error になんかつじつま合わせを書いていた気がします、無知識だったゆえって感じですので今後は Application_Error を書くことは殆ど無いんじゃないかなって感じです。

Application_Error にエラー処理を書いたら負けかなと思っているとまでは言わないですけど、いろんなつじつま合わせなエラー処理が必要なアプリケーションは設計おかしくないか?という観点で見直してみて頂きたく。

この記事に書いたことのほとんどは、MSDNリファレンスの以下から参照できますので、エラー処理とか壮大なものを設計する前に読んだ方がよさそうだと思った人はどうぞ。

ASP.NET Health Monitoring の概要

では、ハッピーなクリスマスを!

Azure Advent Calender–リバーシの全手順探索と最終的には白黒どっちが勝つのか


本記事は Azure Advent Calender 12/06 の記事です。

最初にすいません、コードをべたべた書いたのですが、書きあがりませんでした。

https://github.com/kazuk/AzureReversiFullCover

実際のコードはこちらなのですが書きあがっていません、リファクタリングもできていないので結構見苦しいコードになってます。とりあえず年内には書きあげるつもりで実装は続けますので…

リバーシの全局面の列挙をなぜ題材にしたのか

パブリッククラウドといえば潤沢な処理リソースをオンデマンドで取得できる環境という事で、目下スーパーコンピューターのランキングに入るような物を簡単に利用できるという素晴らしく便利なものでございます。

というわけで、リバーシの全手順探索をする為のソリューション一式作ってみました(出来上がりませんでした

注意事項としては、64個のますに駒の有無で1bit、駒がある場合で白黒どちらかで1bitですので、ますの情報量は1.5bitですから初期状態で置かれてるますの白黒4bit、空きますの60ますが90bitで現在の手番が白黒どちらの1bitを加えて95bitの空間があります。そこそこ効率を考えて実装していますが、バックトラッキングレコードの作成等を含めて膨大なストレージ容量を消費しますし、膨大なストレージトランザクションを発生しますので多分泣けるでは済まない課金額になります。

こんなもん作って意味あるのといえば、単体としては全くくその役にも立たない物なのですが、95bit空間に渡るものを効率的に分散並行処理をする枠組みをどうやって作るか、設計や実装というのがメインになります。

アルゴリズム

局面の列挙

基本的なリバーシの局面列挙については余り変わったことはしていません。 Piece という Empty / Black / White / Outside の4状態を 100要素のバイト既定のenum配列に用意し、10×10要素の周囲をOutsideとして番兵にして置ける場所の判定と、置いた場合の状態の列挙をします。この盤面を文字列化して盤面の状態として使っています。文字列化するときには現在の手番および、駒の埋まり状況の64bit、駒の色の64bitを16進文字列化しています。

ボードで使われる100バイトの配列は内部でスタック管理する事で使いまわすようにしていますのでGC負荷にはならないようになっています。

列挙された各局面は 90度回転、左右反転、上下反転の組み合わせを使って8通りの意味的な同一盤面の中から最も小さい値を持つ代表局面を選出されます。たとえばボードの初期局面から4か所に先手の黒は置くことができますが、実際にはどこにおいても代表局面は一緒になります。このようにすることで1手毎に局面が理論値で8分の1に減ります。

バックトラッキング

局面の列挙時にバックトラッキング用のレコードがTableStorageに生成されます。バックトラッキングレコードは PartitionKey に行きついた局面、RowKeyはWorkerRoleのインスタンスIDと時刻で特に意味はなし(競合防止用ユニーク値)、Sourceプロパティにカンマ区切りで元の局面が入ります。

バックトラッキング用レコードを RowKey = PartitionKey で競合防止をせずに実装した場合、挿入で1トランザクション、キー競合が発生した場合に読み込みを行って更新で計3トランザクションが理論値としてストレージトランザクションが発生します。このキー競合をRowKey を使って防いでいるので必ず1トランザクションでバックトラッキングレコードが生成できます。(この辺、設計変更が追い付いてません)

バックトラッキングレコードは結果反映にも関連しますので、最終的なプロパティ構成等は後述

生成された局面の重複解消

n手目の状態の列挙が完了するとその要素数が1000以下の場合にはそのまま状態をキューにカンマ区切りでメッセージを生成します。キューの先頭文字は A で、2桁の開始からの手数、ハイフン一文字に続いて4文字目からが盤面の状態を示しています。(Aメッセージ)

n手目の状態列挙が1000要素を超えると、Blobストレージに1000局面毎の盤面の列挙を改行区切りで出力し、入力blob名を指定するキューメッセージが生成されるようになります。(Cメッセージ)

CメッセージからはBlob に続く局面の列挙が生成されます、Cメッセージで生成された続く局面の列挙は複数のノードが吐き出した blob を統合する処理が必要になります。

このblobの統合は2つのblob を統合して2つのblob を吐き出す事の繰り返しで行われます。吐き出される2つのblob は入力された2つのblob の重複を除外して文字列表現で昇順に並べた場合の前半と後半に分離されます。

統合の処理的には全部を読み取って Distinct , orderby してあとは個数ベースで前半と後半で分けて書き込むだけで実現できる事なのでそれをベースに実装しています。

この処理の厄介な点は、複数の blob の統合処理を無駄なくかつ分散して行う事で、これの調停には以下のロジックを利用しています。

局面の最大値、最小値、これに関連づくblob名、および、統合中のフラグをもつレコードを Azure Table Storage に作成し。Azure Table Storageからレコードを列挙し、重複する範囲を持つレコードがあれば統合処理を行います、統合結果として出来上がる二つのblob を再度Table Storage に登録し統合元になったレコードは削除します。二つのblobの統合処理の要求は Queue で行われます。

局面の統合順序は登録順と範囲の重なり方で重みがついています。最も古いレコードを起点として統合対象を選択して統合する形になります。

範囲の重なり方のチェック順は、相手を包含すること、自分が相手を包含する事、相手の最小値が自分の最小値より小さいこと、最後に重なりがある事です。

包含関係にあるものを優先するのは、効率よく含まれる範囲が小さくなるからで、範囲が小さくなれば他者との包含関係も生まれやすくなります。

結果的に統合中のレコードも含めて範囲の重なりがなくなったレコードに関連づくblobは次の局面の列挙に回すためにCメッセージの生成に使われます。

最終局面からの結果反映

最終局面では引き分けたのか、黒白どちらかが勝ったのかの3状態になります。この3種に加えて最終局面にまだ到達していないという状態の4状態が各局面の状態を示し、これがバックトラッキングレコードにこの局面の終結として保持されます。

その手順での局面の列挙が完了していれば、そこにたどり着く手のバックトラックレコードには十分なデータが蓄積される事になりますので、その手順での局面列挙が完了したタイミングで結果反映はトリガされます。

結果反映に使用するルールは以下の通りです。局面が黒手番で、黒勝ちの手があればその局面は黒の勝ちです、局面が白手番で白勝ちの手があればその局面は白の勝ちとなります。この場合ほかの手の結果を知るまでもなく元の局面に結果を反映します。自分手番で自分勝ちの手がない場合には引き分ける手か負ける手しかありません。引き分ける手があれば引き分けるのが道理ですのでその局面は引き分けになります。どこに打っても負ける局面であればその局面は負けです。

引き分けおよび負けを前の局面に反映するためにはその局面への結果通知を蓄積する必要があります、この結果通知はバックトラッキング用テーブルに積み上げます。Azure Table Client ではエンティティ型を複数持つクエリーが作れないため、バックトラッキングレコードは一つのエンティティ型になるように双方に必要なプロパティを設けています。

 

Azure での分散処理

結果的にいえば使えるものは Queue 以外にはあまり要素はありません。Queueはその仕組みとして、キューを監視しているノードのいずれかで処理が実行されますのでキューメッセージに処理してほしい事を書いて放り込めば処理がどこかで実行してもらえる。やることが複数発生したら複数のメッセージを書けばきっと平行処理されるでしょう。

その Queue ですが、Azure Storage Queue は単一配信保障はありません。単一配信保障がないというのは、一つのキューメッセージが複数ノードで処理される可能性があるという事になります。結果的に重複処理等が起こりえます。

なぜ単一配信保障がないのかというと、処理ノードがメッセージを受信後に死んでしまう可能性があるからです。処理ノードがメッセージを受信後に死んでしまった場合には、プログラムが指定する一定の期間(visibilityTimeout)ののちにキューメッセージは再度どこかの処理ノードに取得され処理が行われます。これにより単一配信保障を崩すトレードオフとして耐障害性を獲得しています。

Queueメッセージは単純にはバイト列で、一般的には文字列で、キューメッセージは最大64KBのデータを格納する事ができます。

可能であれば Queue メッセージの64KB に処理に必要な全データを格納する事を検討してください。ぶっちゃけて収まらない事は当然に起こりますが、収まってしまえば処理に必要なデータを取得するためのストレージトランザクションを減らす事ができます。ストレージトランザクションは課金対象アクションなので、減れば減っただけ課金額が下がります。

次に Queue メッセージの処理時間の粒度をできるだけ揃えてください。分散並行処理をする主たる目的は処理効率です、QueueからのメッセージをGetMessage で待ち受けするときに指定する visibilityTimeout はメッセージの種別によらず一つしか指定できないため、Queueメッセージの処理時間にばらつきがある場合には最悪としてキューを分離する等を選択する必要が生まれます。処理時間粒度を揃える必要から Queue を監視するのに複数スレッドを使って処理するのは余り薦められません、CPU負担が重いメッセージを複数同時に引き取ってしまうと処理時間の見積もりが大きく崩れることになるからです。スレッドを使うのを避けると結果的にQueueを分離すると別のRoleによってその処理を引き受ける等を選択する必要が生まれますが、各Roleのインスタンス数等の調整が必要になってしまい、設計が複雑化します。

しかし、処理時間の見積もりが困難な場合があります。処理時間の見積もりが困難なケースではキューから取得されたメッセージを UpdateMessage によって適宜 visibilityTimeout を更新し処理が遅延した場合にも別ノードで取得されないように調整する必要が生まれます。

トランザクション

このサンプルの中でも多数の箇所で行われていることですが、Queueメッセージを元に Table Storage やBlob Storage を更新する事はよくあります。結果的に Table を更新した後に Queue メッセージを削除する等の処理をする事になりますが、Queue メッセージの削除メソッドである DeleteMessage と TableやBlob、その他は一つのトランザクションになっていません。(というか単一トランザクションにできません)

結果的にQueueメッセージのDeleteMessage の直前に実行インスタンスに対して戦車砲が直撃したシチュエーションではQueueのDeleteMessageだけが実行されていない状況が発生する事がありえます。QueueメッセージがTable からデータを取得して更新する場合等はこのシチュエーションを想定してください。Queueへの処理要求メッセージが二重処理されてしまっては困る場合にはメッセージ処理が必要とする入力データを Storage Table や Blob から削除してから DeleteMessage を実行する事で DeleteMessage がされずに残った場合にも入力データがないことで Queue メッセージ処理が失敗し多重処理を防ぐことができます。(この場合にRoleインスタンスが死なないようにしてください)

このあたりは結構複雑な考慮をする必要があります。blob にメッセージの入力データを置き、table が参照し、Queueメッセージには Table のキーを入れたとします。処理の完了時には、blob と table から入力データを取り除き、DeleteMessage をするとして、blob とtableのどちらを先に削除する必要があるでしょうか。blob が残存する事によるストレージ容量課金を気にするのであれば blob を先に削除する必要がありますが、blobとtableは単一トランザクションにできないのでその二つの処理の間にロケット弾の直撃の可能性はあります。table を引いてblob を見に行ったら物がなかった場合でもインスタンスは死なないように、そして今や無効となっている table レコードや、メッセージを後始末して処理が抜けれるようにしましょう。

Queueによる保障処理

入力の抹消での Queue メッセージの多重処理の防止の逆として、出力生成物の残存という事態も考慮の必要があります。blobに何等かデータを作成し、table にレコードを作る事で生成データが既存データと関連づく場合に、blob を作成した後で table レコード作成のほんのちょっとの時間差に掃除のおばちゃんが電源ケーブルをひっかけたりするとblob に作られた一生使われない1TBのデータの課金を永遠に払う羽目になります。

このような金銭コストが大きい残存データを残さないためにも Queue は有用です。Queue に初期 visibilityTimeout を設定したメッセージを投入しておき、これをレコード登録後に削除します。結果としてレコードが作られなかった場合には 初期visibilityTimeout の経過後にQueueからメッセージが取得されて処理されますので、このメッセージ処理で登録されなかった残念な blob を削除する事で無駄な容量課金が減るでしょう。(代わりにQueueへのメッセージ投入と削除の2回のストレージトランザクションが増えたことはトレードオフです)

ただし、table レコードの作成後に保障処理用のメッセージを削除するというこの二つの処理の間に掃除のおばちゃんがバケツをひっくり返してインスタンスが水没死する可能性もないわけではありませんから保障処理では正しくレコードが作成されて生きているデータを削除しないようにするなども考慮の必要があるでしょう。

 

まとめ

コードは全然書ききれなかったのですが、Queue と Table / Blob を絡めて動く物をうまく設計すれば大規模分散処理を実行するプラットフォームとしてAzureを生かす事ができるでしょう。

ただし、「失敗する可能性のあるものは失敗する」という法則があります。この法則は可能性がいかに低くても頻度を大きくしていけば確実になるという事であり、大規模分散処理は大規模ゆえの頻度の大きさによりちょっと不具合があれば確実に失敗するという事になりますので正しく実装することの重要度が非常に高い事でもあります。

自分としてはこの題材をとおして、失敗する可能性のある事柄をあぶり出して行きたいと思っています。