使用异地冗余设计高度可用的应用程序

基于云的基础结构(如 Azure 存储)提供用于托管数据和应用程序的高度可用且持久的平台。 基于云的应用程序的开发人员必须仔细考虑如何利用此平台为其用户最大限度地扩大这些优势。 Azure 存储提供异地冗余选项,即便在发生区域性服务中断时也可确保高可用性。 为异地冗余复制配置的存储帐户将以同步方式复制到主要区域,并以异步方式复制到数百英里以外的次要区域。

Azure 存储提供两种选项用于异地冗余复制:异地冗余存储 (GRS)异地区域冗余存储 (GZRS)。 若要使用 Azure 存储异地冗余选项,请确保存储帐户配置为读取访问异地冗余存储 (RA-GRS) 或读取访问异地区域冗余存储 (RA-GZRS)。 如果没有,可以详细了解如何更改存储帐户复制类型

本文介绍如何设计一个能继续正常运行的应用程序,尽管容量有限,即使主要区域发生重大中断也是如此。 如果主要区域不可用,则应用程序可以无缝切换以针对次要区域执行读取操作,直到主要区域再次响应。

应用程序设计注意事项

可以将应用程序设计为在出现问题无法从主要区域读取时,通过从次要区域读取来处理暂时性故障或重大中断。 当主要区域重新变为可用时,应用程序可恢复为从主要区域读取。

在使用 RA-GRS 或 RA-GZRS 设计具有可用性和复原能力的应用程序时,请牢记以下关键注意事项:

  • 主要区域中存储的数据的只读副本在次要区域中异步复制。 此异步复制意味着次要区域中的只读副本最终与主要区域中的数据一致。 存储服务确定次要区域的位置。

  • 可以使用 Azure 存储客户端库针对主要区域终结点执行读取和更新请求。 如果主要区域不可用,可以自动将读取请求重定向到次要区域。 如果需要,还可以将应用配置为直接将读取请求发送到次要区域,即使主要区域可用也是如此。

  • 如果主要区域变得不可用,则可发起帐户故障转移。 故障转移到次要区域时,指向主要区域的 DNS 条目更改为指向次要区域。 故障转移完成后,GRS 和 RA-GRS 帐户的写入访问会恢复。 有关详细信息,请参阅灾难恢复和存储帐户故障转移

处理最终一致的数据

此建议的解决方案假定可以向调用方应用程序返回可能过时的数据。 由于次要区域中的数据是最终一致的,因此在对次要区域的更新完成复制前,主要区域可能会变为不可访问。

例如,假设客户提交更新成功,但在更新传播到次要区域前主要区域发生故障。 当客户要求读回数据时,将收到来自次要区域的过时数据而非更新后的数据。 设计应用程序时,必须确定此行为是否可接受。 如果是,还需要考虑如何通知用户。

本文稍后将详细介绍如何处理最终一致的数据以及如何检查上次同步时间属性,以评估主要区域和次要区域中数据之间的差异。

单独或整体处理服务

尽管可能性不大,但当其他服务仍然完全可用时,某一个服务可能不可用(blob、队列、表 或文件)。 可单独为每个服务处理重试操作,或者以一般方式为所有存储服务统一处理重试操作。

例如,如果在应用程序中使用队列和 blob,可以使用单独的代码处理每个服务的重试错误。 这样,blob 服务错误只会影响处理 blob 的应用程序部分,使队列继续正常运行。 但是,如果决定一起处理所有存储服务重试,若任一服务返回重试错误,则对 blob 和队列服务的请求将受到影响。

从根本上讲,决定取决于应用程序的复杂程度。 你可能更喜欢按服务处理失败,以限制重试的影响。 或者,当检测到主要区域中的任何存储服务存在问题时,可以将对所有存储服务的读取请求重定向到次要区域。

在只读模式下运行应用程序

若要有效地应对主要区域中发生的服务中断,你的应用程序必须能够处理失败的读取请求和失败的更新请求。 如果主要区域发生故障,读取请求可重定向到次要区域。 但是,无法重定向更新请求,因为次要区域中的复制数据是只读的。 因此,需要将应用程序设计为能在只读模式下运行。

