どう使う? Windows Azure テーブル
~ SQL Azure に挑む ~山本 昭宏 (Akihiro Yamamoto)
Twitter : A_Ym
プロフィール
Microsoft 系テクノロジーコンサルタント会社勤務– 興味のあるテクノロジー
Windows Azure, C# 4, LINQ, WPF, WCF, WF, Silverlight Software Factories
Windows Azure の活動– Windows Azure Community
トレーニングキットの翻訳や技術検証、情報の整理をしていました。
– Web メディア @IT 「 Windows Azure Platform 速習シリーズ: SQL Azure -
SQL Azure の機能と制約を理解する」 の記事を執筆させていただきました。
– セミナー Tech Days 2010 「 Windows Azure Platform 徹底検証」 に
て技術検証を行い、スピーカーとして登壇させていただきました。
テーブル ストレージのスペックおさらい
トランザクションスペック (Windows Azure Storage Team Blog より )
1 entity 追加 = 1 トランザクション 100 entity に対する変更の反映 ( エンティティ グ
ループ トランザクション ⇒ EGT を使用しない場合 )
= 100 トランザクション 100 entity に対する変更の反映
(EGT を使用する場合 ) = 1 トランザクション PartitionKey と RowKey が一致した場合のクエ
リ = 1 トランザクション
1 クエリ で 500 entity 返された場合 (継続トークンが発生しない場合) = 1 トランザクション
1 クエリで 5 リクエスト 発生した場合 ( 4 つの継続トークンが発生した場合) = 5 トランザクション
パフォーマンススペック (Windows Azure Storage Team Blog より )
1 パーティションあたりのパフォーマンス– 最大 500 トランザクション / sec
極力パーティションが均等に分散するような設計によりパフォーマンスが向上する
理論値 ?– 参照 500 entity × 500 トランザクション = 250,000
entity / sec– 追加 100 entity × 500 トランザクション = 50,000
entity / sec
出ませんでした。
並列処理実装方法
追加の並列処理 (1/3)
var contexts = new TestTableContext[10000];
Parallel.For(0, contexts.Count(),(idx) =>{ contexts[idx] = EntityController.CreateTestTableContext();
contexts[idx].SaveChangesDefaultOptions = SaveChangesOptions.ContinueOnError | SaveChangesOptions.ReplaceOnUpdate ;}
Int64 from = 0;Int64 to = 1000000;
int counter = 0;int parallelUnit = 100;
using(var manualEvent = new ManualResetEvent(false)){ for (Int64 rowIdx = from; rowIdx < to; rowIdx++) { var entity = new TestTableEntity() { PartitionKey = ((Int64)(Math.Floor((double)rowIdx / 1000))).ToString("0000000000") , RowKey = rowIdx.ToString("0000000000") , Id = rowIdx , Text = "Text_" + rowIdx.ToString("0000000000") };
var context = contexts[(int)Math.Floor((double)rowIdx / to * contexts.Count())];
context.AddTestTableEntity(entity);
使用法
1. 追加に使用する TableServiceContext を生成します。EGT のトランザクション単位は 100 entity に制限されていますので、 1000,000 entity 追加する場合は 10,000 の TableServiceContext を使用します。
2. ManualResetEvent を並列処理の同期に使用します。
追加の並列処理 (2/3)
if (rowIdx % parallelUnit == parallelUnit - 1 || rowIdx == to - 1) { context.SaveChangesAsParallel( (exception) => { if (exception != null) {
}
if (Interlocked.Increment(ref counter) >= (int)(to / parallelUnit)) { manualEvent.Set(); } } ); } }
manualEvent.WaitOne();}
使用法 (つづき)
6. EGT 単位で追加完了時にカウンターを同期インクリメントし、全件追加が完了したらメインスレッドの待機状態を解除します。
3. EGT 単位 (100 entity) で非同期追加処理を行います。
4. メインスレッドを待機状態にします。
5. 例外発生時の処理を行います。
追加の並列処理 (3/3)public static IAsyncResult SaveChangesAsParallel( this TableServiceContext context , Action<Exception> completeAction){ var asyncCallback = new AsyncCallback( (asyncResult) => { DataServiceResponse response;
try { response = context.EndSaveChanges(asyncResult);
completeAction(null); } catch (Exception exception) {
} } );
var ret = context.BeginSaveChanges( SaveChangesOptions.Batch , asyncCallback , null );
return ret;}
TableServiceContext.BeginSaveChanges メソッドの第1 パラメーターに SaveChangesOptions.Batch オプションを指定して EGT 追加処理を行います。
クエリの並列処理 (1/3)
IEnumerable<Expression<Func<TestTableEntity, bool>>> GetWhereExpressions(){ for (int pIdx = 0; pIdx <= 999; pIdx++) { yield return (TestTableEntity entity) => entity.PartitionKey == pIdx.ToString("0000000000"); }}
var context = EntityController.CreateTestTableContext();
var query = from entity in context.TestTableEntity select entity ;
var entities = query.QueryAsParallel<TestTableEntity>( GetWhereExpressions(), null);
使用法
where に使用する式は yield return で返します。これを使用しないと列挙の最後の式しか処理されません。
クエリの並列処理 (2/3)public static IEnumerable<TEntity> QueryAsParallel<TEntity>( this IQueryable<TEntity> query , IEnumerable<Expression<Func<TEntity, bool>>> whereExpressions ) { int whereExpressionsCount = whereExpressions.Count();
var entities = new List<TEntity>(); var syncRoot = (entities as ICollection).SyncRoot;
using (var manualResetEvent = new ManualResetEvent(false)) { ThreadPool.RegisterWaitForSingleObject( manualResetEvent , new WaitOrTimerCallback( (_state, timeout) => { manualResetEvent.Set(); } ) , null , 300000 , false );
foreach (var whereExpression in whereExpressions) { var dataServiceQuery = (DataServiceQuery<TEntity>) query.Where(whereExpression);
var asyncCallback = new AsyncCallback( (asyncResult) => { try { var response = dataServiceQuery.EndExecute(asyncResult);
lock (syncRoot) { entities.AddRange(response.ToArray()); } } catch (Exception exception) { }
2. タイムアウトの設定を行います。
3. Select 式と Wehere 式を組み合わせてクエリを作ります。
5. 結果を lock で同期してリストに追加します。 (4. は次ページ )
6. 例外を処理します。
1. ManualResetEvent を並列処理の同期に使用します。
クエリの並列処理 (3/3) finally { if (Interlocked.Decrement(ref whereExpressionsCount) == 0) { manualResetEvent.Set(); } } } ) ;
dataServiceQuery.BeginExecute(asyncCallback, null); }
manualResetEvent.WaitOne(); }
return entities; }
7. 全てのクエリが完了したらメイン スレッドの待機状態を解除します。
4. メイン スレッドを待機状態にします。
テーブル サービス エンティティの基本形[DataContract][DataServiceKey("PartitionKey", "RowKey")]public class TestTableEntity{ public TestTableEntity() { }
[DataMember] public string PartitionKey { get; set; }
[DataMember] public string RowKey { get; set; }
[DataMember] public DateTime Timestamp { get; set; }
[DataMember] public string TypeAssemblyQualifiedName { get; set; }
public string ETag { get; private set; }
public void SetETag(string etag) { ETag = etag; }
[DataMember] public Int64 Id { get; set; }
[DataMember] public string Text { get; set; }}
テーブル サービス エンティティとして必須のプロパティを用意します。
CLR にマッピングするためのキーを格納するプロパティを用意します。
Etag を保持するプロパティと設定するメソッドを用意します。(テーブル格納対象外)
WCF の データコントラクトとして使用できないので TableServiceEntity クラスは継承せずに、 DataServiceKey 属性を設定します。
継続トークン処理の拡張メソッド化 (1/2)
var context = EntityController.CreateTestTableContext();
var query = from entity in context.TestTableEntity select entity ;
var entities = query.QueryContinuously();
使用法
継続トークン処理の拡張メソッド化 (2/2)public static IEnumerable<TEntity> QueryContinuously<TEntity>(this IQueryable<TEntity> query){ var resultList = new List<TEntity>();
string nextPartitionKey = null; string nextRowKey = null;
do { var dataServiceQuery = query as DataServiceQuery<TEntity>;
if (dataServiceQuery == null) { break; }
if (nextPartitionKey != null) { dataServiceQuery = dataServiceQuery.AddQueryOption("NextPartitionKey", nextPartitionKey);
if (nextRowKey != null) { dataServiceQuery = dataServiceQuery.AddQueryOption("NextRowKey", nextRowKey); } }
var result = dataServiceQuery.Execute(); var response = result as QueryOperationResponse;
resultList.AddRange(result);
nextPartitionKey = null; nextRowKey = null;
response.Headers.TryGetValue("x-ms-continuation-NextPartitionKey", out nextPartitionKey); response.Headers.TryGetValue("x-ms-continuation-NextRowKey" , out nextRowKey); } while (nextPartitionKey != null);
return resultList;}
まとめ
Windows Azure テーブル vs. SQL Azure
並列処理を行えばパフォーマンスで SQL Azure に迫ることができます。
追加処理ではエンティティ グループ トランザクション (EGT) を組み合わせるとスループットが向上し、場合によっては SQL Azure を上回ります。
膨大なデータに対し、インスタンスサイズを大きくしたり、インスタンス数を増やすことによりリニアに対応できます。
ただし、 PartitionKey の設計や並列処理の実装、メンテナンスのコストを考えると SQL Azure で対応可能なことを無理にテーブルで実現するメリットは小さいと言えます。
数 10GB 以上のデータを扱うケースで有効だと考えます。⇒ 現在のところ Dallas との連携が有望だとみています。
参考情報 (1/2)
Web メディア– Windows Azure Storage Team Blog
Understanding Windows Azure Storage Billing – Bandwidth, Transactions, and Capacity(http://blogs.msdn.com/b/windowsazurestorage/archive/2010/07/09/understanding-windows-azure-storage-billing-bandwidth-transactions-and-capacity.aspx)
Windows Azure Storage Abstractions and their Scalability Targets(http://blogs.msdn.com/b/windowsazurestorage/archive/2010/05/10/windows-azure-storage-abstractions-and-their-scalability-targets.aspx)
– @IT 「 Windows Azure ストレージ開発入門(前編) - 初めての
Windows Azure テーブル・ストレージ開発」(http://www.atmarkit.co.jp/fdotnet/dnfuture/winazurestorage_01/winazurestorage_01_01.html)
参考情報 (2/2)
Windows Azure ベンチマーク サイト– Azurescope: Benchmarking and Guidance for Windows
Azure http://azurescope.cloudapp.net/
書籍 (並列処理関連)– More Effective C# ( 翔泳社 )– .NET のクラスライブラリ設計 (日経 BP ソフトプレス)– 究極の C# プログラミング (技術評論社)
おわりご静聴ありがとうございました。