kazuk は null に触れてしまった

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

カテゴリーアーカイブ: Azure

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を生かす事ができるでしょう。

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

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

広告

Azure and Visual Studio 2012 : サーバーエクスプローラーでつなぐ


新ポータルで表示まわりが変わりーの、Visual Studio 2012 で設定まわりが変わりーのでわけわかめなのでメモ

Visual Studio のサーバーエクスプローラーで Azure Storage を見るための設定など。

VS側のサーバーエクスプローラーでAdd New Storage Account でアカウントを登録するダイアログを出す。

image

image

ポータル上の表示は Account name はストレージの表示名(作成時の URL の欄で入力したもの)、Account key は Access Key を入れる。

imageimage

まとめ

用語が揺れ揺れすぎてもう… Azure でアカウントと言ったら請求関連のあれのどっかかなーとか、そういう類推は効きません。ってことが必要知識です!

jp110311 の開発で得られた ASP.NET / 携帯 / Azureでの開発ノウハウあれこれ


サンプルをちょっと書いて乗せてみる程度では得られなかった知見という事で簡単に箇条書きしてみます。

Azure Table の continuation は少なくとも Web Frontend なアプリケーションでは無意味だし不要

今回は携帯対応という事で、どうせ表示できるのは多くても数十件です。この時に1000件単位での continuation はおおよそ無意味で、バッチで舐めるような操作でも作らない限りには不要です。

んで、ページング操作ですが、基本的には RowKey を並べたカラムを持つレコードをインデックスレコードとして作り、このインデックスに並んだ RowKey を or で並べて select します。((PartitionKey=’PK1’) and (RowKey=’RK1’)) or ((PartitionKey=’PK2’) and (RowKey=’RK2’)) を選択するのは普通には以下の形になります。

はい、いきなり式木操作です。

var qidx = context.CreateQuery< Indexのエンティティ型 >( “table” );
var indexEntity = qidx.Where( idx=>idx.PartitionKey== パーティションキー
                   && idx.RowKey== “INDEX” ).SingleOrDefault();
if( indexEntity==null ) return Enumerable.Empty< 戻りエンティティ型 >();

ParameterExpression param = Expression.Parameter( typeof( 戻りエンティティ型 ) );
MemberInfo miPartitionKey = typeof(戻りエンティティ型).GetProperty( “PartitionKey”);
MemberInfo miRowKey = typeof(戻りエンティティ型).GetProperty( “RowKey”);
Expression predBody = null;

foreach( var k = indexEntity.IndexColumn.Split( “;” ) )
{
    if( predBody==null )
    {
        predBody = Expression.And(
             Expression.Equal( Expression.MakeMemberAccess( param,miPartitionKey ),
                                           Expression.Constant( パーティションキー, typeof(string) )),
             Expression.Equal( Expression.MakeMemberAccess( param,miRowKey ),
                                           Expression.Constant( k, typeof(string ) );
    }
    else
    {
        predBody = Expression.Or(
             predBody,  
             Expression.And(
               Expression.Equal( 
                        Expression.MakeMemberAccess( param,miPartitionKey ),
                        Expression.Constant( パーティションキー, typeof(string) )),
               Expression.Equal( Expression.MakeMemberAccess( param,miRowKey ),
                                             Expression.Constant( k, typeof(string ) );
    }
}
if( predBody==null ) return Enumerable.Empty<戻りエンティティ型>();
var pred = (Expression<Func< 戻りエンティティ型,bool >>)
                 Expression.Lambda( predBody, param );
var q = context.CreateQuery< 戻りエンティティ型 >(“table”);
return q.Where( pred ).AsEnumerable();

この (PartitionKey and RowKey) or (PartitionKey and RowKey ) といように パーティションキー、 ローキーをそれぞれ評価しての形のクエリだと、10件程度をとれるクエリまでは正しく処理されるようです。それ以上になるとクエリが長すぎる的エラーで結果が返りません。(クエリの長さなので、パーティションキーやローキーのデータ項目長によって可能な件数が変わるでしょう)

これを変形して PartitionKey=X and ( RowKey=A or RowKey=B ) の様に RowKey だけを or 続きでつないでいくことができます。この形式でとる方が PartitionKey が限定されてエンティティグループトランザクションがかかりやすい、分散クエリにならないはずなので早いという事になります。都度都度 PartitionKey を指定しない分 or 項数を増やせ結果的に一発でとれる件数が増えます。

RowKeyの設計次第ですが、これでとれるのは最大でも数十件(多くて30ぐらい)という所になります。

普通の Web アプリの場合にはこれで数十件が取れれば十分よね(アプリケーションの画面でのページング単位は大抵の場合数十件)って事で、アプリケーション画面の設計に合致しやすい形になります。

50件ぐらいをとりたい場合はフェッチ密度によって戦略が解れます。フェッチ密度が十分に高い場合(各行のサイズが小さく、選択範囲内で75%以上の行をフェッチするなら)にはRowKey の Min/Max で範囲を抽出し、indexで示すレコードのみにフェッチ後にフィルタします。

var qidx = context.CreateQuery< Indexのエンティティ型 >( “table” );
var indexEntity = qidx.Where( idx=>idx.PartitionKey== パーティションキー
&& idx.RowKey== “INDEX” ).SingleOrDefault();
if( indexEntity==null ) return Enumerable.Empty< 戻りエンティティ型 >();
var rowKeys = indexEntity.KeyListColumn.Split(“;”).Skip(start).Take(max).ToArray();

string minKey = rowKeys.Min();   // Enumerable.Min/Max には string 版はありません
string maxKey = rowKeys.Max(); // 拡張メソッドで増やしてあげて下さい。
var q = context.CreateQuery< 戻りエンティティ型 >(“table”);
return q.Where( item=> item.PartitionKey==パーティションキー
                && string.Compare( item.RowKey, minKey)>=0
                && string.Compare( item.RowKey, maxKey)<=0 )
            .AsEnumerable()
            .Where( item=> rowKeys.Contains( item.RowKey ) );

フェッチ密度が低い場合は最初のアプローチでの RowKey 指定で複数列を選択するのをループする形になります。

INDEX ってレコードもってないよ!って場合には実際としてどんな RowKey でとればいいかは自然キーで推測可能という事でない限り解りません、1000件帰ってくるかもしれないクエリ打ってみるしかないになりますので、面倒でも何等かINDEXレコードを持たないと多分性能は出ません。

 

サービスクラスの Testability(テスト可能性)を確保するためのパターン

BbsEntityServicesに実装されています。

http://jp110311.codeplex.com/SourceControl/changeset/view/7674#90653

  1. 開発ストレージに振り向けるための仕組みを持ちましょう。
  2. SaveChanges の発行はメソッドを用意し集約し、SaveChanges発行前イベントを持ちましょう。

1 は開発ストレージにテストでのアクセスを振り向ける為に必要です。CloudStorageAccount.FromConfigurationSetting で通常 CloudStorageAccount を取得しますが、このメソッドは RoleEnvironment がしっかりあって、アプリケーションの初期化コードをちゃんと通らないと動作しません。(SEH例外というイヤーな物が飛んできます)

要するに普通に MsTest テストコンテナでは動くはずもない物なので単純にそれを使ってストレージアカウントにアクセスしているとテストコンテナ内では動きません。

Azure SDK 1.3 まででは開発ストレージではエンティティグループトランザクションは未サポートで、動作しないだけでなく、エンティティグループトランザクションの指定である SaveChangesOption.Batch を指定するとエラーになります。このため、開発ストレージではエンティティグループトランザクションの実際の動作テストはおろかエンティティグループトランザクションを利用するコードのテストもできません。(all or nothing にならないだけでいいのに)

このため、開発ストレージを使っている場合にはエンティティグループトランザクションをかけないようにするコード対応が必要となります。

2 はエンティティグループトランザクションの衝突等のテストをする為に必要です、発行前イベントでわざと競合するような更新をしたうえでエンティティグループトランザクションが適切に all or nothing に落ちていることを確認する必要があります。

 

UseUri でのセッションステートを使う時は POST を受ける前にセッションを保存させなければならない

jp110311 では UseUri で携帯向けにクッキーを利用しないセッションステートを設定しています。この場合にセッションに何も保存していない場合、 POST 要求を初めて受けるとASP.NETはSessionID を確定させようとし、セッションが未保存であれば保存した上でリクエストをリダイレクトします。

このリダイレクトによって POST が失われてしまい、色々な操作が失敗する事になりました。

http://jp110311.codeplex.com/SourceControl/changeset/view/7674#94558

BaseController.cs の OnActionExecuting が回避方法です。要するに新規セッションであれば無意味でもとりあえず値を書き込んで、Dirty にし Session がリクエストの終了時に保存されるようにしています。

実は jp110311 のサイト全体として、セッションはログイン維持の為にしか使っていません、ASP.NETの認証インフラがセッションを使っているだけで、アプリケーションとしては使っていないという形です。「セッションなんて使わねーよ、ばーかばーか」です。

Azure Storage を使うセッションステートプロバイダは独自実装しようと思ったんですが、first release をとにかく早く出したかったって事で出来あいの物を使ってます。

こいつはいくつか問題を抱えてます。

  1. 770行目あたり、 itemData と staticsData を reader.ReadLine で読むところ、セッション保存されてない場合にはこれらは null になりますが Convert.FromBase64String に渡してて ArgumentNullException を飛ばされます。
  2. Expire処理が全般として実装されてませんので、溜まるいっぽうになります。
  3. StorageAccount 回りのテスト可能性の所が全く考慮されてないので、テストが死ねます

 

UseUri でのセッションステートを使う場合 HtmlHelper.BeginForm() は使えない

使えないのはパラメーター無しのメソッド、HtmlHelper.BeginForm( null ) で!でないとフォームのactionがUrlセッションがついてないアドレスに飛ばされ、リダイレクトされてPOSTデータが取れない。

トランザクションを減らすという考え方


Windows Azure Storage Service を用いた在庫管理の設計 – The First Virtue – Site Home – MSDN Blogs

自分はこうしないなー等と思ってみた。

 

そもそもの「注文処理では、この在庫管理テーブルの商品から在庫量を読み取り、必要な注文数が確保できるかどうかを確認します。」の前提が自分の思う所とは食い違うって感じ。

 

歴史的な流れとして、システムのアーキテクチャで一貫性をどこが提供してきたでしょうか。

長らく使われてきた RDBMS に至る流れとしては基本的にはストレージが一貫性を提供するという物です。

「ストレージに保存されたデータは正である」と言っても良いでしょう。

そのストレージをいかに一貫して更新するか、出来るだけ競合を発生させない様にすると共に、発生したとしてアトミックな更新をどうやって提供するのかはストレージが原則として使われてきたわけです。

 

そして、ストレージが一貫性を提供するという考え方の基本は「マシンがいつ落ちるか解らない」という前提に立っているのです。マシンが落ちたとしてもデータを保持し続けるのがストレージの役目ですから。

さて、この前提ははたして現在でも正しいのでしょうか?楽観的悲観的どっちにしてもトランザクションを使って一貫性を提供する理由の第一の前提がこれです。

マシンがいつ落ちるか解らないという様な危うさはもう過去の話でしょう。(まぁ、数万台いればなんかしら落ちてるのは当然でしょうが、1台が頻繁に落ちるなんてのは無いというか、それ修理に出そうぜの世界です。)

 

前提はもう変わった、過去の話だとして考えるならどうでしょう。

1000個を売るのにWebサーバ(アプリケーションサーバ)が4台居たとして、それぞれに250個売らせれば良いなら各アプリケーションサーバに250個づつ売れという4トランザクションで1000個の物は売れます。

100個割り当てて30個売った時点でアプリケーション(の実行サーバが)落ちたとしての障害復旧を考えるなら売上のトランザクションデータを舐めれば70個売れてるって事は解るし、マシンが落ちてからの復旧の手順としてそれを織り込んでおけば1個単位が売れる事での在庫処理トランザクションは必要無いって事です。

受注を書きこむというトランザクションは当然にしっかりしている必要がありますが、そこが担保されているなら在庫処理トランザクションは割り当てられた個数売り切るまではアプリケーションサーバに割当たってるって事だけ知っててそれをカウントダウンすれば済む話です。マシンローカルというか、オンメモリの物だったら Interlocked でオペレーションすれば少なくともアトミック性や一貫性なんて物は自動的に提供されてるよねって話。

障害復旧で過去の全受注を舐めなきゃいけないとなると大きな話でWorkerRoleで多並列で処理してとか大きな事になりますが、在庫をアプリケーションサーバから割り当ててからの一定範囲を、そのアプリケーションサーバでとった受注を舐めるなんて別に大した話では無いでしょう。

 

Application_End で在庫を中央でのストレージベースの管理系に返す、とる時はがばっと取る、がばっと取ったというログレコードをちゃんと残して障害復旧時に舐める範囲を限定できるようにする。個別の受注時にはちゃんと受注明細を残す。これさえ満たせば受注明細を残すのは元々変わらないとしても在庫に関してはいちいちトランザクションを細かくする必要なんてありません。

一貫性を提供するのはどこの何か、安定して動く電源が確保されるならばそれはメモリで何の問題がありますか?電源的なトラブルとかで終了処理もまともにできない様なアプリケーションの落ち方はめったにないよね、あったとしてストレージで一貫性が提供される物をある程度舐めれば済む範囲であればそれを元に障害復旧できるよね。これが満たされる物であれば楽観的同時実行制御の様なトランザクション自体を消す事はできるし、トランザクションすなわち並行性に対する競合点ですので、消せばその分性能は上がるわけですよね。

 

てな感じで考える足がかりを作るとその先は結構トランザクションを消せるし減らせるっていうヨタでございました。

KVSでは join がアレとか言う奴は一回RESTへ帰れ


煽りはタイトルだけ。
 
RowKey の設計レベルの話なんだが、 Table に受注(Order) と受注明細(OrderLine) を格納するとする。
 
Order と OrderLine は親子関係なんで、DBに格納するって場合には Order テーブルと OrderLine テーブルに格納する事になり、これを 1対多結合のリレーションとして捕らえて join する事になる。
 
これを単純に、そのまんまAzureTableに持っていくと Order の RowKey には受注IDが入り、OrderLineは受注IDで引ける必要があるから同様に受注ID+明細IDを入れる事になる。
 
RowKey の編成の仕方は順序でとりやすいように前0埋めの固定長で 受注Id をn桁 明細Idを m桁とする。
 
Table Order
00001 受注レコード
Table OrderLine
000010001 明細レコード
000010002 明細レコード

000010003 明細レコード
 
ってな寸法。(例はn=5, m=4)
 
これを
from order in tableClient.CreateQuery<Order>
where order.RowKey==orderId
select order;
 
from orderLine in tableClient.CreateQuery<OrderLine>
where order.RowKey>orderId && order.RowKey<=orderId+"9999"
select orderLine
 
と引く (string には >= がねーとか言われて通らないのはご愛嬌、string.Compare( a,b,StringComparizon.Ordinal )>0 とか書かないと行けないのはしってる)
 
でも、これ1テーブルに置いちゃってしまえば REST では $filter=(RowKey ge ‘00001’) and (RowKey lt ‘000019999’ ) で1発引きが可能。
 
Azure にはトランザクション課金もないわけじゃないんで、お金的にもこっちの方が良いのは明白。
 
ただし、MS謹製のTableClientは一発のリクエストでは1個のエンティティ型しか扱えないんですよねー、残念でしたって所が罠で、前のエントリーで言ったようにRESTに自分が行った理由でもあったりして。
 
TableClientは捨ててRESTで考えたほうが良いです、RDMBSのテーブルとKVSのテーブルは別物、どっちかというとRDBMSのDBファイル程度の感覚でKVSのテーブルを見たほうが良い。
 
KVSでjoin がアレなんて大嘘だよーって事、だまされちゃいけません。
RowKey をどう編成するかしだいで親子リレーションの一発引きなんてRESTで見てる人には余裕ちゃんでございます。
 

Continuation on Azure Table


メモ、実行確認などはまだしていない、(リファレンスを見ながら「こんな感じ?」って書いてるだけ)
 
コールバック必須だと
 
ResultSegment<T> queryResult;
public IEnumerable<T> ExecuteTableQuery( CloudTableQuery<T> q )
{
     var ar = q.BeginExecuteSegmented( ExecuteTableQueryCallback, q );
     if( !ar.CompleteSynchronously ) ar.AsyncWaitHandle.WaitOne();
     foreach( T item in queryResult.Results ) yield return item;
     while( queryResult.ContinuationToken !=null )
    {
        ar = q.BeginExecuteSegmented( queryResult.ContinuationToken, ExecuteTableQueryCallback, q );
        if( !ar.CompleteSynchronously ) ar.AsyncWaitHandle.WaitOne();
         foreach( T item in queryResult.Results ) yield return item;        
    }
}
 
private void ExecuteTableQueryCallback( IAsyncResult ar )
{
    var q = ar.AsyncState as CloudTableQuery<T>;
    queryResult = q.EndExecuteSegmented( ar );
}
 
コールバックなし可能な非同期パターンなら以下
 
public IEnumerable<T> ExecuteTableQuery( CloudTableQuery<T> q )
{

    ResultSegment<T> queryResult;

     var ar = q.BeginExecuteSegmented( null, q );
     queryResult = q.EndExecuteSegmented( ar );

     foreach( T item in queryResult.Results ) yield return item;
     while( queryResult.ContinuationToken !=null )
    {
        ar = q.BeginExecuteSegmented( queryResult.ContinuationToken, ExecuteTableQueryCallback, q );
        queryResult = q.EndExecuteSegmented( ar );
         foreach( T item in queryResult.Results ) yield return item;        
    }
}
 
動けばこれを元にHTTPリクエストトレースしてRESTでやり取りの詳細がわかるかもしんない。
 

Azure Table ストレージをまっとうに使う為のExpressionVisitor


ランチにビールはかまいませんが、画像をアップするのはテロですのでやめましょう。 などと内輪ねたを最初に持ち出してみます。
 
というわけで、Azure の Table ストレージのKVS固有の制約を取っ払う方法についてです。
 
たとえば、商品マスタをAzureテーブルに乗せたとします、商品コード、商品分類、価格、その他いろいろなプロパティで検索を行いたい場合、これをどうやって実装するかです。
 
価格が1000円代のゲームソフトで機種 xbox な物ならこんなLINQ式になるはずです。
 
from prod in db.Products
  where 1000<=prod.price
      && prod.price<2000 
      && prod.type== "game"
      && prod.platform=="xbox"
  select prod
 
RowKey へのキー重畳によって以下のキーフォーマットを持つと設計すれば良いでしょう。
 
RowKey = 商品コード 8文字 + 商品価格(数字8桁) + 商品種別(英字6文字) + プラットフォーム(英字6文字)
 
これにより、以下の LINQ 式が上記LINQと同等になるわけです。
 
from prod in db.Products
  where string.Compare( prod.RowKey.SubString( 8,8 ) , "00001000", StringComparison.Ordinal ) >=0
   && string.Compare( prod.RowKey.SubString( 8,8 ) , "00002000", StringComparison.Ordinal ) <0
   && prod.RowKey.SubString( 16,6 ).Trim() == "game"
   && prod.RowKey.SubString( 22,6 ).Trim() == "xbox"
 
これがTableストレージの REST の GET での fillter 句は以下のようになるはずです。
 
$filter= (substring( RowKey, 8,8 ) ge ‘00001000’) and (substring(RowKey,8,8) lt ‘00002000’) and (trim(substring(RowKey,16,6)) eq ‘game’) and (trim(substring(RowKey,22,6)) eq ‘xbox’)
 
貴方が Azure ストレージ専用のプログラムを書いているなら要するにこういう LINQ を書き散らせば良いわけです。話は終了
キー項目を増やしたくなったとかになった場合でも64KBまでRowKeyは増やせますので、そうすれば良いだけのこと、やったね!
 
んで Azure ストレージ専用のプログラムを書いてない、他でも通るクエリを書きたい場合にどうするのかって事。
 
ここで ExpressionVisitor クラスの登場となります。
(.NET 3.5 ではクラスライブラリに標準実装は無かったんですが、MSDN上にサンプルがありました。.NET 4では標準です)
 
なんかのプロパティ(またはフィールド)にアクセスしている場所は MemberExpression があります。
 
VisitMember をオーバライドして以下のようにします。
 
protected override Expression VisitMember( MemberExpression m )
{
    MemberInfo mi = m.Member;
    var keyMap =from attr in mi.GetCustomAttributes( typeof( KeyMapAttribute ) ).Cast<KeyMapAttribute>().SingleOrDefault();
    if( keyMap==null ) return base.VisitMember( MemberExpression m );
// 本当はいろいろやるんだけど要するに以下をする
    Expression instance = Expression.MemberAccess( m.Expression, m.Expression.Type.GetMember( keyMap.PropertyName ) )
    retrun Expression.Call( Expression.Call( 
                instance,
                typeof(string).GetMethod( "SubString", new Type[] { typeof(int), typeof(int) } ),
                new Expression[] { Expression.Constant(keyMap.BeginPos), Expression.Constant(keyMap.Length) } )
        , typeof(string).GetMethod( "Trim", Type.EmptyTypes ), new Expression[] {} );
}
 
KeyMapAttribute は以下の定義
 
[AttributeTarget(Property)]
class KeyMapAttribute : Attribute
{
    public string PropertyName { get; private set; }
    public int BeginPos { get; private set; }
    public int Length { get; private set; }
 
    public KeyMapAttribute( string propertyName, int beginPos, int length )
    {
        PropertyName = propertyName;
        BeginPos = beginPos;
        Length = length;
    }
}
 
これで
[KeyMap( "RowKey", 16, 6 )]
public string type { get; set; }
 
としとけば上記のExpressionVisitor が where prod.type == "hoge" の prod.type 部を prod.RowKey.SubString( 16,6 ).Trim() に変換してくれるわけですね。
 
これで普通の LINQ 式でのwhere がキー重畳を意識して RowKey なり PartitionKey を使った格好に書き換えられますので、別のテクノロジ(たとえば LINQ to SQL)向けに書かれた LINQ 式でもかなりが通るようになるわけです。
 
このように Azure テーブルの標準のTableClientはぶっちゃけていえば機能不足というか「ストレージテクノロジの基本特性が露出した状態」にありますので、まだまだアプリケーションで素のまま使うにはアレです。「式木をゴリゴリ操作するなんて怖いこと僕できない」な人にはぶっちゃけまだ手を出すには早い素材と言えるかも知れません。(=人柱希望者でない人は傍観推奨)
 
式木をゴリゴリするのが苦にならない or 苦しかろうが LINQ ですでに書かれたクエリ重要な人はこの辺りいろいろ変換をかける ExpressionVisitor をいろいろ書き散らすつもりで Azure に取り組んだほうがよろしいと思います。
まぁ、うちのばあい、(最近事例が追加で出たみたいなんですが)、CMSの中核は「LINQ クエリを実行して結果をHTMLレンダリングするテンプレートエンジン」だったりして、LINQクエリは「顧客に提供済みのプロダクト上での顧客資産」なわけで、Azureで動かすからといって「KVSだから」うんぬん言うのも無駄な話、できなければ、できるようにするしかないのであります。

System.Web.Management と Azure


ちっとハマッタお話
 
web.config で system.web/helthMonitoring 以下をいじるときで Azure WebRole を使っているときは構成がらみが結構センシティブなんで要注意。
All Events を Trace に振るとかすると、 Development Fabric でWebRoleがぜんぜん起動しなくなったりするから超危険。
 
この辺、Azure はなにやらむちゃくちゃカスタムしている模様なんですが、どういうカスタムしてるか全然情報がなくてこまりまくりんぐ。
 
んで、「唐突に何をって」思う人に向けて話しの展開
 
 
でも言及されてるんだけど、「仮想マシンがクラッシュするとログデータが吹き飛ぶ」(事がある)んですよ。
んで、うちはお金に絡むログデータもあったりしたり、吹き飛ぶことを容認できないケースバイケース要素が非常に多いわけでございます。
なので、絶対吹き飛ばないようにする為に標準の AzureのDiagnosticsMonitorでのログ取りだけでは不十分だったりするわけです。
 
日本におけるAzureの神の一人である酒井さんの本では Queue の使用方法の解説でログを流すサンプルアプリケーションを書いてらっしゃるわけですが、この例はあくまで Queueを使う事を目的としたサンプルなんで「ログを書くことがメイン」なアプリなわけでありますが、Queueはログの出し先としては非常に有用であります。
 
まぁ、詳しくは system.web/helthMonitoring の config リファレンスなんかを見てもらうとしても、 System.Web.Management 配下は「イベントの重要度によるフィルタリングが設定でできる」、「本当に重要なものは即座に処理できる」、「必要であればイベントをどっかへ投げる方法が用意できる or else 用意されている」といいことづくめでございます。
 
要するにログをとりたくなるかも!!って要素は WebEvent 吐いとけ、これ推奨。
んで、重要なログを本当に重要に扱うには WebEventProvider を実装するといいですよ。
んで、何が重要とかはお客やシステムの使われ方によって違うんで、config で構成できる system.web/helthMonitoring は非常に良いわけでございます。
 
仕事で ASP.NET 開発やるならマジ知っとけな名前空間は System.Web.Management 配下って事で AzureQueueWebEventProvider を書いてみたりした今日この頃でありまする。
 
前回の記事も含めてむにゃむにゃProvider しか書いてない今日この頃なんであったりしまする。