例如,可以设置一个标志,在向 Azure 存储提交任何更新请求前需检查此标志。 当更新请求成功时,可以跳过请求,并向用户返回适当的响应。 在问题解决前,甚至可能选择禁用某些功能,并通知用户这些功能是暂时不可用。

如果决定分别处理每个服务的错误,则还需要处理在只读模式下按服务运行应用程序的能力。 例如,可以为每个服务设置只读标志。 然后,可以根据需要启用或禁用代码中的标志。

能够在只读模式下运行应用程序让你可以在主要应用程序升级期间确保有限的功能。 可以触发应用程序在只读模式下运行并指向备用数据中心,确保在升级时没有任何用户访问主要区域中的数据。

以只读模式运行时处理更新

以只读模式运行时,可使用多种方法处理更新请求。 本部分重点介绍一些要考虑的常规模式。

  • 可以响应用户并通知他们当前尚未处理更新请求。 例如,联系人管理系统可使用户访问联系信息但不能进行更新。

  • 可将更新放入另一区域进行排队。 在这种情况下,可将挂起的更新请求写入不同区域中的队列,并在主数据中心再次联机后处理这些请求。 在此方案中,应让用户知道更新请求已排队等待稍后处理。

  • 可将更新写入其他区域中的存储帐户。 在主要区域重新联机后,可以将这些更新合并到主要数据中,具体取决于数据的结构。 例如,如果使用名称中的日期/时间戳创建单独的文件,可将这些文件复制回主要区域。 此解决方案可以应用于日志记录和 IoT 数据等工作负载。

处理重试

与云中运行的服务进行通信的应用程序必须对可能发生的计划外事件和故障敏感。 这些故障可以是暂时性的或永久性的,范围涵盖从暂时的连接丢失到自然灾害造成的重大中断。 务必设计具有适当重试处理的云应用程序,以最大程度地提高可用性并提高应用程序的整体稳定性。

读取请求

如果主要区域不可用,可将读取请求重定向到辅助存储。 如先前所述,应用程序必须能够读取过时数据。 Azure 存储客户端库提供了用于处理重试和将读取请求重定向到次要区域的选项。

在此示例中,Blob 存储的重试处理在 BlobClientOptions 类中配置,并将应用于我们使用这些配置选项创建的 BlobServiceClient 对象。 此配置采取先主后辅的方法,从主要区域读取请求重试后将其重定向到次要区域。 当主要区域中的故障是符合预期的临时故障时,此方法是最佳方法。

string accountName = "<YOURSTORAGEACCOUNTNAME>";
Uri primaryAccountUri = new Uri($"https://{accountName}.blob.core.chinacloudapi.cn/");
Uri secondaryAccountUri = new Uri($"https://{accountName}-secondary.blob.core.chinacloudapi.cn/");

// Provide the client configuration options for connecting to Azure Blob storage
BlobClientOptions blobClientOptions = new BlobClientOptions()
{
    Retry = {
        // The delay between retry attempts for a fixed approach or the delay
        // on which to base calculations for a backoff-based approach
        Delay = TimeSpan.FromSeconds(2),

        // The maximum number of retry attempts before giving up
        MaxRetries = 5,

        // The approach to use for calculating retry delays
        Mode = RetryMode.Exponential,

        // The maximum permissible delay between retry attempts
        MaxDelay = TimeSpan.FromSeconds(10)
    },

    // If the GeoRedundantSecondaryUri property is set, the secondary Uri will be used for 
    // GET or HEAD requests during retries.
    // If the status of the response from the secondary Uri is a 404, then subsequent retries
    // for the request will not use the secondary Uri again, as this indicates that the resource 
    // may not have propagated there yet.
    // Otherwise, subsequent retries will alternate back and forth between primary and secondary Uri.
    GeoRedundantSecondaryUri = secondaryAccountUri
};

// Create a BlobServiceClient object using the configuration options above
BlobServiceClient blobServiceClient = new BlobServiceClient(primaryAccountUri, new DefaultAzureCredential(), blobClientOptions);

如果确定主要区域可能长时间不可用,则可以将所有读取请求配置为指向次要区域。 此配置采取仅辅助方法。 如前所述,你需要采用某种策略来处理在此期间的更新请求,以及通过某种方式通知用户只处理读取请求。 在此示例中,我们将创建一个新实例 BlobServiceClient,该实例使用次要区域终结点。

string accountName = "<YOURSTORAGEACCOUNTNAME>";
Uri primaryAccountUri = new Uri($"https://{accountName}.blob.core.chinacloudapi.cn/");
Uri secondaryAccountUri = new Uri($"https://{accountName}-secondary.blob.core.chinacloudapi.cn/");

// Create a BlobServiceClient object pointed at the secondary Uri
// Use blobServiceClientSecondary only when issuing read requests, as secondary storage is read-only
BlobServiceClient blobServiceClientSecondary = new BlobServiceClient(secondaryAccountUri, new DefaultAzureCredential(), blobClientOptions);

知道何时切换到只读模式和仅辅助请求是称为断路器模式的体系结构设计模式的一部分,将在后面部分进行讨论。

更新请求

更新请求不能重定向到辅助存储,因为辅助存储是只读的。 如前所述,应用程序需要在主要区域不可用时能够处理更新请求

断路器模式还可应用于更新请求。 若要处理更新请求错误,可以在代码中设置阈值,例如连续 10 个失败,并跟踪对主要区域的请求失败数。 满足阈值后,可以将应用程序切换到只读模式,以便不再发出对主要区域的更新请求。

如何实现断路器模式

处理恢复时间不定的故障是称为断路器模式的体系结构设计模式的一部分。 此模式的正确实现可以防止应用程序反复尝试执行可能失败的操作,从而改善应用程序稳定性和复原能力。

断路器模式的一个方面是确定什么时候主终结点会存在持续问题。 若要进行确定,可以监视客户端遇到重试错误的频率。 由于每种情况都不同,因此需要确定切换到辅助终结点并在只读模式下运行应用程序时使用的相应阈值。

例如,可决定在主要区域中存在连续 10 次失败时执行转换。 可以通过在代码中保留失败计数来跟踪此问题。 如果在达到阈值之前成功,请将计数设置为零。 如果计数达到阈值,则切换应用程序以使用次要区域进行读取请求。

可以决定在应用程序中实现自定义监视组件,以此作为替代方法。 此组件可以通过简单的读取请求(例如读取小型 blob)来持续对主存储终结点执行 ping 操作以确定其运行状况。 此方法会占用一些资源,但占用量不大。 发现达到阈值的问题时,则切换到仅辅助读取请求和只读模式。 对于此方案,当再次对主存储终结点成功执行 ping 操作时,可切换回主要区域并继续允许更新。

用于确定何时切换的错误阈值根据应用程序中的不同服务而有所差异,因此应考虑将它们设置为可配置参数。

另一个注意事项是如何处理应用程序的多个实例,以及在每个实例中检测到可重试错误时应如何操作。 例如,可以运行 20 个加载相同应用程序的 VM。 是否分别处理每个实例? 如果一个实例开始出现问题,是否要限制为仅对该实例做出响应? 或者,当一个实例出现问题时,是否希望所有实例以相同的方式做出响应? 单独处理实例比尝试协调跨实例的响应简单得多,但具体方法取决于应用程序的体系结构。

处理最终一致的数据

异地冗余存储的工作方式是将事务从主要区域复制到次要区域。 此复制过程可保证次要区域中的数据最终保持一致。 这意味着主要区域中的所有事务最终都会出现在次要区域中,但是在它们出现之前可能会有延迟。 也不能保证事务将按最初在主要区域中应用的顺序出现在次要区域中。 如果事务未按顺序到达次要区域,则在服务生效前, 可以 认为次要区域中的数据处于不一致状态。

