Orleans 中的群集管理

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

该协议依赖于外部服务来提供 IMembershipTable 的抽象。 IMembershipTable 是一个扁平的类似于 No-SQL 的持久表,用于实现两种目的。 首先,它用作会合点,供接收器相互查找彼此,以及供 Orleans 客户查找接收器。 其次,它用于存储当前成员身份视图(存活 silo 列表)并帮助协调成员身份视图的协议。 IMembershipTable 目前有 6 种实现:基于 Azure 表存储、SQL Server、Apache ZooKeeperConsul IOAWS DynamoDB,以及用于开发的内存中仿真。

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

基本成员身份协议

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

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

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

  4. 如果 P 在 K 秒内有超过 Z 次的怀疑,则 S 会将 P 已消亡信息写入 P 的行,并广播一个请求,使所有 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. 配置:我们提供了一个默认配置,在 Azure 的生产应用中可以手动调整该配置。 目前,默认配置为:每个 silo 由其他三个 silo 监视,两个怀疑就足以声明 silo 消亡,仅限过去三分钟的怀疑(否则怀疑已过时)。 每隔 10 秒发送一次 ping,需要未命中 3 次 ping 才能怀疑一个 silo。

  8. 执行实施完美故障检测 - 理论上可能存在这种情况:silo 进程本身仍在运行,但由于 silo 与其他 silo 失去联系,此 silo 被声明为消亡。 为了解决此问题,一旦 silo 在表中被声明为消亡,它就会被每个 silo 视为消亡,即使它实际上并未消亡(只是已暂时分区或检测信号消息丢失)。 每个 silo 会停止与它通信,一旦它得知自身已消亡(通过从表中读取其自新状态),它就会自尽并关闭其进程。 因此,必须有一个基础结构来以新进程的形式重启 silo(在启动时生成新的周期数)。 当该基础结构托管在 Azure 中时,这种情况会自动发生。 否则,需要部署另一个基础结构。 例如,配置为在失败或 Kubernetes 部署时自动重启的 Windows 服务。

  9. 进行优化以降低定期读取表的频率,并使所有 silo 更快地知道有新 silo 加入和有 silo 消亡。 每当任何 silo 成功写入表(怀疑,新加入等)时,它也会广播到所有其他 silo -“立即重新读取表”。 silo 不会告诉其他 silo 它在表中写入了什么内容(因为这些信息可能已过时/错误),它只是让这些 silo 重新读取该表。 这样,我们可以很快地了解成员身份的变化,而无需等待完成整个定期读取周期。 如果“重新读取表”消息丢失,我们仍然需要进行定期读取。

基本成员身份协议的属性

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

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

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

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

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

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

  4. 规模:

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

  5. 诊断:

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

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

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

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

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

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

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

扩展到顺序成员身份视图

上述基本成员身份协议后来已扩展为支持顺序成员身份视图。 我们将简要介绍进行这种扩展的原因及其实现方式。 这种扩展不会改变上述设计中的任何内容,而只会添加一个属性来将所有成员身份配置全局排序。

将成员身份视图排序为何很有作用?

  • 这可以序列化将新 silo 加入群集的操作。 这样,在将某个新 silo 加入群集时,可以验证与其他每个已启动的 silo 的双向连接。 如果某些已加入的 silo 未做出回应(可能表示新 silo 存在网络连接问题),则不允许新 silo 加入。 这可以确保至少有一个 silo 启动时,群集中的所有 silo 之间才会完全建立连接(这一点已实现)。

  • silo 中更高级别的协议(例如分布式 grain 目录)可以利用成员身份视图已顺序的事实,并使用此信息执行更智能的重复激活解决方案。 具体而言,当目录发现已创建 2 个激活并且成员身份不断变化时,它可能会决定取消激活基于现已过时的成员身份信息创建的旧激活。

扩展的成员身份协议:

  1. 为了实现此功能,我们利用了由 MembershipTable 提供的基于多行的事务的支持。

  2. 我们添加了一个成员身份版本的行用于跟踪表的更改。

  3. 当 silo S 想要为 silo P 写入怀疑或消亡声明时:

    1. S 读取最新的表内容。 如果 P 已消亡,则不执行任何操作。 否则:
    2. 在同一事务中将更改写入 P 的行,递增版本号并将其写回表中。
    3. 这两项写入都以 ETag 为条件。
    4. 如果由于 P 的行或版本行上的 ETag 不匹配而导致事务中止,请再次尝试。
  4. 对表的所有写入都会修改并递增版本行。 这样,对表的所有写入都会序列化(通过序列化对版本行的更改),并且由于 silo 只递增版本号,因此写入操作也按升序全局排序。

扩展的成员身份协议的可伸缩性:

在扩展版本的协议中,所有写入都通过一行来序列化。 这可能会损害群集管理协议的可伸缩性,因为它会增加并发表写入之间发生冲突的风险。 为了部分缓解此问题,silo 通过使用指数退避来重试对表的所有写入。 我们已观察到,在最多包含 200 个 silo 的 Azure 生产环境中,扩展的协议可以平稳运行。 但是,我们确实认为在扩展到一千个以上的 silo 时,该协议可能出现问题。 在这么大的设置中,可以轻松禁用版本行的更新,从而在本质上保留群集管理协议的其余部分,并放弃全局排序属性。 另请注意,此处指的是群集管理协议的可伸缩性,而不是 Orleans 的其他方面。 我们相信,Orleans 运行时的其他方面(消息传递、分布式目录、粒度托管、客户端到网关的连接)可扩展到数百个接收器。

