基于云的基础结构(如 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 次连续故障,则可以决定执行切换。 可以通过在代码中保留失败计数来跟踪此问题。 如果在达到阈值之前成功,请将计数重新设置为零。 如果计数达到阈值,则切换应用程序以将次要区域用于读取请求。
作为替代方法,可以决定在应用程序中实现自定义监视组件。 此组件可以通过简单的读取请求(例如读取小数据块)持续检测主存储终结点的运行状况,以评估其健康状态。 此方法将占用一些资源,但不会占用大量资源。 发现达到阈值的问题时,则切换到仅辅助读取请求和只读模式。 对于这种情况,当 ping 主存储终结点再次成功时,可以切换回主要区域并继续允许更新。
用于确定何时进行切换的错误阈值可能因不同服务而有所变化,因此你应考虑将它们设置为可配置的参数。
另一个考虑因素是如何处理应用程序的多个实例,以及如何检测每个实例中的可重试错误。 例如,你可能有 20 个 VM 在加载相同的应用程序的情况下运行。 是否单独处理每个实例? 如果一个实例开始出现问题,是否仅将响应限制为该一个实例? 或者,当一个实例出现问题时,是否希望所有实例都以相同的方式进行响应? 单独处理实例比尝试跨实例协调响应要简单得多,但你的方法将取决于应用程序的体系结构。
异地冗余存储的工作原理是将事务从主要区域复制到次要区域。 复制过程保证次要区域中的数据最终保持一致。 这意味着主要区域中的所有事务最终都会出现在次要区域中,但是在它们出现之前可能会有延迟。 也不保证事务将以最初在主要区域中应用的顺序到达次要区域。 如果你的交易无序到达次要区域,你可能会认为次要区域中的数据处于不一致状态,直到服务赶上。
Azure 表存储的以下示例显示了更新员工详细信息以使其成为 管理员角色成员时可能发生的情况。 为了本示例,你需要更新 员工 实体,并且更新一个包含管理员总数的 管理员角色 实体。 请注意如何在次要区域中更新被以无序方式应用。
时间 | 交易 | 复制 | 上次同步时间 | 结果 |
---|---|---|---|---|
T0 | 交易 A: 插入员工 主实体中的实体 |
插入到主节点的事务 A、 尚未复制。 |
||
T1 | 交易 A 复制到 次要的 |
T1 | 事务 A 复制到辅助数据库。 上次同步时间已更新。 |
|
T2 | 交易 B: 更新 员工实体 在主要的场合中 |
T1 | 写入主库的事务 B, 尚未复制。 |
|
T3 | 交易 C: 更新 管理员 中的角色实体 主要 |
T1 | 写入主节点的事务 C, 尚未复制。 |
|
T4 | 交易 C 复制到 次要的 |
T1 | 事务 C 已复制到备库。 LastSyncTime 未更新,因为 事务 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 文档中 修改请求或响应 。
如果您已设置用于将应用程序切换为只读状态的可配置阈值,那么在测试环境中使用非生产事务量测试其行为会更容易。
有关演示如何在主终结点和辅助终结点之间来回切换的完整示例,请参阅 Azure 示例 - 将断路器模式与 RA-GRS 存储配合使用。