『星野、目をつぶって。(8)』を読んだ

8 巻は高校最大のイベントと言っていい修学旅行。 体育祭で一躍ヒーローになった小早川だが、だからといって班を組めるかどうかは別問題というのは、マンガなのに厳しい。

この修学旅行で小早川・星野・松方・加納の関係に何か変化が起こるかなと思っていたら、まさかの急展開。おいおい、このタイミングでマジですか?早すぎない? ここからどういう風にストーリーを展開していくのか、ちょっと読めない。 各自のケジメとかどうすんだろうね。今後はそこがメインになるのかな。 ハラハラしてきた。

『かぐや様は告らせたい(7)』を読んだ

7 巻は生徒会選挙編。 対立候補として新たに登場する伊井野ミコが、これまた良いキャラクターだった。清廉潔白かつ品行方正で、前生徒会メンバーとは性格的に衝突しそう。かぐやなんて、目的のためには手段を選ばないタイプなので特に。

肝心の選挙は会長というか元会長が勝つんだが、選挙当日の白銀の行動は器の大きさが表現されていて見事。いやぁ、あんな勝ち方をするとはね。ミコは大方の予想通り生徒会に参加するみたいで、生徒会メンバーとの絡みもこれから増えるだろうし、今後が楽しみ。

Azure Storage に保存したブロブのバックアップを高速化

先日、「Azure Storageに保存したブロブをバックアップ専用ストレージアカウントにコピーする」プログラムを書いたが、素直に実装したため激しく遅かった。並列化すらしていないから当然か。

tnakamura.hatenablog.com

そこで、『Azure Storage Data Movement Library for .Net』というライブラリを使って高速化を試みた。 Microsoft 公式の AzCopy もこいつを使っているらしい。

github.com

ディレクトリをコピーするメソッドが提供されていたので、再起呼び出しが不要になってコードがすっきりした。

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.DataMovement;
using System;
using System.Threading.Tasks;

namespace AzureStorageBackup
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 3)
            {
                Console.WriteLine("AzureStorageBackup <srcConnectionString> <destConnectionString> <containerName>");
                return;
            }

            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            var startedAt = DateTimeOffset.UtcNow;

            var backuper = new Backuper(args[0], args[1], args[2]);

            await backuper.BackupAsync();

            await backuper.EraseAsync();

            var timeSpan = DateTimeOffset.UtcNow - startedAt;
            Console.WriteLine($"TimeSpan = {timeSpan}");
        }
    }

    class Backuper
    {
        CloudStorageAccount SrcAccount { get; }

        CloudStorageAccount DestAccount { get; }

        CloudBlobClient SrcBlobClient { get; }

        CloudBlobClient DestBlobClient { get; }

        string ContainerName { get; }

        public Backuper(string srcConnectionString, string destConnectionString, string containerName)
        {
            SrcAccount = CloudStorageAccount.Parse(srcConnectionString);
            DestAccount = CloudStorageAccount.Parse(destConnectionString);
            ContainerName = containerName;
            SrcBlobClient = SrcAccount.CreateCloudBlobClient();
            DestBlobClient = DestAccount.CreateCloudBlobClient();
        }

        public async Task BackupAsync()
        {
            // コピー元のコンテナが存在しなければ何もせずに即終了
            var srcContainer = SrcBlobClient.GetContainerReference(ContainerName);
            if (await srcContainer.ExistsAsync() == false)
            {
                return;
            }

            // コピー先のストレージアカウントがコピー元のコンテナにアクセスできるようにするために、
            // SAS を発行する
            var blobToken = srcContainer.GetSharedAccessSignature(new SharedAccessBlobPolicy()
            {
                Permissions = SharedAccessBlobPermissions.Read,
                SharedAccessStartTime = DateTimeOffset.UtcNow,
                SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddDays(1),
            });

            // コピー先のコンテナを作成
            var destContainer = DestBlobClient.GetContainerReference(ContainerName + DateTime.UtcNow.ToString("yyyyMMddhhmmss"));
            await destContainer.CreateIfNotExistsAsync();

            // すべてのブロブをコピーする
            await BackupContainerAsync(srcContainer, destContainer, blobToken);
        }

        async Task BackupContainerAsync(CloudBlobContainer srcContainer, CloudBlobContainer destContainer, string blobToken)
        {
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                // ページングされているので、続きを取得するには
                // ContinuationToken を指定する必要がある
                var response = await srcContainer.ListBlobsSegmentedAsync(continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var item in response.Results)
                {
                    if (item is CloudPageBlob pageBlob)
                    {
                        var destBlob = destContainer.GetPageBlobReference(pageBlob.Name);
                        await destBlob.StartCopyAsync(new Uri(item.Uri.AbsoluteUri + blobToken));
                    }
                    else if (item is CloudBlockBlob blockBlob)
                    {
                        var destBlob = destContainer.GetBlockBlobReference(blockBlob.Name);
                        await destBlob.StartCopyAsync(new Uri(item.Uri.AbsoluteUri + blobToken));
                    }
                    else if (item is CloudBlobDirectory directory)
                    {
                        // Data Movement Library を使ってディレクトリごとコピー
                        var destDirectory = destContainer.GetDirectoryReference(directory.Prefix);
                        var status = await TransferManager.CopyDirectoryAsync(
                            sourceBlobDir: directory,
                            destBlobDir: destDirectory,
                            isServiceCopy: true,
                            options: new CopyDirectoryOptions()
                            {
                                Recursive = true,
                            },
                            context: new DirectoryTransferContext());
                    }
                    Console.WriteLine($"COPY {item.Uri}");
                }
            }
        }

        public async Task EraseAsync()
        {
            var aWeekAgo = DateTimeOffset.UtcNow.AddDays(-7);
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                var response = await DestBlobClient.ListContainersSegmentedAsync(
                    $"{ContainerName}",
                    continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var container in response.Results)
                {
                    // 7日前までのバックアップを保持する
                    // 7日より前は削除
                    if (container.Properties.LastModified < aWeekAgo)
                    {
                        await container.DeleteIfExistsAsync();
                    }
                }
            }
        }
    }
}