以下 Azure 表存储的示例显示了更新员工详细信息以使其成为“管理员角色”的成员时可能发生的情况。 此示例要求更新员工条目实体和管理员角色实体的管理员总数。 请注意更新如何以无序方式在次要区域中应用。

时间 事务 复制 上次同步时间 结果
T0 事务 A:
在主要区域中插入
员工实体
事务 A 已插入到主要区域,
但尚未复制。
T1 事务 A
已复制到
次要区域。
T1 事务 A 已复制到次要区域。
已更新上次同步时间。
T2 事务 B:
更新
主要区域中的
员工实体
T1 事务 B 已写入主要区域,
但尚未复制。
T3 事务 C:
更新
主要区域中的
管理员
角色实体
T1 事务 C 已写入主要区域,
但尚未复制。
T4 事务 C
已复制到
次要区域。
T1 事务 C 已复制到次要区域。
上次同步时间未更新,因为
事务 B 尚未复制。
T5 从次要区域
读取实体
T1 得到员工实体的过时值,
因为事务 B
尚未复制。 得到管理员角色实体
的新值,因为 C
已复制。 上次同步时间仍未更新,
因为事务 B
尚未复制。 可以判断出
管理员角色实体不一致,
因为实体日期/时间晚于
上次同步时间。
T6 事务 B
已复制到
次要区域。
T6 T6 - 通过 C 的所有事务都已
复制,上次同步时间
已更新。

在此示例中,假设客户端切换为在 T5 从次要区域读取。 它此时能够成功读取管理员角色实体,但该实体包含的管理员数量值与次要区域中此时标记的员工数量不一致。 客户端需显示此值,并且具有信息不一致的风险。 或者,客户端可能会尝试确定 管理员角色 可能是不一致的状态,因为更新是无序进行的,并随后告知用户这一事实。

若要确定存储帐户是否具有潜在的不一致数据,客户端可以检查上次同步时间属性的值。 上次同步时间可让你了解次要区域中的数据上一次一致的时间,以及服务在该时间点前应用所有事务的时间。 在上述示例中,服务在次要区域中插入员工实体后,上次同步时间设置为 T1。 当服务更新次要区域中的员工实体前,它仍然保持为 T1,之后则设置为 T6。 如果客户端在其读取 T5处的实体时检索上次同步时间,它会将其与实体上的时间戳进行对比。 如果实体上的时间戳晚于上次同步时间,则实体可能处于不一致状态,你可以采取适当操作。 使用此字段要求了解到主要区域上次更新的时间。

若要了解如何检查上次同步时间,请参阅检查存储帐户的“上次同步时间”属性

测试

当应用程序遇到可重试错误时,请务必测试应用程序的行为是否与预期一致。 例如,需要测试应用程序在检测到问题时会切换到次要区域,并在主要区域可用时再次切换回去。 若要正确测试此行为,需以某种方式模拟可重试错误并控制其出现的频率。

可以选择使用 Fiddler 在脚本中截获和修改 HTTP 响应。 此脚本可以标识来自主终结点的响应,并将 HTTP 状态代码更改为存储客户端库识别为可重试错误的代码。 此代码片段显示 Fiddler 脚本的简单示例,此脚本截获响应以读取对 employeedata 表的读取请求,并返回 502 状态:

static function OnBeforeResponse(oSession: Session) {
    ...
    if ((oSession.hostname == "\[YOURSTORAGEACCOUNTNAME\].table.core.chinacloudapi.cn")
      && (oSession.PathAndQuery.StartsWith("/employeedata?$filter"))) {
        oSession.responseCode = 502;
    }
}

可使用此示例对范围更广的请求进行截获,并更改其中一些请求的 responseCode 以更好地模拟真实方案。 有关自定义 Fiddler 脚本的详细信息,请参阅 Fiddler 文档中的 Modifying a Request or Response (修改请求或响应)。

如果已设置可配置的阈值,用于将应用程序切换到只读模式,则可轻松使用非生产事务量测试行为。


后续步骤

有关如何在主终结点和辅助终结点之间来回切换的完整示例,请参阅 Azure 示例 - 将断路器模式与 RA-GRS 存储配合使用