从头开始更好的Kubernetes

2020-11-29 17:37:52

最近,我与优秀的Vallery Lancey进行了有关Kubernetes的聊天。具体来说,如果我们从头开始构建新的东西,而又不考虑与Kubernetes的兼容性,我们将采取不同的做法。我发现对话如此刺激,以至于我觉得有必要写下来,所以就在这里。

这不是完整的设计。其中某些功能可能根本无法使用,或者需要进行大量的重新设计。每个部分都是整个拼图的一个随机部分。

这些不仅仅是我的想法。我认为有些是原创的,但就像Kubernetes社区中的许多事情一样,这是集体思维的产物。我知道至少Vallery和Maisem Ali一次或一次地影响了我的思维,而我忘记了更多。如果您喜欢一个主意,那就是集体努力。如果您讨厌它,那完全是我的。

其中一些事情是两极分化的。我正在设计让我开心的东西。

我对Kubernetes的经验来自两个截然不同的地方:为裸机集群编写MetalLB,以及在GKE SRE中运行大型集群即服务。这两件事都告诉我,Kubernetes非常复杂,并且大多数尝试使用它的人都没有为市场宣传册和这些宣传册所承诺的系统之间的大量工作做好准备。

MetalLB告诉我,不可能构建与Kubernetes集成的强大软件。我认为MetalLB做得很好,但是Kubernetes仍然很容易构造损坏的配置,而调试它们也太困难了。 GKE SRE告诉我,即使是最重要的Kubernetes专家也无法安全地大规模操作Kubernetes。 (尽管GKE SRE使用提供的工具做得非常出色。)

Kubernetes是编排软件的C ++。功能强大,具有所有功能,看上去看似简单,并且会一再伤害您,直到您加入其神职人员并将生命奉献给它的奥秘为止。即便如此,配置和部署它的可能方法的矩阵仍然很大,以至于您永远都不会站稳脚跟。

继续类推,我的导游明星是Go。如果Kubernetes是C ++,那么编排系统Go会是什么样?进取心简单,固执己见,成长缓慢而谨慎,您可以在不到一周的时间里学到它,然后继续进行实际要完成的工作。

这样,我们开始吧。从Kubernetes开始,并获得完全彻底打破兼容性的许可,我该怎么办?

在Kubernetes中,豆荚在创建后大部分(但不是全部)是不可变的。如果您要更改广告连播,则不需要。制作一个新的并删除旧的。这与Kubernetes中的大多数其他事物不同,Kubernetes中的大多数事物都是可变的,可以与新规范优雅地协调一致。

因此,我将使豆荚不特别。使它们完全可读写,并像对待其他任何对象一样协调它们。

我从中得到的直接有用的东西是就地重启。如果计划约束和资源分配没有改变,您猜怎么着? SIGTERM runc,使用不同的参数重新启动runc,您已完成。现在,pod看起来像常规的旧systemd服务,必要时可以在机器之间移动。

请注意,这不需要在运行时层进行可变性。如果您更改Pod定义,终止容器并使用新配置重新启动容器仍然可以。 Pod仍会保留将其安排在此计算机上的资源预留,因此从概念上讲,它等效于systemctl restart blah.service。您可以尝试花哨的方法,并在运行时级别上实际进行一些操作的更新,但不必这样做。主要好处是解耦调度,pod生存期和运行时层的生存期。

在Pod层停留更长的时间:既然它们是可变的,那么我接下来想要做的很明显的事情就是回滚。为此,让我们保留Pod定义的旧版本,并使其“回到N版本”变得微不足道。

现在,pod更新如下所示:编写pod的更新定义,并进行更新以匹配。更新坏了吗?回写版本N-1,您就完成了。

您从中获得的好处是:无需担心GitOps,即可轻松了解集群发生的事情。如果需要的话,请一定不要说GitOps,它有好处,但是您可以回答一个基本的“什么变化?”问题仅使用群集中的数据。

这需要更多的设计。特别是,我想将外部更改(人类提交新的Pod)与机械更改(k8的某些内部更改Pod定义)分开。我还没有考虑过如何对这两种历史进行编码,并使操作员和自动化都可以访问它们。也许它也可能是完全通用的,其中“更改者”在提交新版本时会标识自己,然后您可以查询特定更改者(或排除特定更改者)以查找更改(类似于标签查询的工作原理)。同样,在那里需要更多的设计,我只知道我想要具有可访问历史记录的版本化对象。