これでも、60万ファイルのコピーで5時間弱かかった。さすがにこれ以上の高速化は難しいか。 フルバックアップは週1回くらいにしておき毎日増分バックアップを取る、 といった別アプローチを検討したほうがよさそうだ。

Azure Storage に保存したブロブのバックアップ

仮想マシンのイメージなんかを Azure Storage にバックアップする記事は探せばすぐ見つかるけど、 Azure Storage に保存しているブロブを別のどこかにバックアップする記事は見つけることができなかったので、 Microsoft.WindowsAzure.Storage を使ってバックアップ用のプログラムを書いてみた。 バックアップ用のストレージアカウントを用意しておいて、 ストレージアカウントをまたいでコンテナの中身をコピーする愚直な方法。

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using System;
using System.Threading.Tasks;

namespace AzureStorageBackup
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 3)
            {
                Console.WriteLine("AzureStorageBackup <srcConnectionString> <destConnectionString> <containerName>");
                return;
            }

            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            var backuper = new Backuper(args[0], args[1], args[2]);
            await backuper.BackupAsync();
            await backuper.EraseAsync();
        }
    }

    class Backuper
    {
        CloudStorageAccount SrcAccount { get; }

        CloudStorageAccount DestAccount { get; }

        CloudBlobClient SrcBlobClient { get; }

        CloudBlobClient DestBlobClient { get; }

        string ContainerName { get; }

        public Backuper(string srcConnectionString, string destConnectionString, string containerName)
        {
            SrcAccount = CloudStorageAccount.Parse(srcConnectionString);
            DestAccount = CloudStorageAccount.Parse(destConnectionString);
            ContainerName = containerName;
            SrcBlobClient = SrcAccount.CreateCloudBlobClient();
            DestBlobClient = DestAccount.CreateCloudBlobClient();
        }

        public async Task BackupAsync()
        {
            // コピー元のコンテナが存在しなければ何もせずに即終了
            var srcContainer = SrcBlobClient.GetContainerReference(ContainerName);
            if (await srcContainer.ExistsAsync() == false)
            {
                return;
            }

            // コピー先のストレージアカウントがコピー元のコンテナにアクセスできるようにするために、
            // SAS を発行する
            var blobToken = srcContainer.GetSharedAccessSignature(new SharedAccessBlobPolicy()
            {
                Permissions = SharedAccessBlobPermissions.Read,
                SharedAccessStartTime = DateTimeOffset.UtcNow,
                SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddDays(1),
            });

            // コピー先のコンテナを作成
            var destContainer = DestBlobClient.GetContainerReference(ContainerName + DateTime.UtcNow.ToString("yyyyMMddhhmmss"));
            await destContainer.CreateIfNotExistsAsync();

            // すべてのブロブをコピーする
            await BackupContainerAsync(srcContainer, destContainer, blobToken);
        }

        async Task BackupContainerAsync(CloudBlobContainer srcContainer, CloudBlobContainer destContainer, string blobToken)
        {
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                // ページングされているので、続きを取得するには
                // ContinuationToken を指定する必要がある
                var response = await srcContainer.ListBlobsSegmentedAsync(continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var item in response.Results)
                {
                    CloudBlob destBlob;
                    if (item is CloudPageBlob pageBlob)
                    {
                        destBlob = destContainer.GetPageBlobReference(pageBlob.Name);
                    }
                    else if (item is CloudBlockBlob blockBlob)
                    {
                        destBlob = destContainer.GetBlockBlobReference(blockBlob.Name);
                    }
                    else if (item is CloudBlobDirectory directory)
                    {
                        await BackupDirectoryAsync(directory, destContainer, blobToken);
                        continue;
                    }
                    else
                    {
                        continue;
                    }
                    await destBlob.StartCopyAsync(new Uri(item.Uri.AbsoluteUri + blobToken));
                    Console.WriteLine($"COPY {item.Uri}");
                }
            }
        }

        async Task BackupDirectoryAsync(CloudBlobDirectory srcDirectory, CloudBlobContainer destContainer, string blobToken)
        {
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                // ページングされているので、続きを取得するには
                // ContinuationToken を指定する必要がある
                var response = await srcDirectory.ListBlobsSegmentedAsync(continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var item in response.Results)
                {
                    CloudBlob destBlob;
                    if (item is CloudPageBlob pageBlob)
                    {
                        destBlob = destContainer.GetPageBlobReference(pageBlob.Name);
                    }
                    else if (item is CloudBlockBlob blockBlob)
                    {
                        destBlob = destContainer.GetBlockBlobReference(blockBlob.Name);
                    }
                    else if (item is CloudBlobDirectory directory)
                    {
                        await BackupDirectoryAsync(directory, destContainer, blobToken);
                        continue;
                    }
                    else
                    {
                        continue;
                    }
                    await destBlob.StartCopyAsync(new Uri(item.Uri.AbsoluteUri + blobToken));
                    Console.WriteLine($"COPY {item.Uri}");
                }
            }
        }

        public async Task EraseAsync()
        {
            var aWeekAgo = DateTimeOffset.UtcNow.AddDays(-7);
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                var response = await DestBlobClient.ListContainersSegmentedAsync(
                    $"{ContainerName}",
                    continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var container in response.Results)
                {
                    // 7日前までのバックアップを保持する
                    // 7日より前は削除
                    if (container.Properties.LastModified < aWeekAgo)
                    {
                        await container.DeleteIfExistsAsync();
                    }
                }
            }
        }
    }
}

