65.9K
CodeProject 正在变化。 阅读更多。
Home

Azure Blob:TimeToLive 和 DeadBlobContainer

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2013年8月10日

CPOL

13分钟阅读

viewsIcon

34626

downloadIcon

118

本文描述了 Azure 存储容器中 Blob 生命周期的设计和实现。

 

 

 

目录

 

特点

  • 基于 Azure Lease Blob
  • 自定义元数据属性 TTL 和 DBC
  • 松散耦合模式
  • 多个工作角色
  • 内置的微型调度器
  • 无需数据库

 

 

引言

Windows Azure Blob 存储是一项服务,它以虚拟存储的形式存储非结构化(虚拟)数据,可以通过 HTTP 或 HTTPS 协议进行访问。Blob 存储在现代分布式架构中扮演着非常重要的角色,用于存储图片、文档、文件、视频、备份、故障排除数据等。大部分这类资源在存储后会“永久存在”。基本上,它们可以独立于创建它们的应用程序而存在。

Azure Blob 存储的新功能(包含在 Windows Azure for .NET SDK 2.0 中,支持 2012-02-12 REST API 版本,例如 Blob 和容器租用)为事件驱动的分布式架构中的 Azure Blob 提供了新的机会。业务处理器可以为写和删除操作在 Azure 存储 Blob 和容器上建立和管理锁。更多详情可以在我之前的文章 使用 Azure Lease Blob 中找到。

基本上,在这种事件驱动的分布式架构中,租用 Blob 用于保存业务处理的事件流,数据存储在 Blob 存储中,业务流程以“即发即弃”的方式(或使用 Azure Service Bus 进行发布/订阅)由短消息驱动。下图显示了一个此类序列场景的示例。

 

 

 

上图显示了 Azure Blob 存储在事件驱动架构中的位置,订单消息被拆分并以并行方式分发给处理器(工作节点),并基于事件流(租用 Blob)进行后续处理,可以触发加速结果并向用户发送最终通知消息。在此处理过程中,事件流和数据临时存储在 Azure Blob 存储中。当进程完成时,这些资源可以在一定时间后删除,或移动到具有不同访问策略的存档容器中。

与其他 Azure 基础设施实体(如 Azure Service Bus)一样,消费者有能力确定资源的生命周期,例如 TimeToLive (TTL),以及将实体移动到 DeadLetterQueue (DQL)。如果 Azure Blob 存储内置这些功能将非常棒,但今天我们必须在消费者端进行构建。

本文展示了如何以松散耦合的方式轻松扩展 TimeToLive (TTL) 和 DeadBlobContainer (DBC) 等功能。

下图显示了标记为 TTLDBC 的 Blob 的要求。

 

上述过程非常直接。我们需要一个服务来检查 Blob 的 TTL(和 DBC)属性,并执行诸如 MoveDelete 等操作。正如我们所知,Azure Blob 没有这些属性,因此我们必须在 Blob 的元数据中创建和存储它们。这是我们在应用程序中对 Azure Blob 的唯一要求。请注意,没有这些 TTL 和 DBC 自定义属性,我们就无法执行移动和/或删除容器中的 Blob 资源。

当然,有一种方法可以为容器中的所有 Blob 标记 TTL 和 DBC,但这将需要额外的服务来执行此过程。

一旦我们为 Blob 标记了 TTL/DBC,我们就需要一个服务(最佳解决方案是工作角色)来扫描容器中的 Blob,以便删除或移动到称为 DeadBlobContainer 的特定容器中。此服务是时间驱动的,例如每 10 分钟一次。在本文中,我将描述用于 Azure Blob 存储的 TTL 和 DBC 功能的工作角色的设计和实现。

最后,在将 Blob 移动到特定的 DeadBlobContainer 时,Blob 元数据中存储的 TTL 属性将被移除。根据应用程序需求,Blob 可以在元数据中包含其他属性,例如 SourceUri 等,以查看 Blob 从何处被移动。