最终我们将需要垃圾回收。就是说,对单个Pod的更改应该很好地进行delta压缩,因此我的默认设置是仅保留所有内容,直到它变成真正愚蠢的数据量为止,然后在此基础上解决问题。保留所有内容也是一种有用的适度压力,可避免系统其余部分“因一千次更改而死亡”。最好在一系列控制回路中进行更少,更有意义的更改,每个控制回路都在追求收敛的同时改变一个领域。

一旦有了这段历史,我们也可以做一些整洁的小事情。例如,节点软件可以将最新的N个版本的容器映像固定在机器上,以使回滚尽可能快。有了可访问的历史记录,您可以比“ GC已超过30天并希望”更精确地做到这一点。概括而言,所有编排软件都可以将旧版本用作各种资源的GC根目录,以加快回滚速度。回滚是结束中断的主要方式,这是非常有价值的事情。

这是一个简短的部分,基本上可以说Vallery用她的PinnedDeployment资源将其从公园中淘汰了,该资源使操作员可以通过跟踪两个部署状态版本来明确控制部署。这是由SRE设计的部署对象,对SRE在部署中需要什么有清晰的了解。我喜欢它。

这与上面的版本化就地Pod更新结合得非常好,我真的没有什么可添加的。显然,多脚架应该如何工作。要从Kubernetes受限的世界适应这一新的,不受限制的奇妙宇宙,可能需要进行一些调整,但是总体设计是完美的。

我对Kubernetes的“ API机器”位所遇到的最大问题是编排的思想,即独立控制循环的松散编排。从表面上看,这似乎是一个好主意:您有数十个小控制循环,每个控制循环专注于做一件小事情。当组合成一个集群时,它们彼此间接协作以推动状态前进并收敛于所需的最终状态。所以有什么问题?

问题是出现错误时完全不可能进行调试。 Kubernetes中典型的故障模式是您将更改提交给集群,然后重复刷新以等待聚合。如果还没有...那么,您就被搞砸了。 Kubernetes不知道“系统已成功收敛”和“控制环被束缚并阻止了其他所有事物”之间的区别。您可以希望有问题的控制循环向该对象发布一些事件以对您有所帮助,但总的来说它们并没有帮助。

此时,您唯一的选择是收集可能涉及的每个控制循环的日志,寻找被楔入的循环。如果您对所有控制循环以及每个控制循环都有深入的了解,则可以使其速度更快一些,因为这可以让您从对象的当前状态推断出哪个循环可能正在尝试立即运行。

这里要注意的关键是,复杂度已经从控制循环的设计者转移到了集群运算符。制作一个可以独立执行小动作的控制循环很容易(虽然不容易)。但是,要使用具有许多这样的控制回路的集群来操作,就需要操作员吸收所有这些回路的行为,它们之间的相互作用,并尝试推断出一个松散耦合的系统。这是一个问题,因为您必须编写并测试一次控制循环,但是要处理它及其错误多次。但是,偏向是简化只执行一次的操作。

为了解决这个问题,我希望使用systemd。它解决了类似的生命周期问题:给定当前状态和目标,您如何从A变为B?区别在于在systemd中,步骤及其相关性是明确的。您告诉systemd,您的单元是multi-user.target(又名“正常启动的快乐系统”)的必需部分,它必须在挂载文件系统之后但在联网之前运行,依此类推。您还可以依赖系统的其他具体部分,例如说只要sshd运行,您的东西就需要运行(听起来像边车,对吗?)。

这样做的最终结果是,systemd可以准确地告诉您系统的哪一部分发生故障,或者仍在运行,或者失败了前提条件。它还可以为您打印系统启动过程的图形,并对其进行分析,例如“什么是启动的长杆?”

我想偷走所有这些批发产品,然后将其放入我的集群编排系统中。它确实需要对这个新世界进行一些调整,但是粗略地说:控制循环必须声明它们对其他控制循环的依赖性,必须生成结构化日志,以便我可以轻松搜索“有关Pod X的所有控制循环活动”,并且编排系统可以处理诸如systemd之类的生命周期事件会处理向新目标单元的切换。