当たり前だけど、コンテナ内のブロブが増えれば増えるほど時間がかかる。 Microsoft.WindowsAzure.Storage は内部で REST API を呼び出しているみたいだから、 ただでさえ速いとは言い難いのに。 並列化するか、他のアプローチを考えないといけないかもな。

『アオアシ(11)』を読んだ

武蔵野戦の後半で葦人が覚醒して無双し、ついにAチームへ昇進することに。化け物揃いのAチームに上がって、物語がどうなるのか楽しみ過ぎる。まぁ、すぐにどデカイ壁にぶち当たる気はするが。葦人はどんな選手になるんだろう。予想としては、元ドイツ代表のラームみたいになるんじゃないかと思ってる。

あと 11 巻でも、ダイアゴナル・ランや チャレンジ・アンド・カバーといった、新しいサッカーの知識を得ることができた。読んでいて、少しずつだがサッカーに詳しくなってきた気がするし、試合の見方が変わってきたかも。来年のワールドカップは、今までと違った楽しみ方ができるかもしれない。

Azure Storage を使っていて同時接続数でハマった

Azure Storage に添付ファイルをアップロードする処理を持つ ASP.NET Core MVC(.NET Core) アプリを、 Azure App Service にデプロイしてベンチマークをとってみたら、 スケールアウトやスケールアップしても思ったようにパフォーマンスが上がらなくて、 ボトルネックを調べることになった。 モニターでメトリクスを確認してみたが、App Service Plan の CPU パーセンテージは 30% 程度だし、 メモリも 40% でほぼ一定。SQL Database も使ってはいるが、DTU は 50% 未満でまだ余裕がある。 なのに遅い。

