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

领导选拔模式

Azure Blob 存储

通过选拔一个实例作为领导来负责管理其他实例,对分布式应用程序中协作性实例集合所执行的操作进行协调。 这有助于确保各个实例互不冲突,不会导致争用共享资源或者意外地干扰其他实例正在执行的工作。

上下文和问题

典型的云应用程序具有许多以协调方式执行的任务。 这些任务可能全部是运行相同代码并需要访问相同资源的实例,也可能并行协作来执行复杂计算的各个部分。

任务实例可能大多数时间单独运行,但也可能需要协调每个实例的操作,以确保它们不会冲突、不会导致争用共享资源或者意外地干扰其他任务实例正在执行的工作。

例如:

  • 在实现水平缩放的基于云的系统中,同一任务的多个实例可以同时运行,并且每个实例为一个不同的用户提供服务。 如果这些实例向共享资源进行写入,则需要协调它们的操作以防止各个实例覆盖其他实例所做的更改。
  • 如果各个任务并行执行复杂计算的各个元素,则需要在它们全部完成时对结果进行聚合。

任务实例全部是对等的,因此没有可以充当协调器或聚合器的天生领导。

解决方案

应当选拨单个任务实例来充当领导,并且此实例应当对其他下属任务实例的操作进行协调。 如果所有任务实例都运行相同的代码,则它们每个都能够充当领导。 因此,必须谨慎管理选举过程,以防止两个或多个实例同时接管领导者职位。

系统必须提供用于选拨领导的可靠机制。 此方法必须能够应对网络中断或进程失败等事件。 在许多解决方案中,下属任务实例通过某种类型的检测信号方法或通过轮询来监视领导。 如果指定的领导意外终止,或者网络故障导致领导不可供下属任务实例使用,则它们需要选拨一个新领导。

在分布式环境中,有多种策略可在一系列任务中选拨领导,包括:

  • 争夺一个共享的分布式互斥体。 第一个获得该互斥体的任务实例成为领导。 但是,系统必须确保,当领导终止或者与系统的其余部分断开了连接时,必须释放该互斥体以允许其他任务实例成为领导。 以下的示例演示了这一策略。
  • 实施其中一种常见的领导者选举算法,如欺负算法Raft 共识算法环算法。 这些算法假设选举中的每个候选者都具有唯一的 ID,并且它可以可靠地与其他候选者进行通信。

问题和注意事项

在决定如何实现此模式时,请考虑以下几点:

  • 选拨领导的流程在发生暂时性和永久性故障后应该能够复原。
  • 必须能够检测领导何时发生故障或变得不可用(例如由于通信故障)。 需要多快的检测速度取决于系统。 某些系统也许能够在没有领导的情况下短时行使职责,在这期间,暂时性错误也许能够得到修复。 在其他情况下,可能需要立即检测到领导故障并触发新的选举。
  • 在实现水平自动缩放的系统中,如果系统收缩并关闭一些计算资源,则领导可能会终止。
  • 使用共享的分布式互斥体将依赖于提供该互斥体的外部服务。 该服务成为单一故障点。 如果它由于任何原因而变得不可用,则系统将无法选拨领导。
  • 使用单个专用进程作为领导是简单明了的方法。 但是,如果该进程发生故障,则在它重新启动时可能会导致明显的延迟。 如果其他进程在等待领导来协调某个操作,则导致的延迟可能会影响其他进程的性能和响应时间。
  • 手动实现领导选拨算法之一可以在调整和优化代码方面提供最大的灵活性。
  • 请避免使领导成为系统中的瓶颈。 领导的目的是协调下属任务的工作,并且不是非得亲自参与这项工作(如果该任务没有被选为领导,应该可以参与这项工作)。

何时使用此模式

当分布式应用程序(例如云托管解决方案)中的任务需要仔细协调并且没有天生的领导时,请使用此模式。

在下列情况下,此模式可能不适用:

  • 有一个天生的领导或有一个能够始终充当领导的专用进程。 例如,可以实现一个对任务实例进行协调的单一实例进程。 如果此进程发生故障或变得不正常,则系统可以将其关闭并重新启动它。
  • 任务之间的协调可以使用更轻量的方法来实现。 例如,如果多个任务实例只是需要实现对共享资源的协调访问,则更好的解决方案是使用乐观或悲观锁定来控制访问。
  • 第三方解决方案(如 Apache Zookeeper)可能是一种更高效的解决方案。

工作负荷设计

架构师应评估如何在其工作负载的设计中使用“领导选拔模式”,以解决 Azure Well-Architected Framework 支柱中涵盖的目标和原则。 例如:

支柱 此模式如何支持支柱目标
可靠性设计决策有助于工作负荷在发生故障后复原,并确保它在发生故障后恢复到正常运行状态。 这种模式通过可靠地重定向工作来减轻节点故障的影响。 当领导者出现故障时,它还通过共识算法实现故障转移。

- RE:05 冗余
- RE:07 自我修复

与任何设计决策一样,请考虑对可能采用此模式引入的其他支柱的目标进行权衡。

示例

GitHub 上的领导选拔示例显示了如何使用 Azure 存储 Blob 上的租约来提供实现共享、分布式互斥的机制。 这个互斥可以用于在一组可用的工作实例中选择一个领导者。 获得租约的第一个实例被选为领导者,并在其释放租约或无法续订租约之前一直是领导者。 其他工作实例可以继续监视 Blob 租约,以防领导者不再可用。

Blob 租约是一个针对 blob 的排他写入锁。 单个 blob 在任一时刻只能是一个租约的主题。 工作实例可以请求对指定 Blob 的租约;如果没有其他工作实例对同一 Blob 持有租约,它将被授予租约。 否则,请求将引发异常。

要避免出现故障的领导实例无限期保留租约,请指定租约的生存期。 当生存期过期时,租约将变得可用。 然而,当实例持有租约时,它可以请求续订租约,并将在接下来的一段时间内获得租约。 如果领导实例想要保留租约,则可以不断重复此过程。 有关如何租用 blob 的详细信息,请参阅 Lease Blob (REST API)(租用 Blob (REST API))。

下面 C# 示例中的 BlobDistributedMutex 类包含 RunTaskWhenMutexAcquired 方法,该方法使工作实例能够尝试获取指定 blob 的租约。 当创建 BlobDistributedMutex 对象时(此对象是示例代码中包括的一个简单结构),会将该 blob 的详细信息(名称、容器和存储帐户)传递给 BlobSettings 对象中的构造函数。 构造函数还接受一个 Task,引用工作实例在成功获取 blob 上的租约并被选为领导者时应该运行的代码。 请注意,名为 BlobLeaseManager 的一个单独的帮助程序类中实现了用于处理有关获得租约的低层详细信息的代码。

public class BlobDistributedMutex
{
  ...
  private readonly BlobSettings blobSettings;
  private readonly Func<CancellationToken, Task> taskToRunWhenLeaseAcquired;
  ...

  public BlobDistributedMutex(BlobSettings blobSettings,
           Func<CancellationToken, Task> taskToRunWhenLeaseAcquired, ... )
  {
    this.blobSettings = blobSettings;
    this.taskToRunWhenLeaseAcquired = taskToRunWhenLeaseAcquired;
    ...
  }

  public async Task RunTaskWhenMutexAcquired(CancellationToken token)
  {
    var leaseManager = new BlobLeaseManager(blobSettings);
    await this.RunTaskWhenBlobLeaseAcquired(leaseManager, token);
  }
  ...

上面的代码示例中的 RunTaskWhenMutexAcquired 方法调用下面的代码示例中显示的 RunTaskWhenBlobLeaseAcquired 方法来实际获得租约。 RunTaskWhenBlobLeaseAcquired 方法以异步方式运行。 如果成功获取租约,则工作实例已被选为领导者。 taskToRunWhenLeaseAcquired 委托的目的是执行协调其他工作实例的工作。 如果未获取租约,则另一个工作实例已被选为领导者,而当前工作实例仍为从属实例。 请注意,TryAcquireLeaseOrWait 方法是一个帮助程序方法,它使用 BlobLeaseManager 对象来获得租约。

  private async Task RunTaskWhenBlobLeaseAcquired(
    BlobLeaseManager leaseManager, CancellationToken token)
  {
    while (!token.IsCancellationRequested)
    {
      // Try to acquire the blob lease.
      // Otherwise wait for a short time before trying again.
      string? leaseId = await this.TryAcquireLeaseOrWait(leaseManager, token);

      if (!string.IsNullOrEmpty(leaseId))
      {
        // Create a new linked cancellation token source so that if either the
        // original token is canceled or the lease can't be renewed, the
        // leader task can be canceled.
        using (var leaseCts =
          CancellationTokenSource.CreateLinkedTokenSource(new[] { token }))
        {
          // Run the leader task.
          var leaderTask = this.taskToRunWhenLeaseAcquired.Invoke(leaseCts.Token);
          ...
        }
      }
    }
    ...
  }

领导启动的任务也是以异步方式运行的。 当此任务在运行时,下面的代码示例中显示的 RunTaskWhenBlobLeaseAcquired 方法会定期尝试续订租约。 这有助于确保工作实例仍然是领导者。 在示例解决方案中,续订请求之间的延迟小于为租约持续时间指定的时间,以防止将另一个工作实例选为领导者。 如果由于任何原因续订失败,则取消特定于领导者的任务。

如果租约无法续订或任务被取消(可能是由于工作实例关闭),则租约将被释放。 此时,可以将此或另一个工作实例选为领导者。 下面的代码摘录显示了过程的这一部分。

  private async Task RunTaskWhenBlobLeaseAcquired(
    BlobLeaseManager leaseManager, CancellationToken token)
  {
    while (...)
    {
      ...
      if (...)
      {
        ...
        using (var leaseCts = ...)
        {
          ...
          // Keep renewing the lease in regular intervals.
          // If the lease can't be renewed, then the task completes.
          var renewLeaseTask =
            this.KeepRenewingLease(leaseManager, leaseId, leaseCts.Token);

          // When any task completes (either the leader task itself or when it
          // couldn't renew the lease) then cancel the other task.
          await CancelAllWhenAnyCompletes(leaderTask, renewLeaseTask, leaseCts);
        }
      }
    }
  }
  ...
}

KeepRenewingLease 方法是一个帮助程序方法,它使用 BlobLeaseManager 对象来续订租约。 CancelAllWhenAnyCompletes 方法取消作为前两个参数指定的任务。 下图展示了使用 BlobDistributedMutex 类来选拨领导并运行对操作进行协调的任务。

图 1 展示了 BlobDistributedMutex 类的功能

以下代码示例显示如何在工作实例中使用 BlobDistributedMutex 类。 此代码通过租约的容器 Azure Blob 存储中名为 MyLeaderCoordinatorTask 的 blob 获取租约,并指定如果工作实例被选为领导者,则应运行 MyLeaderCoordinatorTask 方法中定义的代码。

// Create a BlobSettings object with the connection string or managed identity and the name of the blob to use for the lease
BlobSettings blobSettings = new BlobSettings(storageConnStr, "leases", "MyLeaderCoordinatorTask");

// Create a new BlobDistributedMutex object with the BlobSettings object and a task to run when the lease is acquired
var distributedMutex = new BlobDistributedMutex(
    blobSettings, MyLeaderCoordinatorTask);

// Wait for completion of the DistributedMutex and the UI task before exiting
await distributedMutex.RunTaskWhenMutexAcquired(cancellationToken);

...

// Method that runs if the worker instance is elected the leader
private static async Task MyLeaderCoordinatorTask(CancellationToken token)
{
  ...
}

请注意关于示例解决方案的以下要点:

  • Blob 是潜在的单一故障点。 如果 blob 服务不可用或无法访问,则领导者将无法续订租约,并且其他工作实例也无法获取租约。 在这种情况下,任何工作实例都无法充当领导者。 不过,blob 服务设计为具有复原能力,因此,blob 服务完全失败的情况几乎不可能出现。
  • 如果领导者正在执行的任务暂停,领导者可能会继续续订租约,从而阻止任何其他工作实例获取租约,并接管领导者职位以协调任务。 在现实世界中,应当频繁检查领导的运行状况。
  • 选举流程具有不确定性。 不能对哪个工作实例将获得 blob 租约并成为领导者做出任何假设。
  • 用作 blob 租约的目标的 blob 不应当用于任何其他用途。 如果工作实例试图将数据存储在此 blob 中,则除非该工作实例是领导者并持有 blob 租约,否则无法访问此数据。

后续步骤

实现此模式时,以下指南可能也比较有用: