Orleans 中的群集管理
Orleans 通过内置成员身份协议提供群集管理,我们有时称为 群集成员身份。 该协议的目标是让所有接收器(Orleans 服务器)就一组当前存活接收器达成一致,检测失败的接收器,并允许新接收器加入群集。
该协议依赖于外部服务来提供 IMembershipTable 的抽象。 IMembershipTable 是一张平坦耐用的桌子,我们将其用于两个用途。 首先,它用作会合点,供接收器相互查找彼此,以及供 Orleans 客户查找接收器。 其次,它用于存储当前成员身份视图(存活 silo 列表)并帮助协调成员身份视图的协议。
我们目前有 6 种 IMembershipTable实现:基于 Azure 表存储,Azure Cosmos DB,ADO.NET (PostgreSQL, MySQL/MariaDB、SQL Server、Oracle)、Apache ZooKeeper、Consul IO、AWS DynamoDB、MongoDB、Redis、Apache Cassandra以及用于开发的内存中实现。
除了 IMembershipTable 以外,每个 silo 都参与完全分布式的对等成员身份协议,该协议检测失败的 silo 并就一组存活 silo 达成一致。 我们在下面介绍了Orleans 的成员身份协议的内部实现。
成员身份协议
启动后,每个孤岛都会使用 IMembershipTable 的实现将自身条目添加到众所周知的共享表中。 silo 标识 (
ip:port:epoch
) 和服务部署 ID(群集 ID)的组合用作表中的唯一键。 时期只是此接收器启动时的时间(刻度),因此可以保证ip:port:epoch
在给定的 Orleans 部署中唯一。silo 直接通过应用程序探测(“是否存活”
heartbeats
)相互监视。 探测作为直接消息通过与 silo 通信的相同 TCP 套接字在 silo 之间发送。 这样,探测就与实际的网络问题和服务器运行状况完全关联。 每个 silo 都会对一组可配置的其他 silo 进行探测操作。 silo 通过计算其他 silo 的标识中的一致哈希来选择要探测的对象,从而构成所有标识的虚拟环,并在该环中拾取 X 个后继 silo(这是一种众所周知的分布式技术,称为一致哈希,广泛应用于许多分布式哈希表,例如 Chord DHT)。如果 silo S 未收到来自受监视服务器 P 的 Y 次探测回复,则它会通过将带时间戳的怀疑写入 IMembershipTable 中 P 的行来表示怀疑。
如果 P 在 K 秒内有超过 Z 次的怀疑,则 S 会将 P 已消亡信息写入 P 的行,并将当前成员身份表的快照发送给所有其他 silo。 Silo 会定期刷新表,因此快照是一种优化方法,可以减少所有 silo 获取新的成员身份视图所需的时间。
更多详细信息:
怀疑将写入 IMembershipTable 中与 P 对应的行的特殊列中。当 S 怀疑 P 时,它会写入:“在时间点 TTT,S 怀疑了 P”。
一个怀疑不足以声明 P 消亡。 在可配置的时间窗口 T(通常为 3 分钟)内,需要有来自不同 silo 的 Z 次怀疑才能将 P 声明为已消亡。 怀疑是使用 IMembershipTable 提供的乐观并发控制写入的。
怀疑方 silo S 读取 P 的行。
如果
S
是最后一个怀疑方(在 T 周期内已有 Z-1 个怀疑方,在怀疑列中写入),则 S 会决定将 P 声明为消亡。 在这种情况下,S 会将自身添加到怀疑方列表中,并在 P 的状态列中写入“P 已消亡”。否则,如果 S 不是最后一个怀疑方,则 S 只会将自身添加到怀疑方的列中。
在任一情况下,写回都使用读取的版本号或 ETag,因此对此行的更新是序列化的。 如果由于版本/ETag 不匹配而导致写入失败,则 S 会重试(再次读取并尝试写入,除非 P 已标记为消亡)。
概括而言,这种“读取、本地修改、写回”序列是一个事务。 但是,我们不一定使用存储事务来执行此操作。 “事务”代码在服务器上本地执行,我们使用 IMembershipTable 提供的乐观并发来确保隔离性和原子性。
每个 silo 定期读取整个成员身份表来进行部署。 这样,silo 便知道有新的 silo 加入并有其他 silo 被声明为消亡。
快照广播:为了减少定期表读取的频率,每当 silo 写入表(怀疑,新加入等)时,它会向所有其他 silo 发送当前表状态的快照。 由于成员身份表是一致的,并且版本是单调的,因此每次更新都会生成一个可以安全共享的唯一版本快照。 这样就可以立即传播成员身份更改,而无需等待定期读取周期。 在快照分发失败的情况下,定期读取仍被作为备份机制进行维护。
有序成员身份视图:成员身份协议可确保全局完全排序所有成员身份配置。 此排序提供两个关键优势:
连接性保证:当新的仓加入集群时,它必须验证与其他每个活动仓的双向连接。 如果任何现有 silo 没有响应(可能表示网络连接问题),则不允许新 silo 加入。 这可确保在启动时群集中的所有孤岛之间完全连接。 有关 IAmAlive 的注意事项,请参见下文,以了解在灾难恢复情况下的例外。
一致的目录更新:更高级别的协议(例如分布式 grain 目录)依赖于具有一致、单调成员身份视图的所有 silo。 这样就可以更智能地解析重复的粒度激活。 有关详细信息,请参阅 粒度目录 文档。
实现的详细信息:
IMembershipTable 需要原子更新才能保证更改的全局总顺序:
- 实现必须以原子方式更新表条目(孤岛列表)和版本号
- 这可以通过使用数据库事务(如在 SQL Server 中)或使用 ETag 进行原子比较和交换操作(如在 Azure 表存储中)实现。
- 特定机制取决于基础存储系统的功能
表中的特殊会员版本行用于跟踪更改:
- 对表的每次写入(怀疑、消亡声明、加入操作)都会递增此版本号
- 所有写入都通过此行使用原子更新进行序列化
- 单调递增的版本确保所有成员身份更改有一个总排序
当接收器 S 更新接收器 P 的状态时:
- S 首先读取最新表状态
- 在单个原子操作中,它会更新 P 的行并递增版本号
- 如果原子更新失败(例如,由于并发修改),则会使用指数退避重试操作
可伸缩性注意事项:
由于争用增加,通过版本行序列化所有写入可能会影响可伸缩性。 该协议已在生产环境中验证过,可支持高达 200 个存储单元,但在超过一千个存储单元时可能会遇到挑战。 对于非常大的部署,即使成员身份更新成为瓶颈,Orleans(消息传递、粒度目录、托管)的其他部分仍可缩放。
默认配置:在 Azure 的生产使用期间,已手动调整默认配置。 默认情况下:每个筒仓由另外三个筒仓监视,两个怀疑报告足够宣告一个筒仓失效,怀疑报告仅限于过去三分钟(否则将被视为过时)。 探测信号每 10 秒发送一次,如果错过三个信号,就可以怀疑存在隔离问题。
自我监视:故障检测器整合了 HashiCorp 的 Lifeguard 研究(论文、讲座、博客)中的想法,以在灾难性事件期间,当群集中的很大一部分经历部分失效时,提高群集稳定性。
LocalSiloHealthMonitor
组件使用多个启发式方法对每个 silo 的运行状况进行评分:- 成员身份表中的活动状态
- 没有来自其他 silo 的怀疑
- 最近成功的探测响应
- 最近收到的探测请求
- 线程池响应能力(在 1 秒内执行的工作项)
- 计时器准确性(在计划的 3 秒内触发)
silo 的运行状况分数会影响其探测超时:与运行正常的 silo(评分 0)相比,运行不正常的 silo(评分 1-8)的超时时间增加。 这有两个好处:
- 在网络或系统承受压力时,为探测提供更多成功时间
- 这使得运行不正常的 silo 在错误地投票淘汰运行正常的 silo 之前,更有可能被投票淘汰
这在线程池资源不足等方案中尤其有价值;在这种情况下,慢速节点可能会因为无法足够快地处理响应而错误地怀疑正常节点。
间接探测:另一个 Lifeguard 启发的功能,通过减少运行不正常或分区 silo 错误地声明正常 silo 消亡的可能性来提高故障检测准确性。 当一个监视 silo 在投票宣布其消亡之前,对目标 silo 还有两次探测尝试时,它会采用间接探测:
- 监视筒仓随机选择另一个筒仓作为中介,并要求它探测目标
- 中介尝试联系目标 silo
- 如果目标在超时期限内未能响应,则中介会发送负面确认信息。
- 如果监视 silo 收到中介的否定确认,而中介通过上述自我监视声明自己正常,则监视 silo 会投票宣布目标消亡。
- 由于默认配置了两个必需的投票,间接探测中的否定确认视为两个投票,当故障被多个角度确认时,可以更快地宣布消亡 silo
执行实施完美故障检测:一旦 silo 在表中被声明为消亡,它就会被每个 silo 视为消亡,即使它实际上并未消亡(只是已暂时分区或检测信号消息丢失)。 每个 silo 会停止与它通信,一旦它得知自身已消亡(通过从表中读取其自新状态),它就会自尽并关闭其进程。 因此,必须有一个基础结构来以新进程的形式重启 silo(在启动时生成新的周期数)。 当该基础结构托管在 Azure 中时,这种情况会自动发生。 如果没有,可以需要其他基础结构,例如配置为在故障时自动重启的 Windows 服务或 Kubernetes 部署。
如果有一段时间无法访问表会怎样:
当存储服务关闭、不可用或存在通信问题时,Orleans 协议不会错误地将孤岛声明为失效。 正常运行的孤岛将继续正常工作,而不会出现任何问题。 但是,Orleans 将无法将某个孤岛标记为非活跃状态(如果它通过漏检探针发现某些孤岛非活跃状态,它将无法把这一事实写入表格),也无法允许新的孤岛加入。 因此,完整性将受到影响,但准确度不受影响 — 在表中分区永远不会导致 Orleans 错误地将孤岛声明为消亡。 此外,如果使用部分网络分区(有些孤岛可以访问表,而有些则不可以),则可能会发生这种情况:Orleans 会将已消亡的孤岛声明为消亡,但所有其他孤岛需要在一段时间后才知道这一点。 因此检测可能会延迟,但 Orleans 不会因为表不可用而错误地灭杀某个孤岛。
IAmAlive 为诊断和灾难恢复编写数据:
除了在 silo 之间发送的检测信号之外,每个 silo 会定期更新表的、该 silo 自身的行中的“I Am Alive”时间戳。 这有两个用途:
- 对于诊断,它为系统管理员提供了一种简单的方法来检查群集的运行情况,并确定 silo 最后一次运行的时间。 时间戳通常每 5 分钟更新一次。
- 对于灾难恢复,如果 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 以及用于开发的内存中实现。
Azure 表存储 - 在此实现中,我们使用了 Azure 部署 ID 作为分区键,使用了 silo 标识 (
ip:port:epoch
) 作为行键。 它们共同保证了每个 silo 的键是唯一的。 对于并发控制,我们使用了基于 Azure 表 ETag 的乐观并发控制。 每次从表中读取时,我们都会为读取的每个行存储 ETag,并在尝试写回时使用该 ETag。 每次写入时,Azure 表服务都会自动分配和检查 ETag。 对于多行事务,我们利用了 Azure 表提供的批处理事务支持,这可以保证基于具有相同分区键的行完成可序列化事务。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 的确切实现和基本原理。Apache ZooKeeper - 在此实现中,我们使用了配置的部署 ID 作为根节点,并使用了 silo 标识 (
ip:port@epoch
) 作为子节点。 它们共同保证了每个 silo 的路径是唯一的。 对于并发控制,我们使用了基于节点版本的乐观并发控制。 每次从部署根节点读取时,我们都会为读取的每个子 silo 节点存储版本,并在尝试写回时使用该版本。 每次节点的数据发生更改时,ZooKeeper 服务都会以原子方式递增版本号。 对于多行事务,我们利用了 multi 方法,该方法保证基于具有相同父部署 ID 节点的 silo 节点完成可序列化事务。Consul IO - 我们使用了 Consul 的键/值存储来实现成员身份表。 有关更多详细信息,请参阅 Consul 部署。
AWS DynamoDB - 在此实现中,我们使用了群集部署 ID 作为分区键,使用了 silo 标识 (
ip-port-generation
) 作为范围键来统一记录。ETag
属性通过在 DynamoDB 上进行条件写入来实现乐观并发。 实现逻辑与 Azure 表存储非常相似。Apacha Cassandra - 在此实现中,我们将服务 ID 和群集 ID 复合用作分区键,将孤岛标识(
ip:port:epoch
)用作行键。 它们共同保证了每个 silo 的行是唯一的。 对于并发控制,我们使用基于使用轻型事务的静态列版本的乐观并发控制。 此版本列针对分区/群集中的所有行共享,因此为每个群集的成员身份表提供一致的递增版本号。 此实现中没有多行事务。用于开发设置的内存中仿真。 我们为该实现使用特殊系统 grain。 此 grain 驻留在某个指定的主要 silo 上,该 silo 仅用于开发设置。 在任何实际生产应用中不需要主要 silo。
设计理由
一个合理的问题是,为何不完全依赖 Apache ZooKeeper 或 etcd 通过潜在地使用它对包含临时节点的组成员身份的 ZooKeeper 现成支持来实现群集成员身份? 为何我们花费精力来实现自己的成员身份协议? 主要有三个原因:
在云中部署/托管:
Zookeeper 不是托管服务。 这意味着,在云环境中,Orleans 客户必须部署/运行/管理他们自己的 ZK 群集实例。 这只是另一个不必要的负担,我们不想强迫我们的客户。 通过使用 Azure 表,我们可以依赖一个托管服务来大幅简化我们客户的工作。 简单而言,在云中使用的是云即平台,而不是云即基础结构。 另一方面,在本地运行和管理自己的服务器时,依赖 ZK 作为 IMembershipTable 的实现是一个可行的选项。
直接故障检测:
使用包含临时节点的 ZK 组成员身份时,将在 Orleans 服务器(ZK 客户端)与 ZK 服务器之间执行故障检测。 这不一定与 Orleans 服务器之间的实际网络问题有关联。 我们的愿望是,故障检测准确反映群集内的通信状态。 具体而言,在我们的设计中,如果 Orleans 接收器无法与 IMembershipTable 通信,则它不被视为已消亡,而可以继续正常工作。 与此相反,如果我们使用包含临时节点的 ZK 组成员身份,则与 ZK 服务器断开连接可能会导致将 Orleans 接收器(ZK 客户端)声明为消亡,而实际上它可能是存活的,并可完全正常运行。
可移植性和灵活性:
作为 Orleans 理念的一部分,我们不希望强制要求严重依赖于任何特定技术,而是采用灵活的设计,可以通过不同的实现轻松切换不同的组件。 这正是 IMembershipTable 抽象所起到的作用。
成员身份协议的属性
可以处理任意数量的故障:
我们的算法可以处理任意数量的故障(即 f<=n),包括整个群集重启。 这与“传统的”基于 Paxos 的解决方案不同,后者需要仲裁(通常是少数服从多数的机制)。 在生产场合中,我们看到过有超过一半的 silo 出现故障。 我们的系统可保持正常运行,而基于 Paxos 的成员身份无法继续。
发送到表的流量很少:
实际的探测直接在服务器之间进行,而不会探测表。 从故障检测的角度看,这会产生大量的流量,并且准确度会下降 - 如果某个 silo 无法访问表,它就会遗漏写入“I am alive”(我是存活的)检测信号,因而其他 silo 会将其灭杀。
可调准确度与完整性:
虽然你无法同时实现完美和准确的故障检测,但人们通常希望能够在准确度(不希望将一个存活的 silo 声明为消亡)和完整性(希望尽快将一个确实消亡的 silo 声明为消亡)之间进行权衡。 使用可配置的投票(用于声明消亡和未命中的探测)可以在两者之间进行权衡。 有关详细信息,请参阅耶鲁大学文章 Computer Science Failure Detectors(计算机科学故障检测器)。
规模:
协议可以处理数千台甚至数万台服务器。 这与传统的基于 Paxos 的解决方案(例如组通信协议)不同,这些解决方案的规模不超过数十台服务器。
诊断:
该表还能够为诊断和故障排除提供方便。 系统管理员可以立即在表中找到当前存活的 silo 列表,以及查看所有已灭杀 silo 和怀疑的历史记录。 这些信息在诊断问题时特别有用。
为何我们需要为 IMembershipTable 实现使用可靠的永久性存储:
我们将持久存储用于 IMembershipTable,以实现两个目的。 首先,它用作会合点,供接收器相互查找彼此,以及供 Orleans 客户查找接收器。 其次,使用可靠存储可以帮助我们协调成员身份视图的协议。 虽然我们直接在 silo 之间以对等方式执行故障检测,但我们会将成员身份视图存储在可靠存储中,并使用此存储提供的并发控制机制来达成有关哪个 silo 存活、哪个 silo 消亡的协议。 这样,从某种意义上讲,我们的协议会将分布式共识的难题转嫁到云。 在这一点上,我们充分利用了基础云平台的强大功能,将其真正用作平台即服务 (PaaS)。
直接将 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 的实现由 Arshia001从 OrleansCassandraUtils
改编完成。