让你的群集游泳(2020)

2021-06-29 22:34:55

在这个博客文章中我们' ll涵盖系统如何形成集群,实际上是什么集群以及他们的职责是什么。我们' LL还呈现了不同的协议,以满足与他们相关的各种权衡的群集的需求。

为了使这个博客发表一点更实用,我们也会深入了解了众议员协议之一 - 从游泳首字母缩略词中的一个隶属协议 - 从理论上的角度来看,在F#中实现了示例。

从用户的角度来看,群集应该用作A"单机&#34的幻觉;并将其免于与 - 通常动态变化 - 互联服务器网络的所有复杂性安全。

正如事实证明,那里的一系列协议和责任是群集的一部分需要解决以维持这种幻觉。走下自下而上,这里是我们集群通常需要知道如何回答的一些问题:

如何加入群集?通常,当我们有一个我们想要加入群集的新服务器节点时,它需要知道如何与已有群集本身的一部分的另一个节点通信。哪里可以找到这个信息?它'是需要回答的问题:最简单的诀窍是为新服务开始时提供的配置参数提供静态的联系点列表。

另一种方法是使用第三方服务(如数据库,Scenul,etcd或zookeeper)用作节点注册表。我们的集群通常不会生活在真空中,有时我们可以重复使用其他已经建立的服务以获得优势。

在某些情况下,我们可以利用低层Kubernetes DNS服务或MDNS的功能 - 特定于主机环境,以动态发现生活在同一网络中的其他设备。

我们如何知道其他节点是群集的一部分?这是所谓的会员协议的区域,我们' ll焦点在本博客文章的第二部分。在动态集群中,它通常通过跟踪活动发现的节点来完成,然后在节点连接/离开群集时更新和闲置它们。这里的许多决定依赖于部署方案 - 在同一数据中心内形成群集的服务具有不同的特征,从例如eg。移动设备的网格。在托管在同一数据中心托管的常见后端服务的情况下,每个节点都会保持有关群集状态的完整信息。这就是游泳操作的协议。