モニタを眺めていてもボトルネックが分からなかったので、Application Insights と、その Profiler を導入してみた。 Application Insights の Profiler でホットパスを確認したところ、AWAIT_TIME で結構時間を食っていた。 AWAIT_TIME は Task を await してから復帰するまでの時間を表しているらしい。 で、肝心の await しているものが何かというと、Azure Storage へのファイルのアップロード。 Azure Storage には 1 ストレージアカウントの IOPS に上限があるので、最初これに引っかかっているのかと思った。 ただ、App Service をスケールアウトするとパフォーマンスが上がっていくので、これが原因とは考えにくい。

Azure Storage へのアップロードには Microsoft.WindowsAzure.Storage を使っているが、 内部では REST API を呼び出しているはず。 外部への通信が詰まっているのかもと思い、情報を探し回ったら、下記の古い記事を見つけた。

ファイル・ダウンロード時の最大同時接続数を変更するには?[C#、VB] - @IT

ServicePointManager.DefaultConnectionLimit のデフォルトが 2 なのは知っていたが、 『外部からアプリケーションへの接続』ではなく『アプリケーションから外部への接続』だったのか。 勘違いしていた。 もし、.NET Core にもこの設定があって、デフォルト値が 2 のままなら、こいつが怪しい。

試しに、下記のように同時接続数の上限を変更してデプロイし直してみた。

// ASP.NET Core アプリケーションから外部のサービスを呼び出すときの
// 同時接続数を増やす。
ServicePointManager.DefaultConnectionLimit = int.MaxValue;
ServicePointManager.Expect100Continue = false;

ベンチマークをとったところ、40% 以上速くなった。 外部サービスへの通信が詰まっていたのが原因だったようだ。 これでめでたしめでたし…かと思いきや、また新たなボトルネックに遭遇。 調査はまだ続くみたいだ。とほほ。

『WEB+DB PRESS Vol.101』を読んだ

iOS 11 最前線

新たに追加された機械学習と AR のフレームワークは、どちらも面白そう。特に AR の主戦場は当分モバイルだと思うので、これから AR を駆使したどんなアプリが出てくるのか期待。自分でも何か作りたいが、残念なことにアイデアが無い。 Swift と Xcode は順当に進化してきている印象。クライアントとサーバー両方とも Swift で開発するのは夢があるが、つい先日クロスプラットフォーム対応を発表した Kotlin の方が現実的かも。あちらはサーバーサイドで Java の資産も使えるから強い。

Java 9 集中講座

ついに導入された Java のモジュールシステムについての記事は、すごく丁寧に解説してあって、SpringBoot を嗜んだ程度の Java 力の自分でも、なんとか読み進めることができた。理解できたとは言えないけど。主にフレームワークで使われると思うので、Spring あたりが対応したらソースコードを読みたいところ。

現場で使う Slack

Slack の使い方が、簡単なものから高度なものまで満遍なく紹介されていて、Slack 導入マニュアルとして保存しておきたいレベル。それにしても、Slack は結構多機能になったな。シンプルであり続けるのは難しいか。基本機能が無料で使えるようになったのは知らなかった。職場の開発マシンはインターネットから隔離されているので、仕事で Slack を使うことはまず無いけど、プライベートプロジェクトで試してみたいと思った。

継続は力なり【第9回】ログのすすめ

自分の場合、ログ(というかメモ)はGFM のタスクリスト形式で紙やテキストファイルに書いている。脳のワーキングメモリの容量が少ないので、何をどこまでやったか忘れることがまぁまぁあり、人並みに仕事をするためには記録は必須。ログを相談相手と考えたことはさすがになかったので、新しい視点だった。

どんとこい! フロントエンド開発【第3回】Vue.jsでお手軽UI構築

昨年から盛り返してきた Vue.js の紹介だけでなく、状態を管理する vuex、ルーティングを実現する vue-router といった周辺ライブラリについても触れていて、期待以上の内容だった。Vue.js はもう、React や Angular に並ぶ存在にまでなったし、むしろ Angular よりも勢いある。Vue.js は小さく始めることができるのが、個人的に一番気に入っている点。Angular も 1 .x の頃はまだ導入しやすかったんだけどねぇ。

WEB+DB PRESS Vol.101

WEB+DB PRESS Vol.101