作者 | Nelson Elhage
译者 | 王强
策划 | Tina
1 前 言
大家好。最近我的团队发表了我们的第一篇论文(https://transformer-circuits.pub/2021/framework/index.html),这让我感到非常兴奋。这篇文章的内容很零碎,所以我没指望 ML 领域以外有多少人会看,但我确实认为它在理解 GPT-3 之类的模型内部到底发生了什么事情这个主题上取得了一些非常好的(当然也是比较初步的!)进展。除此之外,我还发表了一篇关于 Garçon 的文章(https://transformer-circuits.pub/2021/garcon/index.html),这是我在 Anthropic 的第一批项目之一,它是为我们大部分可解释性工作提供动力的基础设施工具。
说完这些,这篇文章里我想与大家分享一些闲暇时的想法。
2 为什么 Kubernetes 这么难?
Anthropic 在 Kubernetes 中运行我们的大部分系统,因此我对这个工具有了更多经验,相当熟悉它。虽然它真的很棒,但我当然也经历过(我认为谁都有这种经历)“天呐为什么这件事如此复杂”的感受,以及“为什么调试任何东西都这么难”的情况
虽然其中一些感受在学习任何新系统时都是很常见的,但 Kubernetes 确实比我使用过的其他一些系统感觉更大、更可怕、更难处理。当我学习并使用它时,我试着去理解为什么它长成这么一个样子,以及哪些设计决策和权衡导致它长成了这个样子。我并不是说自己已经得到了完整的答案,但这篇文章会试着把我总结出来的两个想法或范式落到纸面上,这是我在试图理解为什么使用 Kubernetes 有时会让人感到如此毛骨悚然时总结出来的经验。
3 Kubernetes 是一个集群操作系统
人们很容易将 Kubernetes 视为一个用于部署容器化应用程序的系统,或者一些类似的功能描述。虽然这可能是一个有意义的观点,但我认为将 Kubernetes 视为通用集群操作系统内核会更合理一些。那么我到底是什么意思呢?这两种定义有什么区别?
传统操作系统的工作是使用单台计算机及其所有附属硬件,并公开一个程序可以用来访问该硬件的接口。虽然具体细节各不相同,但总的来说,这个界面有一些以下目标:
虽然“易于编程”通常是一个额外的目标,但在实践中它的优先级往往会输给上述目标。操作系统内核通常围绕上述目标设计,然后编写一些用户空间库以将底层、通用、高性能的各种接口包装成更易于使用的抽象。操作系统开发人员往往更关心“让 nginx 在我的操作系统上运行有多快”,而不是“我的操作系统的 nginx 端口缩短了多少行代码?”
我开始认为 Kubernetes 是位于一个非常相似的设计空间中的。然而,它不是在一台计算机上进行抽象,而是旨在抽象整个数据中心或云,或者其中的一大部分。
我觉得这个观点很有意义的原因在于,这个问题比“使人们可以在容器中部署 HTTP 应用程序”更难也更通用,它指出了 Kubernetes 如此灵活的具体原因。Kubernetes 渴望变得足够通用和强大,可以在任何类型的硬件(或 VM 实例)上部署任何类型的应用程序,而无需你“绕过”或“走出”Kubernetes 界面。我不会在这里尝试就它是否实现了该目标(或者它在实践中何时实现或没有实现该目标)发表意见;只需将它视为一个要解决的问题,我就能理解所遇到的许多设计决策,这样的视角应该是可行的。
我认为这个观点所解释的最大设计选项可能就是 Kubernetes 的可插拔性和可配置性。一般来说,我们不可能做出对每个人都适用的选项,特别是你还希望不要付出奢侈的性能成本。在现代云环境中尤其如此,因为其中部署的应用程序类型和硬件类型差异很大,并且都是变化非常快的目标。因此,如果你想解决所有人的所有烦恼,你最终需要具有极大的可配置性,这最终会创建一个强大的系统,但它可能就会难以理解,或者甚至会让“简单”的任务变得非常复杂。
另一种观点:在与我的搭档 Kate 讨论这篇文章时,我想出了另一个关于这个主题的视角:
我的感觉是,许多用户认为 Kubernetes 本质上是(或者可能希望它是)“一个 Heroku”,即作为部署应用程序的平台,抽象了大多数传统的底层操作系统和分布式系统细节。
我的论点是 Kubernetes 认为自己解决了一个更接近于“CloudFormation”的问题——从某种意义上说,它希望自己足以定义你的整个基础设施——它还试图以一种对底层云供应商或硬件通用的方式来做到这一点。
4 Kubernetes 中的一切都是一个控制回路
你可以想象一个非常必要的“集群操作系统”,就像前文所述那样,它暴露了诸如“分配 5 个 CPU 的算力”或“创建一个新的虚拟网络”之类的原语,这反过来又能支持系统内部抽象中的配置更改或调用 EC2 API(或其他底层云提供商)。
Kubernetes 作为一个核心设计决策并不是这样工作的。相反,Kubernetes 做出了的核心设计决策是所有配置都是声明性的,并且所有配置都通过充当控制回路 的“Operator”来实现:它们不断对比所需配置与现实状态,然后尝试采取行动来让现实符合理想状态。
这是一个非常深思熟虑的设计选择,并且有着充足的理由。一般来说,任何 不 设计为控制回路的系统都将不可避免地偏离所需的配置,因此,在更大规模层面,需要 有人 编写控制回路。将它们内部化后,Kubernetes 希望大多数核心控制回路只需要领域专家编写一次即可,这样就更容易在它们之上构建可靠的系统。本质而言,Kubernetes 是分布式的,并且是为构建分布式系统而设计的,所以这也是这样的一个系统的自然选择。分布式系统的定义天然允许出现 部分故障,这需要超过一定规模的系统能够自我修复并收敛到正确的状态,而不管本地故障究竟是什么情况。
然而,这种设计选择也带来了大量的复杂性和让人陷入混淆的可能(注)。举两个具体的例子:
错误被延迟
在 Kubernetes 中创建对象(例如 pod)时,通常只是在配置存储中创建一个对象,断言该对象的期望存在。如果由于资源限制(集群已满负荷)或由于对象在某些方面内部不一致(你引用的容器映像不存在)而无法真正满足该请求,那么一般来说你在创建时不会看到该错误。配置创建过程会正常完成,然后当相关 Operator 醒来并尝试实施更改时才会创建错误。
这种间接性让一切事物都更难调试和推理,因为你不能使用“创建成功”作为“结果对象存在”的良好标志。这也意味着与失败相关的日志消息或调试输出不会出现在创建对象的进程的上下文中。一个编写良好的控制器将发出一些 Kubernetes 事件来解释正在发生的事情,或者以其他方式注释有问题的对象;但是对于测试不太完善的控制器或很少发生的故障,你可能只会在控制器自己的日志中获得 logspam。并且某些更改可能涉及多个控制器,它们会独立甚至联合执行,这使得我们更难追踪到底是哪一段该死的代码失败了。
Operator 可能有问题
声明式控制回路模式提供了隐含的承诺,即你(用户)不需要担心系统 如何 从状态 A 到状态 B;你只需将状态 B 写入配置数据库,然后等待即可。当它运行良好时,这实际上大大简化了工作。
然而,有时系统不可能从状态 A 到达状态 B,即使状态 B 可以自行实现。或者也许这是可能的,但需要停机时间才行。或者它虽然是可能的,但却是一个罕见的用例,因此控制器的作者忘了实现它。对于 Kubernetes 中的核心内置原语,你可以很好地保证它们经过良好的测试和实践检验,并且应该能够很好地工作。但是当你开始添加第三方资源、管理 TLS 证书或云负载均衡器或托管数据库或外部 DNS 名称时(Kubernetes 的设计倾向于将你推向这个方向,因为它更希望成为你整个堆栈的真相来源),你会在人迹罕至的道路上徘徊不前,并且很难搞清楚所有路径是不是都经过了良好的测试。并且,与前面关于延迟错误的观点一样的是,故障模式都是很微妙的,并且出现在很远的位置;并且很难区分“尚未收到更改”和“永远不会收到更改”之间的区别。
5 结 论
在这篇文章中,我试图避免作出价值判断,评价这些设计决策到底是好是坏。我认为关于 Kubernetes 应该何时成为什么样的系统才是有意义、有价值的,以及哪些情况下更简单的系统可能就足够了之类的话题已经争论够多了。然而,知道它做出了这些决策后,我发现我对 Kubernetes 本身有了很好的理解,并且能更好地理解它的复杂性来自哪里,以及它服务的目标是什么,这是非常有价值的。
我尝试将这种分析方法应用于我使用的所有系统上。即使一个系统的设计方式在当前环境下看起来——甚至可能就是——次优的,但它之所以会设计成现在这个样子 总会是有一些原因的。对于一个你必须与之交互、推理和做出决定的系统而言,如果你能理解这些原因、动机和将系统带到今天这一步的内部逻辑,你会有更好的体验,而不是轻易放弃它。我希望这篇文章能对其他在生产中使用 Kubernetes 的新手或正在考虑采用它的人们有所帮助,希望本文提供了一些有用的框架来帮大家推理为什么(我认为)它看起来是这个样子,以及对它有哪些期望是合理的。
注:如果我们想要更细化一些,我们可能会说它 预加载 了复杂性,而非添加了复杂性(或者说在添加的同时也是预加载了复杂性)。这种设计使你可以提前处理一些你可能会长期忽略的实用特性。这是否是一个理想的选择取决于你的目标、规模、时间范围和其他相关因素。
原文链接:
https://buttondown.email/nelhage/archive/two-reasons-kubernetes-is-so-complex