テスターですが何か?

ホビープログラマ略してHPです

Azure TableStorageの高速化(2) -処理の並列化-

leave a comment »

前回のエントリーでAzureTableStorageではパフォーマンスのためにエンティティグループトランザクションが必須であることを紹介しました。今回のエントリーでは別の高速化方法の検証を行いたいと思います。

検証内容は以下の通りです。

  • 既定の.NET HTTP接続数を増やす
  • 100-continue を無効にする
  • Nagle を無効にする
  • プログラムを並列化する

 

高速化の手法については以下の情報を参考にさせていただきました。

Windows Azure Table – テーブル ストレージのプログラミング(http://go.microsoft.com/?linkid=9734586)

Windows Azure Table の利用 ~ 特性とパフォーマンスの検証(http://msdn.microsoft.com/ja-jp/windowsazure/hh398428)

 

検証環境

  • Webロール
  • VMサイズ: S
  • インスタンス数: 1

※本当は複数インスタンスでやってみたい…

 

検証プログラム

WebロールのASP.NET MVC3で10,000件のデータをAzureTableStorageへ挿入するプログラムです。コントローラーから文字列だけを返し、画面にデータ挿入にかかった時間を表示します。もちろんエンティティグループトランザクションを使用しています。

ソースコードは以下を参照してください。エンティティグループトランザクションは100件が上限なので、100単位でSaveChangesを実行しています。(これがいいのかどうかわかりませんが…)

OreModels.cs

    [DataServiceKey("PartitionKey", "RowKey")]
    public class OreModels
    {
        public string PartitionKey { get; set; }

        public string RowKey { get; set; }

        public string Data { get; set; }
    }

 

OreDao.cs

    public class OreDao
    {
        private TableServiceContext context;

        public OreDao()
        {
            // ストレージアカウントへ接続し、クライアントを作成
            string connectionString = RoleEnvironment.GetConfigurationSettingValue("StorageConnection");
            var account = CloudStorageAccount.Parse(connectionString);
            var client = account.CreateCloudTableClient();

            context = client.GetDataServiceContext();

            // 初回アクセス時はテーブルの作成を行う
            client.CreateTableIfNotExist("OreModels");
        }

        public void AddOre2(IEnumerable<OreModels> ore)
        {
            int Counter = 0;

            foreach (var m in ore)
            {
                context.AddObject("OreModels", m);
                Counter++;

                if (Counter == 100)
                {
                    context.SaveChanges(SaveChangesOptions.Batch);
                    Counter = 0;
                }
            }
            context.SaveChanges(SaveChangesOptions.Batch);
        }
    }

 

HomeController.cs

    public class HomeController : Controller
    {
        public string Index2()
        {
            var Models = new List<OreModels>();
            OreModels OreModel;

            for (var i = 0; i < 10000; i++)
            {
                OreModel = new OreModels();
                OreModel.PartitionKey = "oretachinoAzureTableStoragegakonnaniosoiwakeganai";
                OreModel.RowKey = Guid.NewGuid().ToString("N");
                OreModel.Data = "oretachinoAzureTableStoragegakonnaniosoiwakeganai" + i.ToString();

                Models.Add(OreModel);
            }

            var sw = new Stopwatch();

            var Dao = new OreDao();
            var sw = new Stopwatch();
            sw.Start();
            Dao.AddOre2(Models);
            sw.Stop();

            return string.Format("処理時間:{0}ミリ秒", sw.ElapsedMilliseconds.ToString());
        }
    }

 

ベースライン

まずは、上記ソースの状態で処理時間を計測します。

1回目 17.46秒
2回目 17.80秒
3回目 17.53秒
4回目 18.17秒
5回目 17.83秒
平均 17.76秒

563エンティティ/秒です。ほぼ100件のときと同じスループットです、件数が増加したからといってパフォーマンスには大きく影響しないようです。むしろわずかに(約2%)向上しています。

それでは、この結果をもとに高速化方法の検証を行なっていきます。

 

既定の.NET HTTP接続数を増やす

「Windows Azure Table – テーブル ストレージのプログラミング」の9.7.2章で紹介されている方法です。デフォルトのHTTP同時接続数は2ですが、この値を増やしてパフォーマンスをアップさせます。Windows Azure Storage Client Libraryの内部ではTableStorageのREST APIを通じてデータアクセスを行なっており、HTTP同時接続数を増やすことでパフォーマンスが改善する可能性があります。

プログラムが直列で処理を行なっているため、あまり効果はなさそうに思えますが…後述するプログラムを並列化したときに効果が現れるような気がします。Global.asax.csのApplication_OnStartメソッドでServicePointManager.DefaultConnectionLimitの値を設定します。今回はとりあえず48に設定しました。

ServicePointManager.DefaultConnectionLimit = 48;

結果は以下の通りです。

1回目 18.34秒
2回目 18.05秒
3回目 18.51秒
4回目 17.78秒
5回目 17.67秒
平均 18.07秒

あれ?速くなってません、ほぼ誤差に近いですが2%弱遅くなってしまいました。デフォルトのHTTP同時接続数が2で、プログラムは直列で処理を行なっているので、同時接続数を増やす方法は効果がないのかもしれません。後述するプログラムを並列化する方法と合わせることで効果があるのかもしれません。

 

100-continue を無効にする

「Windows Azure Table – テーブル ストレージのプログラミング」の9.7.3章で紹介されている方法です。が、読んでもよく分かりません。クライアント-サーバー間のラウンドトリップを挿入するモデルの件数分行うのではなく、1回で行う設定(?)でしょうか。すみません、興味のある方はホワイトペーパーを読んでみてください。

Global.asax.csのApplication_OnStartメソッドでServicePointManager.Expect100Continue の値をfalseに設定します。

ServicePointManager.Expect100Continue = false;

結果は以下の通りです。(ベースラインの状態から100-Continueを無効にする設定のみを行なっています、前述したHTTP同時接続数はベースラインと同じ状態で実行しています。

1回目 17.86秒
2回目 17.11秒
3回目 18.27秒
4回目 17.40秒
5回目 17.51秒
平均 17.63秒

う~ん、かわりません0.5%改善されましたが、誤差の範囲でしょう。

 

Nagle を無効にする

「Windows Azure Table – テーブル ストレージのプログラミング」の9.7.4章で紹介されている方法です。エンティティの挿入や更新の待機時間が大幅に短縮されることがあるようです。ただ「短縮されることある」なので、十分な検証が必要なようです。小さなエンティティが大量にある場合に効果があるようです。

これもGlobal.asax.csのApplication_OnStartメソッドで指定します。

ServicePointManager.UseNagleAlgorithm = false;

以下のサイトでの検証結果によると、この設定はある程度有効なようです。

TableストレージにおけるExpect100ContinueとUseNagleAlgorithmの効果(1)~1件のエンティティ追加の場合

1回目 16.34秒
2回目 17.79秒
3回目 17.94秒
4回目 18.16秒
5回目 17.87秒
平均 17.62秒

う~ん、変わりません。0.5%改善されましたが、誤差の範囲でしょう。この設定も効果がありませんでした。

 

プログラムを並列化する

OreDao.cs内でforeach構文を使用してTableServiceContext.AddObjectメソッドを複数のスレッドから呼び出すようにしようと思いましたが、同一インスタンス内の別スレッドでAddObject&SaveChangesすると例外が発生します。AddObjectはマルチスレッドで実行してもいいと思いますが、SaveChangesをマルチスレッドで実行すると例外が発生します。じゃ、AddObjectだけマルチスレッド化すればいいのかというと、エンティティグループトランザクションが最大100エンティティなので、それほど単純なものでもなく苦労しました。

結果、Controllerで10000件のエンティティのコレクションを作成後、Parallel.Forループを使用してループ内でDaoのインスタンスを生成、100件のエンティティを抜き出してエンティティ登録メソッドを呼び出すようにしています。100件ずつ、マルチスレッドでエンティティを登録できるようになっています。

まぁ、説明を読んでもわかりづらいのでソースを見てください。

HomeController.cs

    public class HomeController : Controller
    {
        public string Index2()
        {
            var Models = new List<OreModels>();
            OreModels OreModel;

            for (var i = 0; i < 10000; i++)
            {
                OreModel = new OreModels();
                OreModel.PartitionKey = "oretachinoAzureTableStoragegakonnaniosoiwakeganai";
                OreModel.RowKey = Guid.NewGuid().ToString("N");
                OreModel.Data = "oretachinoAzureTableStoragegakonnaniosoiwakeganai" + i.ToString();

                Models.Add(OreModel);
            }

            var sw = new Stopwatch();

            sw.Start();
            Parallel.For(0, 100, i =>
                {
                    var Dao = new OreDao();
                    var m = Models.Skip(i * 100).Take(100);
                    Dao.AddOre(m);
                });
            sw.Stop();

            return string.Format("処理時間:{0}ミリ秒", sw.ElapsedMilliseconds.ToString());
        }
    }

 

OreDao.cs

    public class OreDao
    {
        private TableServiceContext context;

        public OreDao()
        {
            // ストレージアカウントへ接続し、クライアントを作成
            string connectionString = RoleEnvironment.GetConfigurationSettingValue("StorageConnection");
            var account = CloudStorageAccount.Parse(connectionString);
            var client = account.CreateCloudTableClient();

            context = client.GetDataServiceContext();

            // 初回アクセス時はテーブルの作成を行う
            client.CreateTableIfNotExist("OreModels");
        }

        public void AddOre(IEnumerable<OreModels> ore)
        {
            foreach (var m in ore)
            {
                context.AddObject("OreModels", m);
            }
            context.SaveChanges(SaveChangesOptions.Batch);
        }
    }

 

そして、実行結果です。

1回目 7.8秒
2回目 6.5秒
3回目 5.0秒
4回目 6.6秒
5回目 8.3秒
平均 6.8秒

キタ━━━━━━(゚∀゚)━━━━━━ !!!!!

10,000エンティティの登録が6.8秒です。1,400~1,500エンティティ/秒です。

エンティティグループトランザクション利用前が5エンティティ/秒なので、ほぼ300倍の性能向上です。

.NET4登場時に話題になりながら、現在はほとんど誰も見向きもしなくなったParallelクラスはとんでもないポテンシャルを秘めています。既存のForやForEachからの書き換えもそんなに大変ではないですし。

 

全部のせ

最後に、これまでに紹介した方法をすべて適用してみます。結果が楽しみですね、+(0゚・∀・) +

1回目 7.2秒
2回目 5.1秒
3回目 9.9秒
4回目 5.0秒
5回目 8.5秒
平均 7.1秒

あれ?5%ほどパフォーマンスが落ちてしまいました、それでも1,400エンティティ/sを達成していますが。シングルコアCPU(インスタンスサイズSは1.6GHz×1)でマルチスレッド処理を行なっているせいか、処理時間のばらつきが大きかったです。全部で10回ほど計測して中間の5件を採用したのですが、12秒かかることもあれば、3.9秒(2000エンティティ/s!!!)で処理が終了することもありました。

ちなみに、ローカルのエミュレーター(ADM Phenom2 X4 955 3.2GHz,メモリ8GB)では15秒前後です。ハードウェアスペックは圧倒的な差がありますが、それでもAzure上の処理時間の方が速いのは特筆すべきですね。

 

結論

  • データ件数が少なくても、AzureTableStorageのCUD操作にはエンティティグループトランザクションは必須。単純なデータ登録であれば500エンティティ/sまでスループットを高めることができる。
  • データ件数が多くなれば、.NET4のParallelクラスを利用してループ処理を並列化する。ただし、SaveChangesは同一インスタンスでマルチスレッドすることはできない、エンティティグループトランザクションは最大100件までであることに注意。単純なデータ登録であれば1,500エンティティ/sを期待できる。
  • 既定の.NET HTTP接続数を増やす、100-continue を無効にする、Nagle を無効にするは効果がない可能性がある。

今回検証用に作成したソースはこちらからダウンロード可能です、最後の全部のせ状態になっています。「oretachinoAzureTableStorage_2.zip」をダウンロードしてみてください。

 

さぁ、最後はSQL Azureとのガチンコ勝負ですね。乞うご期待。

Written by david9142

2011年10月26日 @ 9:02 PM

カテゴリー: WindowsAzure

Tagged with ,

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。