成员身份表

如前所述,IMembershipTable 用作一个会合点,使接收器能够相互找到彼此,使 Orleans 客户端能够找到接收器,同时帮助协调有关成员身份视图的协议。 IMembershipTable 目前有六种实现:基于 Azure 表、SQL Server、Apache ZooKeeper、Consul IO、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. 用于开发设置的内存中仿真。 我们为该实现使用了一个称为 MembershipTableGrain 的特殊系统 grain。 此 grain 驻留在某个指定的主要 silo 上,该 silo 仅用于开发设置。 在任何实际生产应用中不需要主要 silo。

配置

成员身份协议是通过 OrleansConfiguration.xml 文件的 Globals 节中的 Liveness 元素配置的。 默认值已根据 Azure 中的生产应用年数进行调整,我们认为默认设置非常合理。 一般情况下,没有必要更改它们。

示例配置元素:

<Liveness ProbeTimeout="5s"
    TableRefreshTimeout="10s"
    DeathVoteExpirationTimeout="80s"
    NumMissedProbesLimit="3"
    NumProbedSilos="3"
    NumVotesForDeathDeclaration="2" />

实现了 4 种类型的存活状态。 存活状态协议的类型是通过 OrleansConfiguration.xml 文件的 Globals 节中 SystemStore 元素的 SystemStoreType 属性配置的。

  1. MembershipTableGrain:成员身份表存储在主要 silo 上的某个 grain 中。 这只是一个开发设置。
  2. AzureTable:成员身份表存储在 Azure 表中。
  3. SqlServer:成员身份表存储在关系数据库中。
  4. ZooKeeper:成员身份表存储在 ZooKeeper 系宗中。
  5. Consul:配置为采用 MembershipTableAssembly = "OrleansConsulUtils" 设置的自定义系统存储。 有关更多详细信息,请参阅 Consul 部署
  6. DynamoDB:配置为采用 MembershipTableAssembly = "OrleansAWSUtils" 设置的自定义系统存储。

对于所有的存活状态类型,常用配置变量在 Globals.Liveness 元素中定义:

  1. ProbeTimeout:探测其他 silo 存活状态的秒数,或 silo 发送有关自身的“I am alive”检测信号消息的秒数。 默认值为 10 秒。
  2. TableRefreshTimeout:从成员身份表中提取更新的秒数。 默认值为 60 秒。
  3. DeathVoteExpirationTimeout:成员身份表中消亡投票的过期时间(秒)。 默认值为 120 秒
  4. NumMissedProbesLimit:来自某个 silo 的“I am alive”检测信号消息的缺失数量,或导致怀疑此 silo 已消亡的未回复探测数量。 默认值为 3。
  5. NumProbedSilos:每个 silo 探测其存活状态的 silo 数量。 默认值为 3。
  6. NumVotesForDeathDeclaration:将某个 silo 声明为消亡所需的未过期投票数(应该不超过 NumMissedProbesLimit)。 默认值为 2。
  7. UseLivenessGossip:是否使用 gossip 优化来加速传播存活状态信息。 默认为 true。
  8. IAmAliveTablePublishTimeout:在成员身份表中定期写入表示此 silo 存活的信息的秒数。 仅用于诊断。 默认值为 5 分钟。
  9. NumMissedTableIAmAliveLimit:表中来自某个 silo 的“I am alive”更新信息的缺失数量,缺失这些信息导致记录了警告。 不影响存活状态协议。 默认值为 2。
  10. MaxJoinAttemptTime:在放弃之前尝试加入 silo 群集的秒数。 默认值为 5 分钟。
  11. ExpectedClusterSize:群集的预期大小。 不需要非常准确,可以高估。 用于调整重试写入 Azure 表的指数退避算法。 默认值为 20。

设计理由

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

  1. 在云中部署/托管:

    Zookeeper 不是托管服务(至少在 2015 年 7 月撰写本文时如此,当然,在 2011 年夏季首次实现此协议时,没有任何主流云提供商提供作为托管服务运行的 Zookeeper 版本)。 这意味着,在云环境中,Orleans 客户必须部署/运行/管理他们自己的 ZK 群集实例。 这只是另一个不必要的负担,我们不想强迫我们的客户。 通过使用 Azure 表,我们可以依赖一个托管服务来大幅简化我们客户的工作。 简单而言,在云中使用的是云即平台,而不是云即基础结构。 另一方面,在本地运行和管理自己的服务器时,依赖 ZK 作为 IMembershipTable 的实现是一个可行的选项。

  2. 直接故障检测:

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

  3. 可移植性和灵活性:

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

致谢

我们要感谢 Alex Kogan 对该协议第一版的设计和实现所作的贡献。 这项工作是作为 2011 年夏季 Microsoft Research 暑期实习的一部分完成的。 基于 IMembershipTable 的 ZooKeeper 实现由 Shay Hazor 完成,SQL IMembershipTable 的实现由 Veikko Eeva 完成,AWS DynamoDB IMembershipTable 的实现由 Gutemberg Ribeiro 完成,基于 Consul 的 IMembershipTable 实现由 Paul North 完成。