なますて。

とあるAzureプロジェクトで、パフォーマンスが超悪いものがあるので、それを何とかしている今日この頃です。 

 

さて、Azureでトレンディーなもののひとつに、Windows Azure Table があります。
こいつは、自動でスケールアウトされたり、データを大量に詰め込んでもほぼ定数時間で取得出来たり(PartitionKey, RowKey を指定したとき)と、
使いこなせれば非常に強力なものだと思います。

しかし、使い方を間違ったら…マジやばいです。

 

今回のプロジェクトでも、Azure Table を使用していますが、その使用している部分が非常に遅い…

具体的に何が遅いと言うと

・RDBMSのように、親テーブルがあり、詳細情報用に子テーブルがある。
・親テーブル項目をキーとして、子テーブルを Azure Table から一件ずつ取得している。 

実際には、親テーブルは Sql Azure にいて、詳細情報は Azure Table にいるという変則なのですが…

この、Azure Table から一件ずつ取得が超遅いのです。

速度を調べてみると、Azure Table から一件取得するのにかかる時間は大体50ms。
それを約300件分取得しようとするので…15秒!
(現在は苦肉の策で20件ずつのページング処理化してあります)

 

そんなの、一回で全部取得すればいいじゃないか

 

そこで考えたのが、取得するエンティティのPartitionKeyとRowKeyをOrElseでつなげる。

しかし、取得したいエンティティの件数は動的に変化するので、Expressionクラスを用いて動的にWhereメソッドに渡す式を作ってみます。

 

Expression exp = null;
ParameterExpression paramExp = Expression.Parameter(typeof(HogeEntity), "e");

// Expression : e.PartitionKey == pkey
string pkey = string.Format("prospex-hoge-{0:0000000000}", tenantID);
Expression pKeyExp = Expression.Property(paramExp, "PartitionKey");
Expression pkeyEqualExp = Expression.Equal(pKeyExp, Expression.Constant(pkey));

foreach ( var item in items) {
    // Expression : e.RowKey.CompareTo(rkey) >= 0
    string rkey = string.Format(
        "{0:0000000000}-{1:0000000000}-{2:0000000000}-{3:0000000000}-{4:000}-",
        item.TenantID,
        item.BoxID,
        item.ThreadID,
        item.MessageID,
        item.Language);
    Expression rKeyExp = Expression.Property(paramExp, "RowKey");
    System.Reflection.MethodInfo compareToType = typeof(string).GetMethod("CompareTo",new Type[] { typeof(string) });
    Expression rKeyCompareToExp = Expression.Call(rKeyExp, compareToType, Expression.Constant(where_RKey));
    Expression rkeyGteExp = Expression.GreaterThanOrEqual(rKeyCompareToExp, Expression.Constant(0));

    // OrElse結合
    if ( exp == null ) {
        exp = rkeyGteExp;
    } else {
        exp = Expression.OrElse(exp, rkeyGteExp);
    }
}
 
exp = Expression.AndAlso(pkeyEqualExp, exp);
 
TableServiceContext context = _tableClient.GetDataServiceContext();
IQueryable<HogeEntity> entitys = context.CreateQuery<HogeEntity>("HogeEntity");
var q = from t in entities
        select t;
q = q.Where(Expression.Lambda<Func<HogeEntity, bool>>(exp, paramExp));

 

そして実行…したら例外でました。内容は、

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">

  <code>InvalidInput</code>
  <message xml:lang="en-US">0:One of the request inputs is not valid.
RequestId:d54d290e-bc01-4fa7-9c77-30bde58fde6c
Time:2012-02-19T07:49:39.6028980Z</message>
</error> 

理由はたぶん、URLが長くなりすぎたと思われ。
(TableServiceContext? は、REST API のラッパーであるため、最終的には Azure Table のURLにリクエストを投げているだけ)

 

そこで、とあるサイトで「バッチリクエスト(エンティティグループトランザクション)」で送る必要があるようなことが書かれていたため、試してみた。

 

foreach ( QueryOperationResponse res in context.ExecuteBatch((DataServiceQuery<MessageBodyEntity>)q) ) {
    foreach ( HogeEntity entity in res) {
        data = entity;
    }
}


これで実行…だが結果は同じであった。


とりあえず調べてみて分かった Azure Table の制限。

・長いURLはつかえない。(ブチザッキさんに詳しく書かれている)
・バッチリクエスト(ExecuteBatch)では、$filter は使えない。(上の方法だと、最終的に$filterを使うURLになる)


だめじゃん…


Azure Table はちゃんと設計しないとハマります。