使用Serf.io(和Fly.io)构建集群

2021-01-15 20:07:59

假设我们希望看到一秒钟,当网页加载到新加坡的浏览器中时会发生什么。很容易; Fly.io将获取您扔给它的容器图像,将其转换为Firecracker VM,然后在新加坡运行。

我们需要一个在浏览器中加载网页的容器;听起来像无头铬的工作。这是一个Dockerfile;实际上,不要打扰,这只是安装正确的apt-get软件包然后下载正确的Chromium发行版的Dockerfile之一。入口点使用正确的参数运行Chromium。

这几乎可以正常工作。假设我们将Fly应用命名为ichabod-chrome。当flyctl部署完成时,我们的映像将作为VM在新加坡附近的某个地方运行,并且可以从ichabod-chrome.fly.dev从世界各地访问。您可以使用Chrome调试协议来驱动在新加坡运行的Chrome实例,该协议具有多种语言的实现;例如,如果我们想用Ruby截屏页面,我们可以只安装ferrum gem,然后:

超级无聊!整洁的工作,但是!但是,这里有一个明显的问题:Chrome调试协议未经身份验证,因此我们只是在互联网上闲逛,希望没人会对我们在此公共URL上创建的浏览器代理做些愚蠢的事情。

让我们解决这个问题。我们将Chrome作为6PN应用程序运行,并通过WireGuard与之对话。我们打开flyctl为我们生成的fly.toml,并添加:

我们也取消了整个[[services]]部分,因为我们不再将任何公共服务都接受健康检查。然后,我们更改入口点以绑定到其专用IPv6地址。

flyctl部署运行将加载我们的“新”应用程序,该应用程序仅通过专用IPv6地址讲CDP。但是:现在我们不能和它说话!我们不在专用IPv6网络上。

修复起来很容易:安装WireGuard(可在任何地方运行)。然后运行flyctl wireguard create,它将为我们生成一个WireGuard配置,我们可以在客户端中加载该配置。点击“连接”按钮,我们很高兴再次尝试,这次我们使用了加密安全通道来运行CDP。在我们生成的WireGuard配置中包含的内部DNS上,现在可以通过ichabod-chrome.internal访问我们的应用程序。

假设我们要在一堆不同位置的一堆无头铬。也许我们想截取来自不同国家/地区的CNN首页,或运行来自世界各地的Lighthouse测试。我不是在这里来判断您的人生决定。

设置和运行这些Chromium实例非常无聊。假设我们要大致在新加坡,悉尼,巴黎和智利进行跑步:

…就是这样; Fly将找出如何满足这些限制并进行适当部署的方法(我们现在要求4个实例,而Fly将尝试将这些实例分散到尽可能多的数据中心中)。

现在,我们要驱动这些新实例,并有选择地这样做。为此,我们必须能够找到它们。我们可以使用DNS来做到这一点:

这几乎是可行的,仅使用DNS进行实例发现,您可能会获得很长的路要走,尤其是在您的群集很简单的情况下。

但是对我来说,对于这个应用程序,这是一个令人讨厌的地方。我可以选择一堆尼特,但是最重要的是,没有一种很好的方法可以在DNS更改时自动获取更新。当实例启动时,我可以获得一幅相当不错的世界图景,但是随着时间的流逝,我必须经过扭曲才能更新该图景。

在Fly将DNS整合在一起时,我们有相同的想法。但是我们对它们什么也没做!我们很快得出结论,如果人们想要“有趣的”服务发现,他们可以B.Y.O.

让我们看看如何在该集群中发挥作用。我将设置HashiCorp Serf,以使该群集的所有组件相互了解。

他们做的事情有些相似,但是农奴比其HashiCorp兄弟姊妹领事少受到关注。真可惜,因为Serf是一个更简单,更易接近的系统,可完成许多人使用Consul的80%的工作。

Consul的合理思维模式是它是一个分布式系统,可以解决3个问题:

与Consul不同,Serf仅处理这些问题之一,即#1。实际上,Consul在后台使用Serf自行解决该问题。但是领事馆的建立要复杂得多。农奴在没有领导者的情况下运行(集群成员来来往往,每个人都弄清楚了),并且没有存储需求。

农奴很容易。在常规配置中(我们将Serf作为程序运行,而不是作为嵌入到应用程序中的库运行),集群中的每个节点都运行Serf代理,在安装Serf之后,它只是serf agent命令。可以在命令行参数中传递所有配置:

名称=" $ {FLY_APP_NAME}-$ {FLY_REGION}-$(主机名)" paddr = $(grep fly-local-6pn / etc / hosts | cut -f 1)农奴代理\ -node =" $ name" \ -profile = wan \ -bind =" [$ paddr]:7777" \ -tag角色=" $ {FLY_APP_NAME}" \-标记区域=" $ {FLY_REGION}"

