なますて。
とある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 はちゃんと設計しないとハマります。