你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

针对 Azure 存储的 Gridwich 操作

Azure 存储

Gridwich Azure 存储服务 Gridwich.SagaParticipants.Storage.AzureStorage 可为针对 Gridwich 配置的 Azure 存储帐户提供 blob 和容器操作。 示例存储操作包括“创建 blob”、“删除容器”、“复制 blob”或“更改存储层”。

Gridwich 要求其存储机制适用于 Azure 存储块 blob 和容器。 由于适用于 blob 和容器的类和存储服务操作不同,因此在给定存储操作是与 blob 还是容器相关方面十分明确。 本文同时适用于 blob 和容器(除非另外说明)。

Gridwich 在 Storage.AzureStoragesaga 参与者中向外部系统公开大多数存储操作。 其他 saga 参与者在设置编码工作流时将存储服务用于各种任务(例如在不同容器或帐户之间复制 blob)。

本文介绍 Gridwich Azure 存储服务如何满足解决方案要求并与事件处理程序等机制集成。 链接指向对于的源代码,其中包含有关容器、类和机制的更广泛注释。

Azure 存储 SDK

Gridwich 使用 Azure 存储 SDK 中的类与 Azure 存储交互,而不是手动创建 REST 请求。 在存储提供程序中,SDK BlobBaseClientBlobContainerClient 类管理存储请求。

这些 SDK 客户端类目前仅允许间接访问 Gridwich 需要操作的两个 HTTP 标头(x-ms-client-request-id 用于操作上下文,ETag 用于对象版本)。

在 Gridwich 中,一对提供程序类以称为套筒的单元分配 BlobBaseClientProviderBlobContainerClientProvider 功能。 有关套筒的详细信息,请参阅存储套筒

下图说明了 SDK 和 Gridwich 类的结构,以及实例如何彼此相关。 箭头指示“具有相关引用”。

显示存储 SDK 类之间的客户端对象实例关系的图。

管道策略

创建客户端实例时,可将用于操作 HTTP 标头的挂钩设置为管道策略实例。 只能在客户端实例创建时设置此策略,并且无法更改策略。 使用客户端的存储提供程序代码必须能够在执行过程中操作标头值。 挑战在于使存储提供程序和管道可完全交互。

有关 Gridwich 管道策略,请参阅 BlobClientPipelinePolicy 类。

存储服务缓存

当 SDK 客户端对象实例将其第一个请求发送到 Azure 存储时,TCP 连接建立和身份验证会产生开销。 一个外部系统请求中对相同 blob 的多次调用(例如“获取元数据”,然后“删除 blob”)会增加开销。

为了减少开销,Gridwich 会根据操作上下文使用的 SDK 类,为每个存储 blob 或容器维护一个客户端实例的缓存。 Gridwich 会保留此客户端实例,可以在外部系统请求持续时间内为针对相同 blob 或容器执行的多个 Azure 存储操作使用该实例。

Azure SDK 提供的客户端类要求 SDK 客户端对象实例在创建时特定于单个 blob 或容器。 这些实例也不能保证可安全地在不同线程上同时使用。 由于操作上下文表示单个请求,因此 Gridwich 会基于 blob 或容器名称与操作上下文的组合进行缓存。

此实例重复使用与 Azure 存储 SDK 客户端结构相结合,需要额外的支持代码来平衡效率和代码清晰度。

上下文参数

几乎所有 Gridwich 存储服务操作都需要 StorageClientProviderContext 类型的特殊上下文参数。 此上下文参数满足以下要求:

  • 为外部系统提供响应,其中包括基于每个请求的唯一 JSON 的操作上下文值(外部系统在 Gridwich 请求中进行指定)。 有关详细信息,请参阅操作上下文

  • 允许存储服务调用方(如 Gridwich 事件处理程序)控制哪些响应对外部系统可见。 此控制可防止服务使用无关的通知事件导致外部系统溢满。 有关详细信息,请参阅上下文静音

  • 符合 Azure 存储约定,以确保允许并行读取器和编写器混合的环境中具有一致的请求和响应。 例如,支持 ETag 跟踪。 有关详细信息,请参阅 ETag

存储上下文

Blob 和容器存储类型的上下文是 StorageClientProviderContext,类似于下面这样:

    string  ClientRequestID { get; }
    JObject ClientRequestIdAsJObject { get; }
    bool    IsMuted { get; set; }
    string  ETag { get; set; }
    bool    TrackingETag { get; set; }

前两个属性是用于初始化 StorageClientProviderContext 实例的操作上下文的不同表示形式。 该类具有各种构造函数,包括复制构造函数。 其他方法包括 ResetTo(用于实现就地状态复制)和静态 CreateSafe 方法(用于确保有问题的初始化不会引发异常)。

