这几年云原生的热度久高不下,许多大厂纷纷拥抱云原生。提到云原生,不少开发者可能会想到Kubernetes,也称为K8s,是一个用于自动部署、扩展和管理“容器化(containerized)应用程序”的开源系统。作为云原生的重要代表之一,它真的很不错。但也有不少开发者抱怨Kubernetes太复杂了。
近日,在国外知名的技术论坛网站Hacker News上,就有一位用户对Kubernetes为什么这么复杂做出了自己的见解。
为什么Kubernetes这么复杂?
Kubernetes相比其他系统来说,确实要更大,更复杂。相信不少人在使用它的过程中都曾试图了解它为什么这样。该用户也是这样,并将自己的理解写了出来。
Kubernetes是一个集群操作系统
Kubernetes更像是一个通用的集群操作系统内核。传统操作系统的工作是将一台计算机及其所有相关硬件的接口暴露出来,让应用程序可以访问这些接口。具体细节我们不明确,但这些接口都有相应的设计目标。
-
资源共享——将一台物理计算机的资源细分给多个程序,使它们在某种程度上相互隔离;
-
可移植性——在一定程度上抽象出底层硬件的精确细节,这样同一程序就可以在不同的硬件上运行而无需修改,或者只需稍加修改;
-
通用性——当有新的硬件插入计算机时,能够以渐进的方式将这些硬件纳入抽象和接口,最好是在不大幅改变任何接口或破坏任何不使用该硬件的现有软件的情况下。
-
整体性——与通用性相关,操作系统能够调解对硬件的所有访问:软件应该很少或者不可能完全绕过操作系统的内核。软件可以使用操作系统内核来建立与硬件的直接连接,从而使未来的交互直接发生(例如建立一个内存映射的命令管道),但最初的分配和配置仍然在操作系统的监督之下。
-
性能——与 "直接编写一个特殊用途的软件,直接在硬件上运行,并对硬件有独占的直接访问权(如unikernel)”相比,希望拥有这种一个可接受的小的性能成本。在某些情况下,通过提供像I/O调度器或缓存层这样的优化,在实践中达到比这样的系统更高的性能。
操作系统内核通常是围绕上述目标设计,然后编写用户空间库,将低级的、通用的、高性能的接口包装成更容易使用的抽象概念。操作系统开发者往往更关心的是怎么让应用运行的更快,而不是应用在我的系统上运行时代码更少。
Kubernetes与上述设计理念非常类似,它的目标是抽象出一整个数据中心或云。这个观点有助于理解Kubernetes。它指出了Kubernetes为什么非常灵活。Kubernetes希望自己能拥有普遍性并获得更强大的功能,它能够在任何类型的硬件或虚拟机实例上部署任何类型的应用程序。并且还不需要脱离Kubernetes的界面。不论它是否真的能实现这一目标,这样的设计都很有意义。
上述视角所解释的设计选择是Kubernetes的可插拔性和可配置性。一般来说,在不付出奢侈的性能成本下,做不到对所有人都适用的选择。在现代云环境中,应用程序的类型和部署的硬件类型有很大不同,尤其是要求可以在不同位置快速部署时。这也就意味着,如果一个系统想让所有人都适用,它就需要强大的快速配置性能。做到这一点确实会搭建出一个强大的系统,但缺点就是它会变得非常复杂。
许多用户认为Kubernetes本质上是一个“Heroku”,即作为一个部署应用程序的平台,去抽象出大多数传统的底层操作系统和分布式系统的细节。Kubernetes认为自己解决的问题更接近于 "CloudFormation",在这个意义上,它希望足以定义整个基础设施,它还试图以一种在底层云提供商或硬件上都通用的方式做到这一点。
Kubernetes中的一切是一个控制循环
想象一个非常必要的 "集群操作系统",就像上面描述的那样,它暴露了 "分配5个CPU的计算量 " 或 "创建一个新的虚拟网络 "这样的基元,这些基元反过来又支持系统内部抽象的配置变化或对EC2 API(或其他基础云提供商)的调用。
但Kubernetes并非是这样进行工作的,相反,Kubernetes的核心设计决定了所有的配置都是声明性的,并且都是通过作为控制循环"操作者 "的方式实现。他们不断地将期望的配置与现实的状态进行比较,并修改现实状态,达到与期望状态一致。
这是一个非常慎重且理由充分的设计抉择。一般来说,任何没有被设计成控制循环的系统都将不可避免地偏离期望配置,因此,需要有人来编写控制循环并通过内部化来进行控制。Kubernetes希望能让大多数核心控制环路只写一次,而且是由领域专家来写,从而使在其上构建可靠的系统变得更加容易。这也是一个系统的自然选择,因为它的本质是分布式的,而且是为构建分布式系统而设计的。分布式系统的决定性性质是排除部分可能性的故障,这就要求超过一定规模的系统能够自我修复,并收敛于正确的状态,而不考虑局部故障。
然而,这种设计也带来了系统的复杂性和一定几率的混乱。挑两个具体的例子。
第一:错误延迟, 在Kubernetes中创建一个对象(例如一个pod),这只是在配置存储中创建一个对象,断言该对象的预期存在。如果由于资源限制(集群的容量),或者由于对象在某些方面内部不一致(比如引用的容器镜像不存在),系统在实际分配上不可能满足该请求,但用户在创建时无法看到系统的实际情况。事实上,只有当开发者要修改创建对象时,系统才会产生错误提示。
这种情况使得一切都更难调试和推理,因为你不能用"创建成功 "作为 "结果对象存在 "的一个速记。这也意味着,与失败有关的日志信息或调试输出信息不会出现在创建对象的进程中。一个代码完整,功能强大的控制器,系统会解释正在发生的事情,或以其他方式注释有问题的对象;但对于较差的控制器,控制器的日志中只能找到日志垃圾。而且有些变化可能涉及到多个控制器,它们有时独立行动,有时联合行动,这就很难去追踪发生故障的代码。
声明式的控制循环模式提供了一个隐含的承诺:用户不需要担心如何从状态A到状态B,只需要把状态B写进配置数据库,然后等待。当它的代码运行良好时,从状态A自然就进入到状态B了。这是一个巨大的简化。
但有时也会失误,无法或需要等待很长时间从状态A到状态B,即使状态B本身可以实现。这是一个罕见的例子,控制器的作者可能忘记实现它了。Kubernetes中的核心内置基元经过很多测试和使用,以此来一直保持正常工作。但当用户开始添加第三方资源,比如以管理TLS证书、云负载均衡器、托管数据库或外部DNS名称等去运行系统时,程序就会偏离轨道,变得不能清楚的知道路径是怎么经过测试的。这个故障模式和延迟错误一样微妙。很难区分“变化被接受”和“变化永远不会被接受”的区别
以上就是来自Hacker News的博主分享的他对Kubernetes为什么这么复杂的看法。该博主认为,对Kubernetes本身、其复杂性以及对其服务的目标有个很好地理解,是一件非常有意义的事。希望这篇文章对刚开始使用Kubernetes的人能有一定帮助。
参考链接:
https://buttondown.email/nelhage/archive/two-reasons-kubernetes-is-so-complex/
文章评论