其他成员资格协议(如hyparview)启用只能在群集中映像。如果我们的群集在更高的尺度上操作时,则这是优选的。成千上万的节点(通常生活在数据中心的大多数集群服务Don' t达到几十多个服务器)。

我们如何将消息从一个节点发送到另一个节点?这通常也与隶属协议有关。大多数以数据中心为导向的系统可以保守假设系统中每个节点都可以连接到每个其他节点,形成(潜在地)完全连接的网格。

在其他场景中,我们需要考虑到某些节点可能无法连接到其他节点 - 因为网络特征潜在的网络特征。虽然当我们谈论群集协议时,最常见的情况是客户服务器架构,但常常需要应用更高级的方案(如我们所提到的,"服务器"本身不是单一的实体)。

我们如何检测到死区?也称为失败检测。该领域最古老的知识是每立即在预期超时边界内的Ping🡘ACK消息的简单交换,或者只是期望在给定时间间隔内发送心跳消息。心跳通常直接进入运输层(如TCP),有时是我们'重新能够直接捎带失败探测器。该解决方案的缺点是,虽然运输层本身是响应的 - 因为它通常由底层操作系统管理 - 我们的应用层实际上不是(例如,因为它无限期地挂起了僵化的僵局)。另一件事是网络连接的时间失败并不是意味着,那个节点赢得了' t试着重新启动并继续工作。

更常见的是,会员协议促进自己的心跳算法。什么'在这里值得注意,缺少心跳并不一定意味着,我们的节点已经死亡。它也可能不堪重负,提供其他传入请求。像Phi Accuctuct Dector或Lifeguard这样的现代算法(上面提到的游泳协议的扩展)考虑到这一行为。

这些是最基本的问题需要知道如何回答。我们可以称之为任何群集的第0层。本博客文章的主要部分将涵盖如何实施解决这些问题的协议。在它之上,还有许多其他更高的级别功能,旨在解决以下问题:

我们如何检测/响应定期网络分区?通常被称为分裂性情景的问题。它来自基本观察,即它'■不可能在网络边界中区分死区。它可以导致风险非常危险的情况,比如将我们的集群分成两个,每个人都相信,它'唯一一个活着,导致数据不一致甚至腐败。没有一个简单的治疗方法,那就是为什么一些系统(如akka.net分裂大脑resolvers)根据我们关心的权衡提供不同的策略。

不同的节点如何推理和决定群集中的状态?这种情况通常在负责数据管理(如分布式数据库)的系统中是常见的。由于节点必须能够为传入请求提供服务,因此有时它们可​​能会遇到对系统状态的冲突决策。在没有过多的细节中,两个通用方法两个问题是:避免冲突,这通常假设节点必须在提交决策之前建立关于系统状态的共识。它通常需要建立(和维护)节点之间的一些领导者,通过仲裁节点或两者组合同步。这是筏,ZAB或Paxo等流行协议的区域。

解决接受国家冲突的可能性出现的冲突 - 主要是在长期延迟或周期性地发生的权衡或定期无法访问的服务器中进行的权衡 - 但是以足够的元数据丰富,使得所有节点可以单独达到与结果状态的相同结论共识。这是一个主导地区的免除自由复制数据类型,您可以读取eg。在这里发布的博客文章中。

如何将请求路由到集群内部的给定资源?我们的系统状态通常由多种可寻址实体组成 - 通常复制,以便更高的可用性和弹性。然而,通常整个状态太大,无法适合任何一个节点。出于这个原因,它经常动态地在整个群集中划分。现在恳求一个问题:如何告诉,哪个节点包含一些虚拟键标识的实体?天真的方法是询问一些节点的子集希望,其中至少有一个将有我们希望的数据。给定群集的N个节点和实体复制了R次,我们应该能够在调用(n / r)+1节点后到达我们的资源。

更常见的方式是保留注册表,该注册表具有有关系统中每个单个实体的当前本地化的信息。由于这种方法并不展示,在实践中,我们在实践中,在分区中组合并将实体共同定位在一起,因此压缩了注册表以存储有关整个分区的信息而不是单个实体。在这种情况下,资源ID是(PartitionD,EntityID)的复合键。这是如何。 akka.cluster.arding或Riak核心作品。通常可以在每个节点上缓存一些热(常用)分区的一些子集,以便减少询问中央注册表甚至注册表本身可以是复制的商店。

我们还可以使用分布式哈希表 - 散列我们的实体密钥,然后映射到特定节点,该节点负责将属于关键空间的特定空间子集的资源(所有可能的哈希值的范围)。有时这可能意味着,首先尝试我们错过节点。因为群集状态正在发生变化,因此需要应用更多跳跃。 Microsoft Orleans和Cassandra使用该方法是流行的解决方案。

正如你所看到的,即使我们没有问过所有的问题,那里有很多事情发生了很多事情,并且可以根据我们的系统愿意采取的权衡来解决不同的方法。

在那里'今天的机会在未来做出一个单独的文章,今天我们' ll完全专注于游泳 - 在领事和持续几年中的系统中的系统中使用 - 如它&#39 ;很容易实现良好的开始。为了提高其弹性,减少假阳性故障检测,结账关于救生员的谈判 - 一系列关于原始游泳协议的扩展和观察。

新节点想要加入群集 - 如何使其发生,以及如何在群集中通知关于该事件的其他节点?

节点突然终止或者无法达到任何更长。如何检测到其他事实的信息?

虽然前两种情况很容易,但所有的复杂性都带有第三种情况。我们需要发现节点是否无法达到,但是' s还不够 - 由于临时网络故障可能发生在同一数据中心内,他们通常会迅速愈合。我们不想恐慌并将节点扔出群集,因为我们可以在分开第二个中达到它。这将导致非常摇摇欲坠和脆弱的集群。

我们已经提到了心跳机制 - 每一个现在,然后我们将ping消息发送到其他随机节点。预计此节点将在预期超时之前用ACK回答。简单的。

现在,如果节点没有在超时回应的情况下会发生什么?正如我们所说,我们不想过度反应。所以我们不要考虑这个节点已经死了。相反,我们认为它是怀疑并告知别人我们的怀疑。接收嫌疑八卦的其他节点预计它将在指定的超时中确认 - 否则他们将考虑一个恶作剧,并从其疑似列表中删除节点。

现在我们所需要的只是确认 - 为了这样做,我们需要向别人询问别人进行验证。所以我们'重新选择另一个未经用的节点,并要求它为我们的ping嫌疑人(此请求被称为ping-req)。现在,如果该调解员从嫌疑人那里收到ACK,我们知道节点是活着的,只是我们的网络连接被切断了原因:

在另一边,如果我们的调解员也没有收到Ack,我们现在有两只手验证,节点对大量时间框架无响应 - 因此它可以确认为每个人死亡。

当然这一点并不意味着节点确实死了 - 一个转义机制是,如果怀疑将获得关于自己的可疑通知,它可以通过广播活动来覆盖它。可能有不同的原因,为什么怀疑' t按时回应:它可能太占用(由其他请求的积压或ev。停止 - 世界垃圾收集的底层VM)。也可能发生,我们的网络分开了分开的集群 - 这种现象被称为分裂性。

最终没有办法保证100%可靠的故障检测。我们'重新交易合理的错误失败比,我们如何迅速地检测到节点作为死亡 - 通常这两个目标相互互相努力。在游泳方面,我再次建议查找救生员,该救生员地解决了一些提到的方案。然而,我们' LL在下面的实施中没有涵盖它,以保持简洁。

现在,我们'重新实际实施该协议。为了这样做'重新应用一些简化 - 我们' ll这样做,使协议更容易理解:

一般来说要关联特定请求/响应消息,我们应该使用某种形式的序列号。这可能导致场景,当A向B发送Ping1请求时,其与Ack1响应,一段时间后者将另一个Ping2发送到B并接收Ack1回来 - 问题是它不能告诉该消息的问题是为了确认这条消息。这种情况非常罕见,但不是不可能的。如果也可能导致"复活和#34;死亡节点。

虽然我们'重新进入肩背上的肩带 - 像纸张暗示 - 我们每次都只是八卦整个会员国。它不是最优越的方式,但它' ll工作足够好。

PS:避免使用每个八卦发送整个状态的其他方式是计算当前节点成员身份视图的一致哈希,并将该散列放在ping内而不是完整视图。这样我们的ping就会携带哈希值,这是响应者可以与自己状态的哈希相比。只有当哈希不同(大多数时间都有大部分时间的集群,他们在他们不时的时间和#39; t),我们'重新将会员状态放在Ack之上。

此处使用的代码可在本发明员中提供,我建议使用,如我们' LL在这里不涵盖所有内容。

由于我们不想处理节点到节点通信的所有复杂性,我们' LL只是在抽象之上构建一个模型,这将让我们评估算法,而不会导致解决其他问题。以下基本先决条件是:

一个传输层,让我们向另一个(可能远程)端点发送消息。我们希望处理管理网络连接或序列化详细信息。

可访问端点后面的代理,能够为多个请求提供服务并以线程安全方式更改其状态。

由于这些原因,我将使用Akka.net/Kkling和模拟我们的节点使用生活在同一过程中的演员。您可以轻松地采用它以通过Akka.Remote连接的不同演员系统。我们将此作为培训练习 - 在实践中,如果您想在Akka中使用群集,您已经在Akka.Cluster顶部建立了整个生态系统,这些生态系统都在处理会员资格 - 以及引言中提到的其他问题 - 适合您。

我们的演员将通过提供它的初始联系点列表来创建:这些是已知为群集的一部分的端点:

打开akklinguse sys = system.create"游泳群和#34; < | configuration.parse config //在此示例中,其他//`b`是生活在其他节点上的其他//游泳集群成员的演员​​refs。让C =产卵系统"节点B" < |道具(游泳.Membership [A; B])

为了创建该演员,我们需要处理加入过程。这个想法是我们' ll尝试从提供的列表中挑选端点(它对应于Akka.Cluster中的种子节点地址)并向它们发送加入请求。如果群集节点演员将接收这样的请求,则' s义务通过向所有群集成员发送加入响应(包括请求者)来接受它。

如果加入WORN和#39; T到达,例如。因为我们错误地配置了我们的群集和其他端点,所以达到了' t and#39;我们等待一段时间并从列表中尝试另一个节点。这可以表示为:

让成员身份(种子:端点列表)(CTX:Actor< _>)=设态= {mylising = {endpoint = ctx.self} Active = set.empty嫌疑人= map.empty}(* actor代码的其余部分.. 。*)让REC加入州取消=演员{匹配! ctx.receive()与|职工[] - >返回停止//未能加入任何会员|职工(下一个::剩下) - >下一个<加入State.Myself让Call取消= CTX.schedule Connectimeout CTX.Self(剩余剩余)返回!加入州取消|加入八卦 - > CANCEL.CANCEL()//取消加入超时返回!成为州八卦|当peer = statem.myself - > //建立新群集取消.Cancel()//取消加入超时返回!成为州(set.singleton state.myself)| _ - >返回未处理的}匹配种子| [] - >成为州(set.singleton state.myself)|种子::剩下 - >种子<加入State.Myself让Call = CTX.Schedule Connectimeout CTX.Self(剩余剩余)加入州取消

接收到加入的请求Actor将自己视为群集的操作成员,准备工作。从现在开始,它将触发自身,以定期检查其他人是否保持响应:

让合并八卦状态= {youst active = state.active + gossip}让berseready状态gossip = ctx.schedule pinginterval ctx.self nextround |>忽略就绪(合并八卦状态)

如前所述,在这种情况下,八卦只是一整套活动集群成员,而准备好是用于标准群集活动的演员行为:

让Recrade State =演员{匹配! ctx.receive()使用//其他消息处理程序| _ - >返回未处理}

由于我们从请求者方面覆盖了加入程序,因此为操作集群成员提供了:

比赛! ctx.receive |.加入peer - >让Gossip = Set.Add Peer状态。Active Let Msg =加入八卦八卦|> set.remove state.myself |> set.iter(有趣的peer - > peer.endpoint< msg)返回!准备就绪{状态与active = gossip} |加入八卦 - >返回!准备好(合并八卦状态)//其他处理程序

正如我们所说,一旦新成员试图加入,我们只需将我们的活动成员状态更新并将其汇报到加入消息中的所有其他成员,因此它们也可以更新。

现在,让' s覆盖ping过程。首先,我们提到我们现在想要触发我们的会员,然后 - 当我们计划成比赛中的Nextround事件时,您可以看到。现在它如何工作?我们'重新选择一个节点随机 - 除了当前的一个没有疑似(嫌疑人是' t按时回复ping的节点) - 并向它发送ping请求。与此同时,我们还安排了该请求完成的超时:

让选择同行=匹配set.count对等体0 - >没有|数 - >让idx = threadlocalrandom.current.next计数一些(SEQ.ITEM IDX对等体)让就绪状态= ACTOR {匹配! ctx.receive()与| nextround - > ctx.schedule pinginterval ctx.self nextround |>忽略让嫌疑人= state.suspects |> Map.Toseq |≫ seq.map fst |> set.ofseq让别人=(set.remove state.myself state.active) - 嫌疑人//随意选择一个成员,除了自我而不是标记为怀疑匹配选择没有 - >返回!就绪状态|一些同伴 - > //将ping请求发送到该对等体和调度超时peer.endpoint< ping(state.active,ctx.self)让取消= ctx.schedule pingtimeout ctx.self(pingtimeout peer)让skiplist = set.ofArray [|州。同伴|]回来!准备{state with suspects = map.add peer(取消,waitpingack skiplist)state.suspects} | ping(八卦,发件人) - > //回复传入ping ricking sender&lt ;! Pingack(州.MYSELF,State.active)返回!准备好(合并八卦状态)//其他处理程序}

在这里,我们使用我们的嫌疑人映射来跟踪超时取消和跳过列表。什么'跳过列表?我们很快掩盖了。正如我们所提到的那样,当Pingack' t准时到达时,我们会选择另一个成员(随意)并要求它使用pingreq消息来为我们审视我们的嫌疑人。这样,我们试图在我们的故障检测算法中减轻假否定的风险:

让准备状态=演员{匹配! ctx.receive()与| Pingreq(嫌疑人,八卦,发件人) - >让取消= ctx.schedule IndirectpingTimeout CTX.Self(PingTimeout Suspect)嫌疑次数.EndPoint< ping(state.active,ctx.self)退货!准备{Merge Gossip州与嫌疑人= map.add嫌疑人(取消,等待,等待reqack)state.suspects

......