在实践中看起来像什么?让我们集中讨论pod的生命周期。可能我们将定义一个抽象的“运行中”目标,这是我们要结束的目标-吊舱已经开始并且很高兴。向后工作,容器运行时将添加一个在“运行”之前发生的任务,以启动容器。但它可能要到存储系统有机会设置网络安装后才能运行,因此它将在“存储”目标之后自行订购。同样对于网络,容器启动希望在“网络”目标之后发生。

现在,您的Ceph控制循环将自己安排为在“存储”目标之前运行,因为它负责启动存储。其他存储控制循环也执行相同的操作(本地绑定安装,NFS等)。请注意,这意味着它们的设置可以同时运行,因为它们都声明要在存储准备就绪之前运行,但是不在乎它们在其他运行之前还是之后运行。也许他们确实在乎!也许您编写了一个很棒的存储插件,它的功能令人赞叹,但是必须先进行NFS挂载,然后才能执行。很好,很好,在nfs-mounts步骤中添加一个依赖项,就可以完成了。像systemd一样,我们既有订购要求,又有严格的“我还需要其他东西才能完全发挥作用”的要求,因此您可以轻松地进行可选的步骤订购。

(我在这里稍微简化一下,并假设步骤不是很纠结。如果需要的话,这可以概括为更复杂的流程-但请参阅下文进一步探讨如何努力避免首先需要非常复杂的流程。)

完成此操作后,协调器可以帮助您回答“为什么我的吊舱没有启动?”您可以简单地转储pod的工作图,并查看哪些步骤已完成,哪些步骤失败,哪些仍在运行。 NFS安装已经进行了5分钟?我猜测服务器已关闭,并且控制循环缺少超时。回到关于可能的配置和状态矩阵庞大的观察:如果您提供调试它的工具,那可以。 Systemd允许您以任何顺序,任何约束向引导过程添加任意内容。但是,即使出现错误,我仍然可以对其进行故障排除,因为它的限制条件与它提供的工具相结合,使我可以从最初的原理快速理解特定的机器。

与systemd给系统启动带来的好处类似,这还使您可以尽可能积极地并行化生命周期操作,但仅此而已。而且由于工作流程图是显式的,因此可以扩展。您的集群是否在每个Pod上都有特定于公司的步骤,并且必须在生命周期的特定位置进行?为此定义一个新的中间目标,使其取决于正确的前提条件和前提条件,然后将控制循环挂接到那里。编排系统将确保您的控制循环始终处于生命周期中的正确位置。

请注意,这还解决了诸如Istio之类的怪异问题,在Istio中,它们必须将自己笨拙地注入到人类提供的定义中才能起作用。没必要!将适当的控制循环插入生命周期图中,并根据需要在内部进行调整。只要您可以向系统表示在生命周期中需要执行操作的位置,就无需考虑操作员提供的对象。

本节既长又短。这与k8s的API机制大相径庭,因此需要大量新的设计工作才能充实。特别是,主要的变化是控制循环不再简单地观察所有集群状态并竞相做任何事情,而是必须等待协调器为特定对象调用,当这些对象到达其生命周期中的正确点时。您现在可以使用注释和编程约定将其固定在k8s上,但是在将现有的东西烧成灰烬之前,清晰的可观察性和可调试性的好处并没有完全实现。

有趣的是,Kubernetes排序已经实现了其中一些想法的原型实现:初始化器和终结器实际上是发生在两个生命周期步骤的钩子之前。它使您可以将控制循环挂接到两个硬编码的“目标”上。他们将控制循环分为三个部分:初始化,“默认”和终结。这是显式工作流程图的硬编码开始。我正在争论把它推到逻辑上的结论。

上一部分的适度扩展:使对象的每个字段都由特定的控制循环显式拥有。该循环是唯一允许写入该字段的循环。如果未定义所有者,则该字段可被集群操作员写入,而不能写其他任何内容。这是由API机制(而非约定)强制执行的。

这已经是大多数情况,但是所有权是隐式的。这导致两个问题:如果字段错误,则很难弄清谁负责。而且很容易意外地运行无限期在战场上作战的“决斗控制器”。

