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回くらいにしておき毎日増分バックアップを取る、 といった別アプローチを検討したほうがよさそうだ。