请注意,DBC 功能是 TTL 的可选功能。为了实现“更干净的服务”,必须设置 TimeToLive (TTL) 属性才能开始执行 Move 和/或 Delete 等任务。可以通过设置 TTL 属性元数据来永久删除 Blob。

好的,让我们继续概念和设计。我假设您已掌握 Windows Azure Platform 的工作知识。

 

概念与设计

Azure Blob TimeToLive (TTL/DBC) 的概念基于轮询 Blob,以使其存储在自定义元数据属性 TimeToLive 中的 Utc DateTime 值过期。以下屏幕截图显示了一个自定义 Blob 元数据属性 TTL 和 DBC 的示例。

 

一旦 Blob 被标记为 TTL(以及可选的 DBC 属性),它就准备好进行轮询过程。这是概念和设计的简易部分。应用程序可以轻松地为特定 Blob 设置任何自定义元数据。在将 Blob 移动到 DeadBlobContainer 的情况下,建议在其元数据中添加更多关于 Blob 的详细信息,例如 SourceUri 等。

正如您所知,清理几个容器中的少量 Blob 并不难。但是,要创建一个通用的清理服务来清除和移动来自许多容器(包含数千个 Blob)中的 Blob,需要额外的考虑,例如可伸缩性、性能等。这是 Azure Blob TTL/DBC 的概念和设计的第二部分。

满足服务可伸缩性和性能要求的魔法在于利用 Azure Blob 的功能,例如 Lease BlobContinuationToken 用于从容器中分页获取 Blob。Azure Blob 的这两个魔法功能使我们能够创建一个基于需求可伸缩的设计,只需添加更多的 Worker Role 实例。

以下屏幕截图展示了这部分内容。

 

 

Worker Role 需要一个 Lease Blob 来维护跨多个实例的清除过程的状态。清除作业的状态存储在此租用 Blob 中,用户(或其他应用程序)可以指定将要清除哪些容器(来自任何帐户)。请注意,私有容器必须使用共享访问签名(SAS)和有效的 List/Delete/Read 策略进行寻址。

在将 Blob 移动到 DeadBlobContainer (DBC) 的情况下,Worker Role 需要知道 DBC 容器的帐户。请注意,只有一个通用的 DBC 容器用于清除任何扫描容器中的 Blob。

在轮询期间,Worker Role(一次只有一个)需要独占地获取 Lease Blob 中的作业状态,以便根据存储在此 Lease Blob 中的作业状态开始逐页(分页)提取容器中的 Blob。一旦 Worker Role 获得此 Blob 页,它将释放 Lease Blob,允许另一个实例根据此 Lease Blob 中存储的作业状态执行相同的操作。

通过使用 C# 5.0 的 async/await 关键字,可以使用多个核心以高度并行和异步的方式执行从 Lease Blob 序列中删除和移动 Blob 的过程。特定容器中的 Blob 可以通过前缀进行过滤。按页面提取 Blob(包括它们的元数据/属性)的状态由以下 enum 值指示。

请注意,state=Off 将跳过特定容器的清除过程。当提取 Blob 的过程完成时,它们的状态设置为 Init,并且 timestamp 将更新为 DateTime.UtcNow 值,以指示何时发生此情况,包括已清除 Blob 的总计数。下一次轮询将从此时开始。

 

实现

Azure Blob TTL Blob 服务的实现非常直接,通过 Worker Role 实现。轮询调度程序的清除作业循环显示在以下代码片段中。

public override void Run()
{
    Trace.TraceInformation("WorkerRoleTimeToLiveBlob entry point called, Id={0}", RoleEnvironment.CurrentRoleInstance.Id);

    int poolingTimeInMin = Int32.Parse(RoleEnvironment.GetConfigurationSettingValue("RKiss.TTLBlob.PoolingTimeInMin"));
    TimeSpan sleepingTime = TimeSpan.FromMinutes(poolingTimeInMin);

    while (true)
    {
        var ttl = new TimeToLiveJob();
        Task.Run(() => ttl.StartAsync(sleepingTime)).Wait();

        DateTime dtNow = DateTime.UtcNow;
        sleepingTime = RoundUp(dtNow, TimeSpan.FromMinutes(poolingTimeInMin)) - dtNow;
        Trace.TraceInformation("[{0}] Working", sleepingTime, "Information");
        Thread.Sleep(sleepingTime);
    }
}

async 任务 StartAsync 代表了移动和删除 TTL Blob 的实际工作。作业必须在下一个计划的循环之前完成。当前时间被向上取整到轮询时间,这个差值被设置为睡眠时间。

如您所见,上述实现被封装在静态方法 StartAsync 中,所以让我们看看这个强大的逻辑。

首先,下图展示了此实现中使用的对象的类图。

基本上,我们只有一个抽象,即 Containers 类。此类代表存储在 XML 格式文本的 Lease Blob 中的容器的作业状态。基于此对象,每个 Worker Role 都可以提取其 Blob 的页面以进行移动和/或删除操作。页面结果中的 Blob 数量是可配置的,默认值为 100。

好的,假设我们有了这个 Blob 页面,以下代码片段展示了如何以并行 async/await 方式处理此页面以移动和删除 TTL Blob。

Parallel.ForEach(listBlobs, new ParallelOptions {MaxDegreeOfParallelism = 15}, async blob =>
{
  try
  {
    if (blob.Metadata.ContainsKey("TimeToLive") && DateTime.Parse(blob.Metadata["TimeToLive"]) <= DateTime.UtcNow)
    {
      string ttl = blob.Metadata["TimeToLive"];

      if (blob.Metadata.ContainsKey("DeadBlobContainer") && !string.IsNullOrEmpty(connectionstringForDBC))
      {
        var client = CloudStorageAccount.Parse(connectionstringForDBC).CreateCloudBlobClient();

        string destRef = blob.Metadata["DeadBlobContainer"];
        if (destRef.EndsWith("/"))
          destRef += blob.Name;
        else if (destRef.IndexOf('/') < 0)
          destRef += "/" + blob.Name;

        CloudBlockBlob destBlob = client.GetBlockBlobReference(destRef);
        destBlob.Container.CreateIfNotExists();

        blob.Metadata.Remove("TimeToLive");
        await Task.Factory.FromAsync(blob.BeginSetMetadata(null, null), blob.EndSetMetadata);

        Task<string> copy = Task.Factory.FromAsync<string>(
          destBlob.BeginStartCopyFromBlob(blob, null, null), destBlob.EndStartCopyFromBlob);
                  
        copy.Wait();
                
        if (destBlob.CopyState.Status != CopyStatus.Success)
        {
          throw new Exception("Copy blob failed, url=" + blob.Name);
        }
                
        Trace.TraceInformation("[{0}] COPY [{1}] {2}", 
          (DateTime.Now - dtStart).TotalMilliseconds.ToString("######"), 
           blob.Metadata["DeadBlobContainer"], blob.Name);
      }

      Interlocked.Increment(ref counter);
      await Task.Factory.FromAsync(blob.BeginDeleteIfExists(null, null), blob.EndDeleteIfExists);
            
      Trace.TraceInformation("[{0}] DELETE [{1}] {2}", 
        (DateTime.Now - dtStart).TotalMilliseconds.ToString("######"), ttl, blob.Name);
    }
  }
  catch (Exception ex)
  {
    Trace.TraceInformation(ex.InnerException == null ? ex.Message : ex.InnerException.Message);
  }
});
如您所见,目标 Blob 名称是根据 DeadBlobContainer 值可配置的,并带有可选的结尾字符 '/',这允许将原始 Blob 名称保留在指定路径下。如果 DBC 不存在,也可以动态创建。所有 Azure 存储 Blob 客户端的 Begin/End 方法都作为 async/await 任务运行。

以下代码片段展示了我们如何获取 Lease Blob 序列中的 Blob 列表。

var container = job.Container.AsParallel()
  .FirstOrDefault(c => c.Status == TokenStatus.Init || c.Status == TokenStatus.Current);
  
if (container != null)
{
  Trace.TraceInformation("[{0}] Get {1}", 
    (DateTime.Now - dtStart).TotalMilliseconds.ToString("######"), container.ToString());

  var cbc = new CloudBlobContainer(new Uri(container.Uri));

  var brs = await Task.Factory.FromAsync<BlobResultSegment>(
    cbc.BeginListBlobsSegmented(container.Prefix, true, BlobListingDetails.Metadata, pageSize, 
      new BlobContinuationToken() { NextMarker = container.Token }, 
      new BlobRequestOptions(), null, null, null), 
    cbc.EndListBlobsSegmented);
      
  if (brs.ContinuationToken != null)
  {
    container.Token = brs.ContinuationToken.NextMarker;
    container.Status = TokenStatus.Current;
  }
  else
  {
    container.Token = string.Empty;
    container.Status = container.Status == TokenStatus.Init ? 
       TokenStatus.FirstAndlast : TokenStatus.Last;
  }
  listOfBlobs = brs.Results.Cast<CloudBlockBlob>().ToList();                 

  Trace.TraceInformation("[{0}] Put {1}", 
    (DateTime.Now - dtStart).TotalMilliseconds.ToString("######"), container.ToString());
}
else
{
  // nothing to do
  Trace.TraceInformation("DONE status=Init");
    
  job.Container.AsParallel().Where(c => c.Status != TokenStatus.Off)
    .ForAll(c => { c.Status = TokenStatus.Init; });
  job.Timestamp = DateTime.UtcNow.ToString();
}

 

我们可以从 Lease Blob 状态获取第一个容器进行分页。然后,我们以 async/await 并行方式异步调用分段 ListBlobs 来获取一个由前缀过滤的 Blob 页面。基于结果,作业状态会更新并返回给 Lease Blob。

一旦所有容器都处理完毕,作业状态就会更新为 Init 状态,并且 job.Timestamp 会被标记。如您所见,这个过程非常快。事实上,它必须如此,因为这是 Lease Blob 的目标,即尽快获取 Blob 页面并释放 Lease Blob,以便其他 Worker Role 实例可以继续。

 

用法与测试

首先,以下是先决条件:

Azure TTL Blob 的使用非常直接,基于标记 Blob 为 TTL 并拥有用于处理移动和/或删除 Blob 从其容器中的服务。在本节中,我将描述如何测试此用法和场景。

首先,让我们描述 Worker Role 服务解决方案。下图显示了本文档包含的 AzureTimeToLiveBlob 解决方案包。

 

 

该解决方案只有一个 Worker Role,其中实现了 TTL Blob 的调度作业。在通过 Azure Emulator 运行 Worker Role 之前,您需要根据您的需求和环境自定义设置。请遵循以下说明步骤。请注意,我们需要使用一个工具来探索 Azure 存储 Blob。您可以使用您喜欢的 Azure 存储浏览器。在本文中,我将使用免费的 Azure Storage Explorer,由 Neudesic 公司提供,并由我的朋友 David Pallmann 构建。

步骤 1. 创建 Lease Blob

  • 使用 Azure Storage Explorer 创建一个私有 Blob,例如:TimeToLiveBlobs,Content Type 为 text/xml,SAS 策略为 RWDL。
  • 生成此 Blob 的 SAS 签名并复制到剪贴板。
  • 打开 Role Settings 并将剪贴板内容粘贴到 RKiss.TTLBlob.LeaseBlob 的 Value 中。
  • 更新您的 Azure 存储帐户的 RKiss.TTLBlob.AccountForDBC 的 Value。这是您首选的 TTL Blob 的 DeadBlobContainer 存储帐户。
  • 仅用于测试目的,将 RKiss.TTLBlob.PoolingTimeInMin 设置为 1 分钟。

 

步骤 2. 创建 Lease Blob 的内容

在此步骤中,我们将创建 Lease Blob 的内容,例如描述 Worker Role 服务将处理 TTL 作业的容器。

  • 从解决方案中打开文件 TimeToLive_template.xml 进行自定义,即添加用于 TTL 作业的特定容器。

Container 元素具有以下属性:

<Container name="myName" 
  status="Init" 
  prefix="" 
  uri="https://myaccount.blob.core.windows.net/events?sr=c&si=mypolicy&sig=mySignature" 
  token="" />
  • 使用您的 SAS 签名容器 URI 更新 uri 属性。请注意,字符 '&' 必须用 '&' 进行转义。
  • 在过滤 Blob 的情况下,我们可以使用标准的 Blob 前缀
  • 请注意,token 属性用于服务存储特定容器的 ContinuationToken
  • 根据您的需要,向集合中添加更多 Container 元素。
  • 现在,将 TimeToLive_template.xml 文件的内容复制到剪贴板,然后粘贴到 Lease Blob 内容中并保存。

好了,基本上我们可以运行我们的 Worker Role 服务了。

 

步骤 3. 运行 Worker Role 服务。

编译解决方案并按 Ctrl+F5 在 Azure Emulator 下运行服务。该服务通过 TraceInformation 检查点进行仪器化,因此我们可以看到 Worker Role 中正在发生的事情。

下图显示了两个 Worker Role 实例正在等待下一次轮询时间。在此示例中,它们正在等待(休眠)将近 6 分钟。此外,您可以看到第一个实例比第二个实例更快,因为重试计数器显示一次轮询已获取 Lease Blob 的周期。

 

另一种查看服务工作方式的方法是使用浏览器弹出 Lease Blob 的内容。每次轮询作业完成后,都会标记时间戳。

现在我们还有一步。这是最后一步,我们可以证明 TTL Blob 正在工作。

 

步骤 4. 标记 Blob 以进行 TTL/DBC

在此步骤中,我们需要将 Blob 标记为 TTL。基本上,这是应用程序的责任,但在我们的测试案例中,我们可以使用 Azure Storage Explorer 来完成。

双击容器中要清除的特定 Blob,然后单击 Metadata 选项卡。

 

 

在上例中,特定 Blob 被标记为 TTL 和 DBC。在 TTL 时间,该 Blob 将被移动到 dbc 容器,前缀为 deleteme/

您可以标记 Lease Blob(参见步骤 2.)中指定的容器中的更多 Blob,并带有不同的 DBC 值,例如:dbc/deleteme

在这种情况下,Blob 将被移动到 dbc 容器,并拥有一个新名称,例如 deleteme。请注意,移动的文件将覆盖现有文件。

以下屏幕截图显示了一个 Worker Role 实例检查点的示例。您可以看到,第二个实例执行了一个 DELETE Blob 操作。

 

 

要查看 Worker Role 实例的工作方式,请使用浏览器弹出 Lease Blob 的内容。以下屏幕截图显示了我的示例。

 

步骤 4. 部署到 Azure

现在我们可以将服务部署到 Azure。下图显示了一个每 10 分钟清除数百个 TTL Blob 的示例。正如您所看到的,两个 ExtraSmall 实例并没有很忙。

 

 

 

 

结论

本文描述了我们如何轻松地为 Azure Blob Storage 扩展 TimeToLive 和 DeadBlobContainer 功能,这与其他 Azure 实体(如 Azure Service Bus)类似。Azure TTL Blob 在事件驱动的分布式架构中将找到一个很好的机会,其中 Lease Blobs 发挥了基本作用,并且 Blob 是动态创建的,必须在特定时间内存在。所描述的概念基于 Lease Blob,用于在多个 Worker Role 实例之间共享 TTL 作业。希望您喜欢。

 

参考文献:

[1] Windows Azure

[2] Windows Azure SDK 2.1 for .Net

[3] Introducing Windows Azure Storage Client Library 2.1 for .NET and Windows Runtime

[4] Lease Blob

[5] How to use the Windows Azure Storage

[6] What's New in Windows Azure

[7] Azure Storage Explorer

© . All rights reserved.