没什么。我们给每个节点一个唯一的名称。默认情况下,Serf假定我们在局域网上运行,并相应地设置计时器;我们将其切换为WAN模式。重要的是,我们将Serf绑定到我们的6PN专用地址。然后,我们设置一些标签,以方便以后选择成员时使用。

为了帮助Serf在集群中找到其他成员并了解其成员的完整情况,可以进行一些快速介绍:

挖aaaa" $ {FLY_APP_NAME} .internal" +短|同时阅读raddr;如果[" $ raddr" !=" $ paddr" ];然后农奴加入" $ raddr"完成

在这里,我们只是从DNS中转储群集的当前快照,并使用serf join引入这些成员。现在,如果我们有节点Alice,Bob和Chuck,并且Alice向Bob介绍了自己,而Bob向Chuck介绍了自己,则Bob将确保Alice也了解Chuck。稍后我们将讨论其工作原理。

我将这两个动作(运行代理并进行介绍)包装在一个小的Shell脚本中。因为我现在在Docker映像中运行多个thingies,所以我将overmind用作驱动Procfile的新入口点。这是整个Dockerfile。

这给我带来了什么?好吧,从现在开始,如果我在组织的专用IPv6网络上,我可以找到任何节点,并立即获得所有其他节点的地图:

我可以将这些信息与Shell脚本集成在一起,但是我也可以直接将其引入我的应用程序代码中(这里是相对简单的serfx gem:

我可以轻松地按位置(通过“区域”)标签,角色或网络紧密度(如下所述)过滤。该界面比DNS更为简单,闪电般快速,并且始终是最新的。

Serf具有安全功能:您可以静态设置Serf通信的密钥,因此没有密钥的恶意节点将无法参与或阅读消息。

我想很好。如果我在确实依赖Serf加密的安全性环境中部署Serf,我会感到紧张。但是,坦率地说,这对我们来说并不重要,因为我们已经在专用网络上运行,并且与该网络的外部连接利用了WireGuard中极为复杂的加密技术。

人们描述Serf时出现的分布式系统术语的第一位是SWIM,即“可伸缩的弱一致性感染成员身份”协议。分布式系统中充满了带有缩写名称的协议,很难理解,SWIM并不是其中之一。我认为您甚至不需要图来绘制它。

您可以想象最简单的成员身份协议,在其中进行介绍(就像我们在上一节中所做的那样),每个成员都简单地中继消息并尝试连接到它所了解的每个新主机。如果您在项目中意外遇到成员资格问题,并且只需要敲出一些东西来解决它,这可能就是您想要的,它在某种程度上可以正常工作。

SWIM只是从该朴素协议向前迈出的启发式步骤,这些步骤使协议(1)可以更好地扩展,因此您可以处理成千上万个节点,并且(2)快速检测失败的节点。

首先,我们不是以一定的间隔向我们学习的每个主机发送垃圾邮件,而是选择一个随机子集。我们实质上只是对该子集中的每个主机执行ping操作。如果收到ACK,则表明他们仍然是成员(并且,当新节点连接到我们时,我们可以与他们共享整个世界的概况,以使它们快速更新)。如果我们没有收到ACK,则说明有些问题。

现在,为了防止每次网络中任何地方ping失败时组成员资格图不会震荡,我们向协议中添加了另一个事务:将无法ping的节点标记为SUS,我们选择了另一个随机的节点子集,我们要求他们为我们ping SUS节点。如果成功,他们会告诉我们,该节点不再是SUS。如果没有人可以对节点执行ping操作,我们最终得出结论,该节点是冒名顶替者,然后将其从船上弹出。

Serf的SWIM实施具有一些CS宽限说明,但如果需要,您可以在一两个小时内完成基本协议的制定。

Serf不仅仅是SWIM的实现,SWIM也不是其中最有趣的部分。该荣誉将必须归功于网络映射算法Vivaldi。由我的MIT CSAIL英雄(包括Russ Cox,Frans Kaashoek和(是的)Robert Morris)合集创作的Vivaldi计算了一个群集的所有点成对网络距离图。几年后,拉斯·考克斯(Russ Cox)发现了一条有趣的话题,即HashiCorp为Serf实施了他的论文。

我们将集群成员建模为存在于某个空间中。为了让您绕开它,请将它们视为具有笛卡尔3D坐标。这些坐标是抽象的。它们与真实的3D空间无关。

要在此空间中分配节点坐标,我们需要使用长度可变(并且以不确定为开始)的弹簧将它们相互连接。我们的工作将是学习这些长度,我们将通过采样网络延迟测量来做到这一点。

首先,我们将收集弹簧连接节点的集合并将其压缩到原点。首先,这些节点都彼此重叠。

然后,当我们从其他节点收集测量值时,我们将测量误差,将模型中的距离与测量值反映的距离进行比较。我们将沿着某个随机方向(通过生成随机单位矢量)将自己从正在测量的节点中移开,并按误差和灵敏度因子进行缩放。该灵敏度因子本身会根据我们的误差测量的历史记录发生变化,因此我们可以根据测量的质量或多或少地自信地更新模型。

我们的群集收敛于所有节点的一组网络坐标,我们希望它们可以相对准确地表示节点之间的真实网络距离。

这一切听起来都很复杂,我想是的,但是从同样的意义上讲它很复杂,TCP拥塞控制(它最初也基于物理模型)很复杂,而不是说Paxos就是这样:多数情况下不会暴露给我们,也不会牺牲有意义的性能。 Serf将Vivaldi数据潜入其成员更新中,因此我们实际上是免费获得它们。

现在,我们可以要求Serf为我们提供网络上任意两点之间的RTT:

如果您像我一样,请阅读Serf关于其Vivaldi实现的说明,并在他们说自己使用8维坐标系时有一个记录性的擦伤时刻。这些坐标可能代表什么?但是您可以通过以下方式直观地解决问题:

想象一下,网络性能完全由物理距离决定,因此,通过对RTT进行采样并更新模型,我们有效进行的工作就是概括节点在何处的物理图。然后,2D或3D坐标空间可以有效地建模网络距离。但是我们知道,除了物理距离之外,还有更多因素会影响网络距离!我们不知道它们是什么,但是它们以某种方式嵌入到我们收集的测量数据中。我们希望在坐标中有足够的尺寸,以便通过迭代和随机远离其他节点的方式来捕获确定RTT的所有因素,但并没有那么多,以至于我们收集的数据是多余的。无论如何,要有8个坐标,再加上一些(再次)优雅的注释。

HashiCorp的首席技术官Armon Dadger在维瓦尔第(Vivaldi)上发表了非常精彩的演讲,您应该只是看一下这些东西是否对您感兴趣。

坦率地说,我之所以写有关维瓦尔第的文章是因为它很简洁,而不是因为我从中获得了巨大的价值。从理论上讲,Serf的Vivaldi实施为Consul中的“近邻”指标提供了动力,根据我们的经验,这是很好的,但还不是很好。我相信相对距离和数量级。但是,除了RTT之外,理论上您还可以自己获取8D坐标,并使用它们进行更有趣的建模,例如自动创建附近或其他类似节点的集群。

Serf最后要指出的一点是:成员资格很有趣,但是如果您有成员资格协议,那么您就不会拥有消息传递系统,而Serf确实拥有其中之一。您可以将事件发送到Serf群集,并告诉您的代理对它们作出反应,还可以定义查询,这些事件会生成答复。因此,我可以这样设置Serf:

农奴代理\ -node =" $ name" \ -profile = wan \ -bind =" [$ paddr]:7777" \ -tag角色=" $ {FLY_APP_NAME}" \-标记区域=" $ {FLY_REGION}" \-事件处理程序= query:load =正常运行时间

> $ serf查询load Query' load'来自ichabod-iad-bdc999ff&#39的dispatchedAck:来自' ichabod-iad-bdc999ff'的响应:23:51:58最多1天,19:25、0个用户,平均负载:0.00, ' ichabod-yyz-3a64c0ba的0.00,0.00Ack' ichabod-ord-401dbe36的'来自' ichabod-ord-401dbe36'的响应:23:51: 47天1天,19:25,0用户,平均负载:0.00,0.00,0.00来自' ichabod-yyz-3a64c0ba'的响应:23:51:59 1天,19:26,0用户,平均负载:0.00、0.00、0.00总攻击次数:3总响应次数:3

在幕后,Serf使用逻辑时间戳可靠(但不完美)地分发了这些消息。我非常喜欢逻辑时间戳。

所以无论如何,我的意思是,在Fly的6PN设置中,您可以比DNS做得更好,可以发现服务。我的观点是,Serf确实有用,并且比Consul或Zookeeper更容易设置和运行。您可以将其烘烤到Dockerfile中,而不必理会。

另外,我的观点是,Fly是一种非常简单的方法,可以像这样随意使用分布式系统工具,您应该这样做!您可以在Fly上启动Consul集群,或者,如果您是Elixir,则可以使用Partisan而不是Serf,后者的工作原理大致相同。