后者是MetalLB存在的祸根,在其中它与其他负载平衡器实现方式发生了冲突。那永远不会发生。协调员应该拒绝MetalLB添加到集群中,因为与LB相关的字段将有两个所有者。

可能需要一个泄压阀,让您明确允许多个字段的拥有权,但是我将首先没有它,然后看看我们能走多远。共享所有权是代码气味和/或错误,除非另行证明。

如果您还需要显式注册控制循环读取的字段(并剔除那些不读取的字段-不作弊),这还使您可以做一些令人兴奋的事情,例如证明系统收敛(没有read-> write-> read的循环) ),或至少要考虑编排中的长杆。

我对Kubernetes联网非常熟悉,因此,这是我最想批发的部分。它看起来像是有原因的,我并不是要说没有k8s联网的地方。但是那个地方不在我的业务流程系统中。这将是一个很长的部分,所以请扎紧。

因此,对于初学者来说,让我们淘汰所有k8s网络。覆盖网络不见了。服务不见了。 CNI,走了。 kube-proxy,走了。网络插件不见了。

(顺便说一句,这就是为什么这在k8s中永远不可能发生的原因。到目前为止,有一个由生态系统组成的公司在出售网络插件,您最好相信他们不会袖手旁观,让我摆脱他们的存在的理由。在性质和软件上,所有生态系统的首要任务是确保它们的持续存在。您不能要求生态系统自我灭绝,而必须从外部引发灭绝。)

对,干净的板岩。我们有容器,它们可能确实需要与彼此和世界进行对话。做什么?

让我们为每个吊舱提供一个IPv6地址。是的,目前只有一个IPv6地址。他们来自哪里?您的LAN已经有一个/ 64(如果没有,请与该程序一起使用,我不是在这里为过去设计的),因此请从那里获取IP。您甚至都不需要执行重复的地址检测,2 ^ 64足够大,以至于滚动随机数几乎可以正常工作。我们将需要在每个节点上使用少量机器来使邻居发现工作正常进行,以便机器可以找到其他Pod的托管位置,但这很容易做到,原因如下:在LAN的其余部分,pod似乎可以在运行它的机器上。

或者,也许我们只是组成一个ULA,然后手动在每个节点上进行路由。实施起来确实很容易,而且寻址方案基本上仍然是“选择一个随机数,您就完成了”。也许只需要进行一点点设置,即可使节点到节点的路由更有效,但这一切都很简单。

令人烦恼的是,云喜欢打破基本的网络原语,因此IPAM部分可能必须保持可插拔性(在上面的工作流模型中),以便我们可以做一些事情,例如向AWS解释流量的含义。当然,使用IPv6将使其无法在GCP上运行。哈哈哈哈

无论如何,有几种方法可以使这只猫变皮,但从根本上讲,我们将在节点之间使用IPv6和基本的无聊路由。由于IPv6足够大的空间,我们可以将随机数扔在墙上然后放在顶部,因此可以在尽可能接近零的配置中处理pod <> pod连接。

如果您有更详尽的连接需求,则可以将其作为其他网络接口和无聊的可预测IPv6路由。需要保护节点到节点的通信吗?搭建Wireguard隧道,添加路由以通过Wireguard隧道推送节点IP,即可完成。编排系统不需要了解任何这些内容,除了可能在节点生命周期中添加一个小的控制环之外,这样直到隧道建立起来,它才准备就绪。

好的,所以我们有pod <> pod连接性和pod <>互联网连接性,尽管仅IPv6。我们如何将IPv4纳入其中?

首先,我们决定IPv4仅用于pod <> internet。您必须在群集中使用IPv6。

在这种限制下,我们可以通过两种方法来做到这一点。简单地说,我们可以让每个节点伪装IPv4流量,并为Pod分配一些小的rfc1918空间(所有节点上的相同空间)。这样一来,他们就可以连接到IPv4互联网,但这完全是静态的每个节点的配置,根本不需要集群可见。您甚至可以从控制平面完全隐藏IPv4内容,这只是每台机器运行时的实现细节。

我们还可以对NAT64和CLAT感到一些乐趣:将整个网络设置为仅IPv6,但使用CLAT诱使pod认为它们具有v4连接性。在Pod中,进行4到6转换,然后将流量发送到NAT64网关。

......