该类还包含用于基于 GUID 和空字符串创建上下文的特殊处理。 Blob CreatedDeleted 的 Azure 存储通知处理程序(还处理源自外部代理的通知)需要 GUID 表单。

上下文静音

IsMuted 属性控制应用程序是否期望服务将生成的通知发布回调用方(例如发布回外部系统)。 在静音操作中,服务不会发布生成的事件。

例如,编码器执行 blob 复制以将 Azure 存储中的 blob 安排为编码任务的输入。 外部系统并不关心这些详细信息,而只关心编码作业的状态以及可以检索编码输出的位置。 为了反映这些问题,编码器会:

  1. 基于请求操作上下文创建非静音存储上下文,例如 ctxNotMuted

  2. 通过使用上下文类复制构造函数或创建新实例,来创建静音存储上下文,例如 ctxMuted 任一选项都会具有相同的操作上下文值。

  3. 为用于编码的设置中涉及的存储操作指定 ctxMuted。 外部系统看不到发生这些操作的任何指示。

  4. 为反映编码完成的存储操作的指定 ctxNotMuted 上下文,例如将输出文件复制到目标容器。 Gridwich 处理程序会将生成的 Azure 存储通知事件发布到外部系统。

调用方控制操作的最终可见性。 静音和非静音操作都基于等效 operationContext 值。 上下文静音的意图是可更轻松地从事件跟踪日志执行问题诊断,因为无论操作静音状态如何,都可以查看与请求相关的存储操作。

ResponseBaseDTO 具有布尔属性 DoNotPublish,事件调度会使用该属性确定有关是否发布的最后决策。 事件调度进而会基于上下文的 IsMuted 属性设置 DoNotPublish 属性。

服务将静音设置传输到 Azure 存储,后者随后会在它呈现给两个 Gridwich 处理程序(CreatedDeleted)的存储通知事件中设置 clientRequestId。 这两个处理程序会设置 DoNotPublish 以反映调用方请求的静音。

用于实现目标一致性的 ETag

Azure 存储将 HTTP ETag 标头用于应具有目标一致性的请求序列。 例如用于确保 blob 在“检索元数据”与“更新元数据”存储操作之间未更改。

为了与标准 HTTP 用法保持一致,此标头具有不透明值,其解释是,如果标头值更改,则基础对象也已更改。 如果请求为对象发送其当前 ETag 值,并且它与当前存储服务 ETag 值不匹配,则请求会立即失败。 如果请求不包含 ETag 值,则 Azure 存储会跳过该检查,不会阻止请求。

存储服务中的 ETag

对于 Gridwich,ETag 是 Gridwich 存储服务与 Azure 存储之间的内部详细信息。 没有其他代码需要了解 ETag。 存储服务将 ETag 用于“获取 Blob 元数据”、“删除 Blob”操作(用于处理 BlobDelete Event 请求)等序列。 使用 ETag 可确保“删除 Blob”操作将与“获取元数据”操作完全相同的 blob 版本作为目标。

若要对前面的示例使用 ETag,请执行以下操作:

  1. 发送包含空白 ETag 的“获取元数据”请求
  2. 保存响应中的 ETag 值。
  3. 将保存的 ETag 值添加到“删除 Blob”请求。

如果两个 ETag 值不同,则删除操作会失败。 失败意味着某个其他操作在步骤 2 和 3 之间更改了 blob。 从步骤 1 开始重复该过程。

ETag 是构造函数的参数以及 StorageClientProviderContext 类的字符串属性。 只有特定于 Gridwich 的 BlobClientPipelinePolicy 才可操作 ETag 值。

控制 ETag 使用

TrackingETag 属性可控制是否在下一个请求中发送 ETag 值。 值 true 表示服务会发送 ETag(如果有 ETag 可用)。

ETag 值与使用者 blob 或容器不匹配的 Azure 存储请求会导致操作失败。 此失败符合设计意图,因为 ETag 是表示“作为请求目标的确切版本”的标准 HTTP 方式。请求可以包含 TrackingETag 属性以声明 ETags 必须匹配,或者不包含 TrackingETag 属性以指示 ETag 值无关紧要。

如果 REST 响应中存在 ETag 值,则管道会始终从 Azure 存储操作中检索值。 管道会始终自上次操作起更新上下文 ETag 属性(如果可能)。 TrackingETag 标志仅控制来自相同客户端实例的下一个请求是否发送 ETag 属性的值。 如果 ETag 值为 null 或空,则当前请求不设置 HTTP ETag 值,而不考虑 TrackingETag 的值。

存储套筒

Gridwich 要求其存储机制同时适用于 Azure 存储块 blob 和容器。 由于适用于 blob 和容器的类和存储服务操作不同,因此在给定存储操作是与 blob 还是容器相关方面十分明确。

