作者 | matdevdug
译者 | 平川
策划 | Tina
本文最初发布于 matduggan.com。
大约在 2012 到 2013 年,在系统管理员社区,我开始经常听到一种名为 “Borg” 的技术。显然,它是谷歌内部的一种 Linux 容器系统,运行着他们所有的服务。这个术语有点令人困惑,具有“单元格”的集群中有名为 “Borglet” 的东西,但一些基本要素开始传播开来。一个是“服务”的概念,应用程序可以使用服务来响应用户请求,一个是“作业”的概念,应用程序可以使用作业来完成运行时间更长的批处理作业。
然后在 2014 年 6 月 7 日,Kubernetes 第一次提交。这个希腊词语是“舵手”的意思,在最初的三年里,几乎没人能读对。
很快,微软、RedHat、IBM、Docker 加入了 Kubernetes 社区,似乎使 Kubernetes 从一个有趣的谷歌项目变成了“一个真正的产品”。2015 年 7 月 21 日,我们迎来了 Kubernetes v1.0 版本以及云原生计算基金会(CNCF)。
在初次提交之后的十年里,Kubernetes 成为我职业生涯的一个重要组成部分。在家里,在工作中,在业余项目里,只要有必要,我就会使用它。这是一个学习曲线陡峭的工具,但也是一个巨大的力量倍增器。我们不再管理服务器级别的基础设施;一切都是声明性、可扩展、可恢复的,如果你幸运的话,还是自我修复的。
但这个过程并非没有问题。一些常见的趋势出现了,在一些 Kubernetes 没有严格要求的地方出现了错误或配置错误。即使在十年后,我们看到,生态系统内部仍然有很多变动,人们还会踩到已经记录在案的地雷。那么,根据我们现在已经了解的情况,我们能做些什么改变,使这个伟大的工具更适用于更多的人和问题?
k8s 做对了什么?
让我们从积极的方面开始。为什么到现在我们还在谈论这个平台?
大规模容器
对于软件开发,容器是一个非常有意义的工具。它帮助开发者摆脱了个人笔记本电脑配置的混乱,提供了一个适用于整个技术栈的用完即弃的标准概念。虽然像 Docker Compose 这样的工具可以实现一些容器的部署,但它们很笨重,很多步骤仍然需要管理员来处理。我设置了一个 Compose 栈和一个部署脚本,它会从负载均衡器中移除实例,拉取新容器,确保它们已经启动,然后重新添加到 LB,很多人都是这样做的。
K8s 允许横向扩展,也就是可以将笔记本电脑上的容器部署到成千上万的服务器上。这种灵活性使组织能够重新审视他们的整体设计策略,放弃单体应用,采用更灵活(通常也更复杂)的微服务设计。
低维护成本
如果将运维的历史看作是一种“从宠物到牲畜的命名时间线”,那么让我们从我亲切地称之为“辛普森”的时代开始。服务器是由团队配置好的裸机盒子,它们通常都有一个独一无二的名字,在团队内部成了一个俚语。服务器运行的时间越长,积累的垃圾就越多,甚至连重启都成了可怕的操作,更不用说尝试重建了。我称之为“辛普森”时代,因为在我当时工作的职位中,用辛普森一家的角色命名服务器的情况出奇地常见。没有什么可以自我修复,一切都是手动操作。
然后我们进入了“01 时代”。像 Puppet 和 Ansible 这样的工具变得司空见惯,服务器更是用完即弃,你开始看到,堡垒机和其他访问控制系统成为常态。服务器并不都是面向互联网的,它们在负载均衡器后面,我们已经放弃了像 “app01” 或 “vpn02” 这样的可爱的名字。组织在设计时就允许它可以偶尔丢失一些服务器。然而,故障仍然不是自我修复的,仍然需要有人 SSH 进去看看哪里坏了,在工具中编写一个修复方案,然后在整个机群中部署它。操作系统升级仍然是件复杂的事。
现在,我们处于“UUID 时代”。服务器的存在是为了运行容器,它们完全是用完即弃的概念。没有人关心特定版本的操作系统支持多久,你只需要准备一个新的 AMI 并替换整个机器。K8s 不是唯一能使这一点成为可能的技术,但它加速了这一点的实现。现在,通过一个带有 SSH 密钥的堡垒服务器去修复底层服务器的问题,这种想法更多地被视为“破窗”解决方案。几乎所有的解决方案都是“销毁那个节点,让 k8s 按需重新组织,新建一个节点”。
很多过去至关重要的 Linux 技能现在大多是锦上添花,而不是必需的。你可以为此感到高兴或悲伤,无疑,我经常在这两种情绪之间切换,但事实就是这样。
运行作业
K8s 的作业系统并不完美,但与过去几年中非常常见的 “snowflake cron01 box”,它还是要好得多。无论是按照 cron 计划运行还是从消息队列运行,现在它都可以可靠地将作业放入队列,运行它们,并在它们出现问题时重启,你的生活并不会受到影响。
这不仅将人类从耗时且无聊的任务中解放出来,而且还能更有效地利用资源。你仍然需要为队列中的每个项目启动一个 pod,但团队在 “pod” 概念内部有很多灵活性,他们可以确定需要运行什么以及如何运行。这确实提高了很多人的生活质量,包括我自己,我们只需要轻松地将任务置于后台,就不用再考虑它们了。
服务可发现性和负载均衡
多年来,IP 地址硬编码一直困扰着我,它们存在于应用程序内部,作为请求路由目标的模板。如果你够幸运的话,这些依赖关系是基于 DNS 条目而不是 IP 地址的,那么你就可以更改 DNS 条目背后的内容,而不必协调上百万应用程序的部署。
K8s 允许使用简单的 DNS 名称调用其他服务。它消除了一大类错误和麻烦,简化了整个过程。有了服务 API,你就有了一个稳定的、长期存在的 IP 和主机名,你只需指向它们,而不需要考虑任何底层概念。你甚至还有像 ExternalName 这样的概念,允许你将外部服务和集群的内部服务一样看待。
我会在 Kubernetes 2.0 中加入什么?
放弃 YAML,使用 HCL
YAML 之所以吸引人,是因为它既不是 JSON 也不是 XML,这就像说你的新车很棒,因为它既不是马也不是独轮车。它在 k8s 中的演示效果更好,在 repo 中看起来也更美观,而且还能给人一种文件格式简单的错觉。实际上,对于我们尝试用 k8s 来做的事情来说,YAML 太过复杂,而且还不是一个足够安全的格式。缩进容易出错,文件不易扩展(你真的不想要一个超长的 YAML 文件),调试可能很烦人。YAML 规范中有很多不易察觉的行为。
我仍然记得,我第一次看到挪威问题(Norway Problem)时都不敢相信自己的眼睛。你可能比较幸运,没有处理过这个问题,所谓 YAML 的挪威问题是指 'NO' 被解释为 false。想象一下,向你的挪威同事解释,他们的整个国家在你的配置文件中被评估为 false。再加上由于缺少引号而意外出现的数字,问题不胜枚举。有关 YAML 的愚蠢,这里 有篇文章比我写得好。
为什么选择 HCL?
HCL 已经是 Terraform 的格式,所以至少,我们只需要讨厌一种配置语言,而不是两种。HCL 是强类型的,有显式类型。它有良好的验证机制。其设计初衷就是用来做我们要求 YAML 做的事,而且比较容易阅读。它有内置函数,人们已经在使用,这有助于我们从 YAML 工作流程中移除一些第三方工具。
我敢打赌,今天已经有 30% 的 Kubernetes 集群通过 Terraform 使用 HCL 进行管理。我们不需要 Terraform 部分就能从这门更优越的配置语言中获得很多好处。
唯一的缺点是 HCL 比 YAML 更冗长一点,在集成到像 Kubernetes 这样的 Apache 2.0 项目中时,它的 Mozilla 公共许可证 2.0(MPL-2.0)需要仔细的法律审查。然而,对于它能提供的生活质量改善,这些是值得克服的障碍。
为什么 HCL 更好?
让我们来看一个简单的 YAML 文件。
即使在最基本的示例中,也到处都是陷阱。HCL 和类型系统会捕捉到所有这些问题。
像这样的 YAML 文件,你的 k8s 软件仓库中可能有 6000 个。
现在看看 HCL,它不需要外部工具。
HCL 可提供以下好处:
类型安全:在部署前防止类型相关的错误
变量和引用:减少重复并提高可维护性
函数和表达式:实现配置动态生成
条件逻辑:支持特定于环境的配置
循环和迭代:简化重复配置
更好的注释:改进文档和可读性
模块化:实现配置组件的重用
验证:防止无效配置
数据转换:支持复杂数据操作
允许 etcd 替换
我知道,我是第 1 万个写文章讨论它的人。Etcd 做得很好,但要说它是唯一一个适合这项工作的工具,多少有点疯狂。对于比较小的集群或硬件配置,它占用了大量的资源,而你的节点数量永远不足以发挥这种集群类型的优势。现在,k8s 和 etcd 之间的关系也很奇怪,基本上,k8s 是 etcd 仅剩的客户了。
我建议将 kine 的工作正式化。对于项目的长期健康来说,能够接入更多的后端是有好处的,增加这种抽象意味着未来更容易替换成新的 / 不同的后端,并且还允许根据部署的硬件进行有针对性的优化。
我觉得,这个最终看起来会像这个样子: https://github.com/canonical/k8s-dqlite。内存模式的分布式 SQlite ,支持 Raft 共识,几乎不需要升级工作,使集群操作员在 k8s 安装的持久层有更多的灵活性。如果你在数据中心有一个传统的服务器设置,并且 etcd 资源使用不是问题,那就太好了!但这能使低端 k8s 有更好的体验,并(有望)减少对 etcd 项目的依赖。
超越 Helm:原生包管理器
Helm 是一个临时解决方案成长为永久依赖的完美示例。这原本一个黑客马拉松项目,感谢 Helm 维护者们的辛勤工作,将其变成了将软件安装到 k8s 集群的惯常做法。在没有比较深入地集成 k8s 的情况下,它已经做得足够好了。
话虽如此,Helm 很难用。Go 模板很难调试,其中通常包含复杂的逻辑,会导致非常令人困惑的错误场景。通常,这些场景提供的错误消息很不明确。Helm 并不是一个非常好的包系统,因为你需要它做的一些基本任务(传递依赖和解决依赖冲突),它做得并不好。
我是什么意思?
告诉我这个条件逻辑在做什么: