kazuk は null に触れてしまった

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

月別アーカイブ: 7月 2010

Azure Table 用のエンティティデザイナ(書きかけ)


DSL Tools で表題のものを書きましたのでおすそ分け。スクリーンレコーディングを Expression のスクリーンキャプチャで取ったのですが、さすがにFullHDでのスクリーンキャプチャはマシン性能が厳しいですな。SkyDriveへあげるつもりだったんだけど、1ファイル50MBまでって事であがんね(w

 うげ、SkyDrive にあげた物の埋め込みタグが貼り付けられずに切られるんですが、これってアリなんでしょうか、同じ所が提供してるサービスとしてどうよな感じ。

←のバーに出している SkyDrive のパブリック配下に AzureTableEntityDesigner.zip がございますのでご自由にお持ちください。

 Spacesもう駄目か?

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で見てる人には余裕ちゃんでございます。
 

.NET Framework CLR2 & CLR4 で例外の性能を見てみた


簡単なコードで例外の発行性能を見てみた。
 
private void ThrowCatch( int count )
{
    var exception = new ApplicationException();
    for( int i=0;i<count;i++ ) {
       try {
            throw exception;
        }
        catch( Exception ) {}
    }
}
 
を count を大きめに指定して Stopwatch (System.Diagnostics名前空間のね、手でカチカチしたわけじゃないよ) で経過時間を count で割ってみるって感じ。
(実際には初回にはリソースの読み込みやGITなど処理が絡むので、count=10 で動かしてから、count=1000で呼んでみた。
 
大雑把な計測だが Core2Duo T9600 2.8GHzで .NET 2.0 は x86/x64 共に 30 exception per ms 程度。 .NET 4.0 だと x86 は若干性能下がって 28 exception per ms、x64 は大幅に性能向上が見られて 60超 exception per ms をたたき出した。
 
この値を見ると例外の発生を嫌うチューニングは、あんまり意味がないように見えるし、普通ならしないでもOKと判断できる。
ただし、この値はもう一段別の事象についてもパフォーマンスベクトルがあって、デバッガがアタッチしてる時の事も考慮に入れる必要がある。
 
Visual Studio 2010 のデバッガがアタッチしている状況においては、最速を誇った .NET 4 CLR x64 でも 0,6 exception per ms しか出なかった。
要するに例外についての詳細を知りたいと思ってるような状況においてパフォーマンスが100分の1に劣化するって事、最悪ケースを想定すれば、普通に動かすと1分で再現する事に2時間近く浪費する可能性があるって事ね。
 
Azure では ADO.NET Data Service Client によるTableストレージのアクセスが提供されるんだけど、このTableアクセスは System.Net.HttpWebRequest を使う、HttpWebRequestはRESTの文脈でのデータねーよっていう極当たり前のシチュエーションである 404 Not Foundに対して WebException 例外を放り投げる。GetResponse に TryGetResponse があればいいんだけど無いものなんで仕方が無い。あれまー、残念なことだねぇって感じ。
 
結果として IgnoreResourceNotFoundException なんてプロパティがあって、これをセットしておけば DataServiceContext が try catch して無視してくれるけど、Visual Studio でのデバッグ中はデバッガのアウトプットに 「~の初回例外が発生しました」がずらずらと並び、パフォーマンスは思いっきりスポイルされる。
自分には 「~の初回例外が発生しました」がずらずらと並びの状態で本来のアプリケーションのデバッグをできる精神力が無いんで TryGetResponse を作りました。性能に関しての視点としてリリースバイナリーでの実行だけを考える&デバックなんてしねーよwの普通の人には無駄だったりしますんでレシピだけ。
 
 
からソースをダウンロードする。
HttpWebRequest のGetResponse を切り出して拡張メソッドにする。
メンバフィールド触ってる所を以下のようにもにょる
  アクセサ static readonly Func<HttpWebRequest,フィールドの型> funcHogeField を定義する。
  static コンストラクタで以下のようにアクセサを作る
  Expression paramReq = Expression.Parameter( typeof( HttpWebRequest ), "req" );
  funcHogeField =
     Expression.Lambda(
        Expression.MakeMemberAccess( paramReq, typeof( HttpWebRequest ).GetField( "フィールド名", BindingFlags.Instance | BindingFlags.NonPublic ) )
        paramReq ).Compile();
 んでフィールドを見てるところを funcHogeField( this ) にする。
メソッドを呼んでる所も似たり寄ったりの方法( Expression.Call を使う)でサロゲートする。
System アセンブリの中の internal class を使ってる所は適当なラッパークラスにアクセサを纏めて作る。
 
以上でアクセサのinline化がされなてないって若干のパフォーマンスペナルティはありますが TryGetResponse を作る事ができまする。
 
注) Azure Table の改善のためにはデバッグのためにはそれを呼んでいる Data Service Client も直さなければいけませんが、自分は全部HTTP RESTコールをする方向に行ってしまったんで Data Service Client の修正についてはソンナコトシランです。

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でやり取りの詳細がわかるかもしんない。
 

他のQueryProviderに寄生するQueryProviderの作り方


Expression の解釈に介入したい場合とかに利用するパターンなんですが、別のQueryProviderに寄生するQueryProvider を書く事がよくありますんで、コード例おば。
 
public partial class InterceptorQueryProvider : IQueryProvider
{
    IQueryProvider originalProvider;

    public InterceptorQueryProvider( IQueryProvider originalProvider ) { this.originalProvider = originalProvider; }

    public IQueryable CreateQuery( Expression exp )
    {
        return new WeapperQueryableNoType( this,originalProvider.CreateQuery( exp ) );
    }

    public IQueryable<T> CreateQuery<T>( Expression exp )
    {
        return new WrapperQueryable<T>( this,originalProvider.CreateQuery<T>( exp ) );
    }

 
    public object Execute( Expression exp )
    {
        //ここで Expression に対して介入することができる
        return originalProvider.Execute( exp );
    }
    public IEnumerable<T> Execute<T>( Expression exp )
    {
        //ここでExpressionに対して介入することができる
        return originalProvider.Execute<T>( exp );
    }
}
 
public class WrapperQueryable<T> : IQueryable<T>
{
    readonly IQueryable<T> Query;
    readonly InterceptorQueryProvider provider;
    public WrapperQueryable(InterceptorQueryProvider p, IQueryable<T> q )
    {
        this.provider = p;
        this.Query = q;
    }
    public IEnumerable<T> GetEnumerator()
    {
        return this.Query.GetEnumerator<T>();
    }
    public IEnumerable GetEnumerator()
    {
         return this.Query.GetEnumerator();
    }
    public Type ElementType { get { return this.Query.ElementType; } }
    public Expression Expression { get { return this.Query.Expression; } }
    public IQueryProvider Provider { get { return this.provider; } } // Provider だけ置き換える
}
public class WrapperQueryableNoType : IQueryable
{
    readonly IQueryable Query;
    readonly InterceptorQueryProvider provider;
    public WrapperQueryable(InterceptorQueryProvider p, IQueryable q )
    {
        this.provider = p;
        this.Query = q;
    }
    public IEnumerable GetEnumerator()
    {
        return this.Query.GetEnumerator();
    }
    public Type ElementType { get { return this.Query.ElementType; } }
    public Expression Expression { get { return this.Query.Expression; } }
    public IQueryProvider Provider { get { return this.provider; } } // Provider だけ置き換える
}
 
ADO.NET Data Service Client の場合には CreateQuery の戻りが IQueryable なので以下の要領で Intercepter をはさみます
var interceptor = new InterceptorQueryProvider();
return WrapperQueryable<T>( interceptor, dataServiceContext.CreateQuery( entitySetName ) );
 
介入できることのメリットとしては特定の QueryProviderでサポートされない要素をExpressionからそぎ落として抽出をかけるようにするとか、 そぎ落とした操作を Queryable -> Enumerable へ変換する事で、クエリ実行の一部をローカル化する等があります。たとえばAzureではサポートされない集計演算や、order by 操作をクエリ対象で実行するのでなくローカル実行する様に式木を変形してしまえばアプリケーションコードのあちこちを直すのでは無くてミドルとして割り込む形でプラットフォームに無い機能の実装を提供できるでしょう。
 
ちなみに式木をローカル実行する場合には Expression.Lambda で包み、CompileしてDelegateにしてからDynamicInvoke で実行します。リモート実行したい部分についてリモートするメソッドを以下のように用意したとすると
 
public abstract IEnumerable<T> ExecuteRemote( Expression t )
{
}
 
式木の一部をリモート実行する部分については
Expression.Call( Expression.Constant(this), this.GetType().GetMethod(“ExecuteRemote”), Expression.Constant( リモートしたい Expression ) )
 
といった変換が必要になるでしょう。 Queryable.Concat の source1, source2 をリモートでそれぞれ実行して結果をローカルで Concat するとすれば ExpressionVisitor 派生で以下のようにコードします
 
// こういう初期化がされているとして
MethodInfo remoteMethod = typeof(Interceptor).GetMethod(“ExecuteRemote”)
MethodInfo[] enumerableMethods = typeof(Enumerable).GetMethods();
// こう
public override Expression VisitMethodCall( MethodCallExpression node )
{
    if( node.Method.DeclaringType == typeof( Queryable ) && node.Method.Name==”Concat” ) )
    {
         return Expression.Call( enumerableMethods.Where( m=>m.Name==”Concat” ).Single().MakeGenericMethod( elementType ),
                      Expression.Call( Expression.Constant(this), remoteMethod, Expression.Constant( node.Arguments[0] ) ),
                      Expression.Call( Expression.Constant(this), remoteMethod, Expression.Constant( node.Arguments[1] ) ) );
    }
    …
}
 
要するに以下のコードに落ちるわけですね。
 
 Enumerable.Concat<elementType>( this.ExecuteRemote( source1), this.ExecuteRemote(source2) );
 
Queryableには同等のEnumerableメソッドが必ずありますのでローカル実行に入った後の部分は機械的に変換する事ができますが、Generic パラメータが絡んでいるとリフレクションでのGetMethod の結果は激しくあてにならないので( Enumerable.Concat を探そうとして GetMethod( “Concat”, new Type[]{ typeof(IEnumerable<>),typeof( IEnumerable<>) } ) で検索すると出てこないあたり)、ぶっちゃけてマッピング用の辞書は static コンストラクタで構築すること推奨します。(全部列挙しちゃって合うはずのものを Where で抽出して MakeGenericMethod で特化させて使うのが安心かも)。
 
クエリを示す式木のどこがリモート実行可能かとかは下請けに使う QueryProvider 次第だったりしますが Queryable -> Enumerable の変換をしてローカル化してしまえば KVS でjoin できないとかも、ローカルでIEnumerableになってからjoin されるのでリクエストが複数回飛ぶとか無駄要素も拾う可能性とかもろもろの性能面の我慢のしようによっては実行可能という落としどころが得られます。最悪ケースとしてExpressionから必要なIndexを探してみてあればそれを単純に読む、無ければ WorkerRoleにExpressionをうまいこと文字列化して送って「インデックスつくってー」って悲鳴を上げて(Queue書きして) HttpException で 503 Service Unavaiable 投げちゃうのも決して適切とはいえないかも知れませんがとりうる実装のひとつとなります。
(アプリケーション内で使うすべての Expression を取得する方法は .NET Framework内には無かったりしますので、そういうことをする方法をアプリケーションレベルで用意してないと必要なインデックスが準備できてるかは判断できませんのよ!!)
 
Concat とかは単純だったりしますけど、Where句のand項を別々のクエリに振り分ける(例:Queryable.Where( Queryable.SelectMeny( X, Y) , q => q.X.a && q.Y.b )  を Queryable.SelectMeny( Queryable.Where( X , e => e.a ), Queryable.Where( Y, e=>e.b ) ) から Enumerable.SelectMeny( remote1でWhere、remote2でWhere) にしないと各Whereがちゃんと実行できないとか) とかはまじめに実装すると結構大変です。