一对提供程序类(一个用于 blob,一个用于容器)按称为“套筒”的单元配发两组功能。 套筒包含属于 Azure SDK 一部分的存储帮助程序类的实例。 初始化存储服务会创建提供程序,并使它们直接可用于存储服务方法。

套筒结构

套筒是 SDK 客户端对象实例的容器和存储上下文。 存储提供程序函数通过两个属性 ClientContext 引用套筒。 有一个用于 blobs 的套筒类型和另一个用于容器的套筒类型,它们分别具有 BlobBaseClientBlobContainerClient 类型的 Client 属性。

blob 的常规套筒结构如下所示:

BlobBaseClient Client { get; }
BlobServiceClient Service { get; }
StorageClientProviderContext Context { get; }

套筒中的 Service 属性是一种便利。 使用 SDK BlobServiceClient 类的一些最终编码器相关操作需要存储帐户凭据。 此要求会导致将服务客户端实例添加到两种现有套筒类型,而不是生成单独的提供程序。

套筒用法

客户端存储提供程序会配发套筒实例。 存储服务代码类似于以下经过批注的代码序列(其中为清晰起见,列出了类型):

public bool DeleteBlob(Uri sourceUri, StorageClientProviderContext context)
{
    . . .
    StorageBlobClientSleeve sleeve = _blobBaseClientProvider.GetBlobBaseClientForUri(sourceUri, context); // Line A
    BlobProperties propsIncludingMetadata = sleeve.Client.GetProperties(); // Line B
    sleeve.Context.TrackingETag = true;   // Send ETag from GetProperties()
    var wasDeleted = sleeve.Client.DeleteBlob(); // Line C
    sleeve.Context.TrackingETag = false;
    var someResult = sleeve.Client.AnotherOperation(); // Line D
    . . .
}
  1. Gridwich 会将操作上下文自动填充到行 A 上的套筒上下文中。TrackingETag 默认为 false。
  2. 在行 B 后面,sleeve.Context 包含来自行 A 的 ETag,并保留相同的 ClientRequestID 值。
  3. 行 C 会发送来自行 B 的 ETag 值和 ClientRequestId
  4. 在行 C 后面,上下文具有新的 ETag 值(在 Delete() 响应中返回)。
  5. 行 D 不会在针对 AnotherOperation() 的请求中发送 ETag 值。
  6. 在行 D 后面,上下文具有新的 ETag 值(在 AnotherOperation() 响应中返回)。

存储服务当前在依赖项注入配置中设置为 Transient,这意味着基于套筒的缓存会基于每个请求。 有关详细信息,请参阅存储服务和依赖项注入

存储服务替代项

以下部分介绍不属于当前 Gridwich 存储解决方案的替代方法。

通过子类创建隐藏管道策略

创建 SDK 客户端类型的子类会将两个简单属性添加到客户端(每个 HTTP 标头值各一个)以完全隐藏与管道策略的交互。 但由于存在深层 Moq bug,因此无法通过 mock 为这些派生类型创建单元测试。 Gridwich 使用 Moq,因此不使用此子类创建方法。

Moq bug 与存在内部范围虚拟函数时跨程序集的子类创建的不当处理有关。 SDK 客户端类会使用涉及对普通外部用户不可见的内部范围类型的内部范围虚拟函数。 当 Moq 尝试创建子类(处于一个 Gridwich 程序集中)的 mock 时,它会在测试执行时失败,因为它在 SDK 客户端类中找不到从中派生 Gridwich 的内部范围虚拟对象。 在不更改 Moq Castle 代理生成的情况下没有解决方法。

存储服务和依赖项注入

Gridwich 当前将存储服务注册为 Transient 依赖项注入服务。 也就是说,每次对服务请求依赖项注入时,都会创建新实例。 如果注册更改为 Scoped(这意味着每个请求一个实例,例如外部系统的请求),当前代码也应正常工作。

但是,如果注册更改为 Singleton(整个 Gridwich 函数应用中一个实例),则会出现问题。 用于套筒和数据字节范围的 Gridwich 缓存机制因而不会区分不同的请求。 此外,缓存模型不是签出模型,因此 Gridwich 不会从缓存中移除正在使用的实例。 由于 SDK 客户端类不能保证线程安全,因此协调需要进行许多更改。

出于这些原因,请勿将 Gridwich 存储服务按原样更改为 Singleton 依赖项注入注册。 Gridwich 在依赖项注入注册中遵循此规则,并包括单元测试 CheckThatStorageServiceIsNotASingleton 以强制实施它。

后续步骤

产品文档:

Microsoft Learn 模块: