Orleans 中的群集管理

Orleans 通过内置成员身份协议提供群集管理,我们有时称为 群集成员身份。 该协议的目标是让所有接收器(Orleans 服务器)就一组当前存活接收器达成一致,检测失败的接收器,并允许新接收器加入群集。

该协议依赖于外部服务来提供 IMembershipTable 的抽象。 IMembershipTable 是一张平坦耐用的桌子,我们将其用于两个用途。 首先,它用作会合点,供接收器相互查找彼此,以及供 Orleans 客户查找接收器。 其次,它用于存储当前成员身份视图(存活 silo 列表)并帮助协调成员身份视图的协议。

我们目前有 6 种 IMembershipTable实现:基于 Azure 表存储Azure Cosmos DB,ADO.NET (PostgreSQL, MySQL/MariaDB、SQL Server、Oracle)、Apache ZooKeeperConsul IOAWS DynamoDBMongoDBRedisApache Cassandra以及用于开发的内存中实现。

除了 IMembershipTable 以外,每个 silo 都参与完全分布式的对等成员身份协议,该协议检测失败的 silo 并就一组存活 silo 达成一致。 我们在下面介绍了Orleans 的成员身份协议的内部实现。

成员身份协议

  1. 启动后,每个孤岛都会使用 IMembershipTable 的实现将自身条目添加到众所周知的共享表中。 silo 标识 (ip:port:epoch) 和服务部署 ID(群集 ID)的组合用作表中的唯一键。 时期只是此接收器启动时的时间(刻度),因此可以保证 ip:port:epoch 在给定的 Orleans 部署中唯一。

  2. silo 直接通过应用程序探测(“是否存活”heartbeats)相互监视。 探测作为直接消息通过与 silo 通信的相同 TCP 套接字在 silo 之间发送。 这样,探测就与实际的网络问题和服务器运行状况完全关联。 每个 silo 都会对一组可配置的其他 silo 进行探测操作。 silo 通过计算其他 silo 的标识中的一致哈希来选择要探测的对象,从而构成所有标识的虚拟环,并在该环中拾取 X 个后继 silo(这是一种众所周知的分布式技术,称为一致哈希,广泛应用于许多分布式哈希表,例如 Chord DHT)。

  3. 如果 silo S 未收到来自受监视服务器 P 的 Y 次探测回复,则它会通过将带时间戳的怀疑写入 IMembershipTable 中 P 的行来表示怀疑。

  4. 如果 P 在 K 秒内有超过 Z 次的怀疑,则 S 会将 P 已消亡信息写入 P 的行,并将当前成员身份表的快照发送给所有其他 silo。 Silo 会定期刷新表,因此快照是一种优化方法,可以减少所有 silo 获取新的成员身份视图所需的时间。

  5. 更多详细信息:

    1. 怀疑将写入 IMembershipTable 中与 P 对应的行的特殊列中。当 S 怀疑 P 时,它会写入:“在时间点 TTT,S 怀疑了 P”。

    2. 一个怀疑不足以声明 P 消亡。 在可配置的时间窗口 T(通常为 3 分钟)内,需要有来自不同 silo 的 Z 次怀疑才能将 P 声明为已消亡。 怀疑是使用 IMembershipTable 提供的乐观并发控制写入的。

    3. 怀疑方 silo S 读取 P 的行。

    4. 如果 S 是最后一个怀疑方(在 T 周期内已有 Z-1 个怀疑方,在怀疑列中写入),则 S 会决定将 P 声明为消亡。 在这种情况下,S 会将自身添加到怀疑方列表中,并在 P 的状态列中写入“P 已消亡”。

    5. 否则,如果 S 不是最后一个怀疑方,则 S 只会将自身添加到怀疑方的列中。

    6. 在任一情况下,写回都使用读取的版本号或 ETag,因此对此行的更新是序列化的。 如果由于版本/ETag 不匹配而导致写入失败,则 S 会重试(再次读取并尝试写入,除非 P 已标记为消亡)。

    7. 概括而言,这种“读取、本地修改、写回”序列是一个事务。 但是,我们不一定使用存储事务来执行此操作。 “事务”代码在服务器上本地执行,我们使用 IMembershipTable 提供的乐观并发来确保隔离性和原子性。

  6. 每个 silo 定期读取整个成员身份表来进行部署。 这样,silo 便知道有新的 silo 加入并有其他 silo 被声明为消亡。

  7. 快照广播:为了减少定期表读取的频率,每当 silo 写入表(怀疑,新加入等)时,它会向所有其他 silo 发送当前表状态的快照。 由于成员身份表是一致的,并且版本是单调的,因此每次更新都会生成一个可以安全共享的唯一版本快照。 这样就可以立即传播成员身份更改,而无需等待定期读取周期。 在快照分发失败的情况下,定期读取仍被作为备份机制进行维护。

  8. 有序成员身份视图:成员身份协议可确保全局完全排序所有成员身份配置。 此排序提供两个关键优势:

    1. 连接性保证:当新的仓加入集群时,它必须验证与其他每个活动仓的双向连接。 如果任何现有 silo 没有响应(可能表示网络连接问题),则不允许新 silo 加入。 这可确保在启动时群集中的所有孤岛之间完全连接。 有关 IAmAlive 的注意事项,请参见下文,以了解在灾难恢复情况下的例外。

    2. 一致的目录更新:更高级别的协议(例如分布式 grain 目录)依赖于具有一致、单调成员身份视图的所有 silo。 这样就可以更智能地解析重复的粒度激活。 有关详细信息,请参阅 粒度目录 文档。

    实现的详细信息

    1. IMembershipTable 需要原子更新才能保证更改的全局总顺序:

      • 实现必须以原子方式更新表条目(孤岛列表)和版本号
      • 这可以通过使用数据库事务(如在 SQL Server 中)或使用 ETag 进行原子比较和交换操作(如在 Azure 表存储中)实现。
      • 特定机制取决于基础存储系统的功能
    2. 表中的特殊会员版本行用于跟踪更改:

      • 对表的每次写入(怀疑、消亡声明、加入操作)都会递增此版本号
      • 所有写入都通过此行使用原子更新进行序列化
      • 单调递增的版本确保所有成员身份更改有一个总排序
    3. 当接收器 S 更新接收器 P 的状态时:

      • S 首先读取最新表状态
      • 在单个原子操作中,它会更新 P 的行并递增版本号
      • 如果原子更新失败(例如,由于并发修改),则会使用指数退避重试操作

    可伸缩性注意事项

    由于争用增加,通过版本行序列化所有写入可能会影响可伸缩性。 该协议已在生产环境中验证过,可支持高达 200 个存储单元,但在超过一千个存储单元时可能会遇到挑战。 对于非常大的部署,即使成员身份更新成为瓶颈,Orleans(消息传递、粒度目录、托管)的其他部分仍可缩放。

  9. 默认配置:在 Azure 的生产使用期间,已手动调整默认配置。 默认情况下:每个筒仓由另外三个筒仓监视,两个怀疑报告足够宣告一个筒仓失效,怀疑报告仅限于过去三分钟(否则将被视为过时)。 探测信号每 10 秒发送一次,如果错过三个信号,就可以怀疑存在隔离问题。

  10. 自我监视:故障检测器整合了 HashiCorp 的 Lifeguard 研究(论文讲座博客)中的想法,以在灾难性事件期间,当群集中的很大一部分经历部分失效时,提高群集稳定性。 LocalSiloHealthMonitor 组件使用多个启发式方法对每个 silo 的运行状况进行评分:

    • 成员身份表中的活动状态
    • 没有来自其他 silo 的怀疑
    • 最近成功的探测响应
    • 最近收到的探测请求
    • 线程池响应能力(在 1 秒内执行的工作项)
    • 计时器准确性(在计划的 3 秒内触发)

    silo 的运行状况分数会影响其探测超时:与运行正常的 silo(评分 0)相比,运行不正常的 silo(评分 1-8)的超时时间增加。 这有两个好处:

    • 在网络或系统承受压力时,为探测提供更多成功时间
    • 这使得运行不正常的 silo 在错误地投票淘汰运行正常的 silo 之前,更有可能被投票淘汰

    这在线程池资源不足等方案中尤其有价值;在这种情况下,慢速节点可能会因为无法足够快地处理响应而错误地怀疑正常节点。

  11. 间接探测:另一个 Lifeguard 启发的功能,通过减少运行不正常或分区 silo 错误地声明正常 silo 消亡的可能性来提高故障检测准确性。 当一个监视 silo 在投票宣布其消亡之前,对目标 silo 还有两次探测尝试时,它会采用间接探测:

    • 监视筒仓随机选择另一个筒仓作为中介,并要求它探测目标
    • 中介尝试联系目标 silo
    • 如果目标在超时期限内未能响应,则中介会发送负面确认信息。
    • 如果监视 silo 收到中介的否定确认,而中介通过上述自我监视声明自己正常,则监视 silo 会投票宣布目标消亡。
    • 由于默认配置了两个必需的投票,间接探测中的否定确认视为两个投票,当故障被多个角度确认时,可以更快地宣布消亡 silo
  12. 执行实施完美故障检测:一旦 silo 在表中被声明为消亡,它就会被每个 silo 视为消亡,即使它实际上并未消亡(只是已暂时分区或检测信号消息丢失)。 每个 silo 会停止与它通信,一旦它得知自身已消亡(通过从表中读取其自新状态),它就会自尽并关闭其进程。 因此,必须有一个基础结构来以新进程的形式重启 silo(在启动时生成新的周期数)。 当该基础结构托管在 Azure 中时,这种情况会自动发生。 如果没有,可以需要其他基础结构,例如配置为在故障时自动重启的 Windows 服务或 Kubernetes 部署。

  13. 如果有一段时间无法访问表会怎样:

    当存储服务关闭、不可用或存在通信问题时,Orleans 协议不会错误地将孤岛声明为失效。 正常运行的孤岛将继续正常工作,而不会出现任何问题。 但是,Orleans 将无法将某个孤岛标记为非活跃状态(如果它通过漏检探针发现某些孤岛非活跃状态,它将无法把这一事实写入表格),也无法允许新的孤岛加入。 因此,完整性将受到影响,但准确度不受影响 — 在表中分区永远不会导致 Orleans 错误地将孤岛声明为消亡。 此外,如果使用部分网络分区(有些孤岛可以访问表,而有些则不可以),则可能会发生这种情况:Orleans 会将已消亡的孤岛声明为消亡,但所有其他孤岛需要在一段时间后才知道这一点。 因此检测可能会延迟,但 Orleans 不会因为表不可用而错误地灭杀某个孤岛。

  14. IAmAlive 为诊断和灾难恢复编写数据

    除了在 silo 之间发送的检测信号之外,每个 silo 会定期更新表的、该 silo 自身的行中的“I Am Alive”时间戳。 这有两个用途:

    1. 对于诊断,它为系统管理员提供了一种简单的方法来检查群集的运行情况,并确定 silo 最后一次运行的时间。 时间戳通常每 5 分钟更新一次。
    2. 对于灾难恢复,如果 silo 在几个时间段(通过 NumMissedTableIAmAliveLimit 配置)未更新其时间戳,则新 silo 将在启动连接性检查期间忽略它,从而允许群集在未正确清理的情况下从 silo 崩溃的情况中恢复。

成员身份表

如前所述,IMembershipTable 用作一个会合点,使接收器能够相互找到彼此,使 Orleans 客户端能够找到接收器,同时帮助协调有关成员身份视图的协议。 主 Orleans 存储库包含许多系统的实现,例如 Azure 表存储、Azure Cosmos DB、PostgreSQL、MySQL/MariaDB、SQL Server、Apache ZooKeeper、Consul IO、Apache Cassandra、MongoDB、Redis、AWS DynamoDB 以及用于开发的内存中实现。

  1. Azure 表存储 - 在此实现中,我们使用了 Azure 部署 ID 作为分区键,使用了 silo 标识 (ip:port:epoch) 作为行键。 它们共同保证了每个 silo 的键是唯一的。 对于并发控制,我们使用了基于 Azure 表 ETag 的乐观并发控制。 每次从表中读取时,我们都会为读取的每个行存储 ETag,并在尝试写回时使用该 ETag。 每次写入时,Azure 表服务都会自动分配和检查 ETag。 对于多行事务,我们利用了 Azure 表提供的批处理事务支持,这可以保证基于具有相同分区键的行完成可序列化事务。

  2. SQL Server - 在此实现中,配置的部署 ID 用于区分部署,以及用于标识 silo 属于哪些部署。 silo 标识定义为相应表和列中 deploymentID, ip, port, epoch 的组合。 关系后端使用乐观并发控制和事务,类似于在 Azure 表实现中使用 ETag 的过程。 关系实现预期数据库引擎生成使用的 ETag。 对于 SQL Server,在 SQL Server 2000 上,生成的 ETag 是从 NEWID() 调用中获取的 ETag。 在 SQL Server 2005 和更高版本上使用的是 ROWVERSION。 Orleans 以不透明的 VARBINARY(16) 标记形式读取和写入关系 ETag,并将其以 base64 编码字符串的形式存储在内存中。 Orleans 支持使用 UNION ALL(适用于Oracle,包括 DUAL,目前用于插入统计数据)执行多行插入。 可以在 CreateOrleansTables_SqlServer.sql 中查看 SQL Server 的确切实现和基本原理。

  3. Apache ZooKeeper - 在此实现中,我们使用了配置的部署 ID 作为根节点,并使用了 silo 标识 (ip:port@epoch) 作为子节点。 它们共同保证了每个 silo 的路径是唯一的。 对于并发控制,我们使用了基于节点版本的乐观并发控制。 每次从部署根节点读取时,我们都会为读取的每个子 silo 节点存储版本,并在尝试写回时使用该版本。 每次节点的数据发生更改时,ZooKeeper 服务都会以原子方式递增版本号。 对于多行事务,我们利用了 multi 方法,该方法保证基于具有相同父部署 ID 节点的 silo 节点完成可序列化事务。

  4. Consul IO - 我们使用了 Consul 的键/值存储来实现成员身份表。 有关更多详细信息,请参阅 Consul 部署

  5. AWS DynamoDB - 在此实现中,我们使用了群集部署 ID 作为分区键,使用了 silo 标识 (ip-port-generation) 作为范围键来统一记录。 ETag 属性通过在 DynamoDB 上进行条件写入来实现乐观并发。 实现逻辑与 Azure 表存储非常相似。

  6. Apacha Cassandra - 在此实现中,我们将服务 ID 和群集 ID 复合用作分区键,将孤岛标识(ip:port:epoch)用作行键。 它们共同保证了每个 silo 的行是唯一的。 对于并发控制,我们使用基于使用轻型事务的静态列版本的乐观并发控制。 此版本列针对分区/群集中的所有行共享,因此为每个群集的成员身份表提供一致的递增版本号。 此实现中没有多行事务。

  7. 用于开发设置的内存中仿真。 我们为该实现使用特殊系统 grain。 此 grain 驻留在某个指定的主要 silo 上,该 silo 仅用于开发设置。 在任何实际生产应用中不需要主要 silo。

设计理由

一个合理的问题是,为何不完全依赖 Apache ZooKeeperetcd 通过潜在地使用它对包含临时节点的组成员身份的 ZooKeeper 现成支持来实现群集成员身份? 为何我们花费精力来实现自己的成员身份协议? 主要有三个原因:

  1. 在云中部署/托管:

    Zookeeper 不是托管服务。 这意味着,在云环境中,Orleans 客户必须部署/运行/管理他们自己的 ZK 群集实例。 这只是另一个不必要的负担,我们不想强迫我们的客户。 通过使用 Azure 表,我们可以依赖一个托管服务来大幅简化我们客户的工作。 简单而言,在云中使用的是云即平台,而不是云即基础结构。 另一方面,在本地运行和管理自己的服务器时,依赖 ZK 作为 IMembershipTable 的实现是一个可行的选项。

  2. 直接故障检测:

    使用包含临时节点的 ZK 组成员身份时,将在 Orleans 服务器(ZK 客户端)与 ZK 服务器之间执行故障检测。 这不一定与 Orleans 服务器之间的实际网络问题有关联。 我们的愿望是,故障检测准确反映群集内的通信状态。 具体而言,在我们的设计中,如果 Orleans 接收器无法与 IMembershipTable 通信,则它不被视为已消亡,而可以继续正常工作。 与此相反,如果我们使用包含临时节点的 ZK 组成员身份,则与 ZK 服务器断开连接可能会导致将 Orleans 接收器(ZK 客户端)声明为消亡,而实际上它可能是存活的,并可完全正常运行。

  3. 可移植性和灵活性:

    作为 Orleans 理念的一部分,我们不希望强制要求严重依赖于任何特定技术,而是采用灵活的设计,可以通过不同的实现轻松切换不同的组件。 这正是 IMembershipTable 抽象所起到的作用。

成员身份协议的属性

  1. 可以处理任意数量的故障:

    我们的算法可以处理任意数量的故障(即 f<=n),包括整个群集重启。 这与“传统的”基于 Paxos 的解决方案不同,后者需要仲裁(通常是少数服从多数的机制)。 在生产场合中,我们看到过有超过一半的 silo 出现故障。 我们的系统可保持正常运行,而基于 Paxos 的成员身份无法继续。

  2. 发送到表的流量很少:

    实际的探测直接在服务器之间进行,而不会探测表。 从故障检测的角度看,这会产生大量的流量,并且准确度会下降 - 如果某个 silo 无法访问表,它就会遗漏写入“I am alive”(我是存活的)检测信号,因而其他 silo 会将其灭杀。

  3. 可调准确度与完整性:

    虽然你无法同时实现完美和准确的故障检测,但人们通常希望能够在准确度(不希望将一个存活的 silo 声明为消亡)和完整性(希望尽快将一个确实消亡的 silo 声明为消亡)之间进行权衡。 使用可配置的投票(用于声明消亡和未命中的探测)可以在两者之间进行权衡。 有关详细信息,请参阅耶鲁大学文章 Computer Science Failure Detectors(计算机科学故障检测器)。

  4. 规模:

    协议可以处理数千台甚至数万台服务器。 这与传统的基于 Paxos 的解决方案(例如组通信协议)不同,这些解决方案的规模不超过数十台服务器。

  5. 诊断:

    该表还能够为诊断和故障排除提供方便。 系统管理员可以立即在表中找到当前存活的 silo 列表,以及查看所有已灭杀 silo 和怀疑的历史记录。 这些信息在诊断问题时特别有用。

  6. 为何我们需要为 IMembershipTable 实现使用可靠的永久性存储:

    我们将持久存储用于 IMembershipTable,以实现两个目的。 首先,它用作会合点,供接收器相互查找彼此,以及供 Orleans 客户查找接收器。 其次,使用可靠存储可以帮助我们协调成员身份视图的协议。 虽然我们直接在 silo 之间以对等方式执行故障检测,但我们会将成员身份视图存储在可靠存储中,并使用此存储提供的并发控制机制来达成有关哪个 silo 存活、哪个 silo 消亡的协议。 这样,从某种意义上讲,我们的协议会将分布式共识的难题转嫁到云。 在这一点上,我们充分利用了基础云平台的强大功能,将其真正用作平台即服务 (PaaS)。

  7. 直接将 IAmAlive 写入表中(仅用于诊断):

    除了在 silo 之间发送的检测信号之外,每个 silo 还会定期更新表的、该 silo 自身的行中的“I Am Alive”列。 此“I Am Alive”列仅用于手动故障排除和诊断,成员身份协议本身不使用此列。 通常以低得多的频率写入此列(每 5 分钟一次),对于系统管理员而言,这是一种非常有用的手段,可以检查群集的存活状态,或者轻松发现 silo 的上次存活时间。

致谢

我们希望承认 Alex Kogan 对此协议的第一个版本的设计和实现的贡献。 这项工作是作为 2011 年夏季 Microsoft Research 暑期实习的一部分完成的。 基于 ZooKeeper 的 IMembershipTable 的实现由 Shay Hazor完成,SQL IMembershipTable 的实现由 Veikko Eeva完成,AWS DynamoDB IMembershipTable 的实现由 古腾堡·里贝罗 完成,基于 Consul 的 IMembershipTable 实现由 保罗·诺斯完成,最后阿帕奇 Cassandra IMembershipTable 的实现由 Arshia001OrleansCassandraUtils 改编完成。