在过去相当长一段时间内,我自认为都是 Kubernetes 的强烈怀疑者。无论是做项目还是做初创公司,裸机始终都是我的首选,包括运行这篇博客的堆栈也同样用的是裸机(https://freeman.vc/notes/architecting-a-blog)。
堆栈是一个持续集成(CI)的工具链,主机上有 Nginx 配置。它可以处理惊人的并发负载,而且托管成本很低,每月只需 10 美元,完全可以与托管成本高出两个数量级的企业级博客平台和谐共处。
我相信,一家公司如果过早地使其架构太复杂,不仅给工程师出了难题,还会给用户带来不稳定性。
所以我倒觉得带有简单服务器的单一代码库(Monorepo)或许就够了。这时候只需要运行基本的 Docker 实例以尽量减小依赖地狱(Dependency Hell),并确保远程配置可在本地开发的机器上重现。
如果需要增加流量,可以租用功能更强大的服务器;或者部署简单的负载均衡系统,路由到几个后端设备。
虽然我正是这么做的,但最近负载均衡系统后面用于几个副业项目的一台物理服务器已满足不了我的要求——大部分时间里它是空闲的,但突发状态下则需要几十个带有 GPU 的虚拟机。
这迫使我只好采用更复杂的服务器管理解决方案。在用 Kubernetes 构建了几个月之后,我必须承认:我一天比一天更喜欢它。
第一天:努力争取
我已经在裸机上有一个完全正常运行的后端应用程序。我真要用不同的产品重新设计我堆栈的架构吗?尤其是像 Kubernetes 这样的超强解决方案?
我尝试使用实例模板和托管组来设置集群,这是相当于亚马逊 ECS 的谷歌产品。
从控制台启动基本集群相对简单,但我需要一些自定义逻辑来处理更新 SHAS 时的启动和停止。
我将其切换到一个编写成 Python 库的 GitHub Actions 管道。在研读 GCP 说明文档之后,我想出了可行的解决方案,但与此同时也发现了一些弊端。
① 大部分必要的部署代码可以用标准的谷歌客户端库函数来实现,这对于参数类型检查和面向对象非常有用。
然而,pypi 库中有一些命令未得到支持。为此,我通过代理连接到 GCP 公开的 REST Web 服务。
为什么存在这种不匹配?我不确定。在开发过程中处理两种相似但不同的 API 格式真的很容易让人混淆。
② 该部署需要在启动时提供 Docker 镜像,而实例模板支持这些镜像。但是它们并不通过 API 支持它们,而是仅在 Web 控制台或 CLI 中支持。
通过 REST 调用实现容器服务需要检查 gcloud 网络流量,并拷贝包含在实例创建负载中的 yaml 文件。
它附带一条注释,警告不得拷贝该 yaml 文件,因为变更不是语义版本控制的,可能随时有变。
③ 我开始觉得我在重新实现服务器的核心逻辑。这个问题以前肯定有人解决过成百上千次了,考虑到范围相对简单,我可以对此不屑一顾,但它多少对我还是构成了困扰。
这里边关键的阻碍因素是价格。我需要用不同的容器部署其中两个集群,这些容器需要由内部负载均衡系统作为前端。这允许主后端服务器与静态 API 端点通信并分配负载。
这两个负载均衡系统是对路由后端流量的现有负载均衡系统的补充。这意味着存储成本也开始成倍增加,因为我需要在默认配置的硬驱上留出一些缓冲区。
即使在零数据处理的情况下,测试该基础架构的费率也飙升至每天 28 美元,这对于一家有钱烧的公司来说微不足道,但对本人来说吃不消。
第二天:Kubernetes,你好吗?
很显然,谷歌裸机方法看起来并不乐观。它需要太多的手动结合,这导致成本飙升,于是我开始寻找其他的托管产品。
如果我重构这个单独的数据管理集群中的计算密集型部分,可能会将数据处理与机器学习分开来。
然而,我查看的所有产品都只提供预留的 GPU。如果我们选择硬件配置,它们会生成虚拟机或专用服务器。
如果我们有请求有待处理时,却没有动态扩展一说。虽然每美元的芯片比谷歌更好,但除此之外,它是专用计算的相同结构。对于该部署而言,空闲时间将多于使用时间,因此这行不通。
我不情愿地投入到 Kubernetes 堆栈中,从文档开始,然后用扩展逻辑的核心元素构建了一个 POC 集群。
我甚至没有专注于部署实际的应用程序。我只是优先考虑基础架构配置,这是让我亲身参与 Kubernetes 做出的设计的好方法。
第三天:晚餐费用
GCP 和 AWS 每月都为 Kubernetes 控制面板收取 70 美元左右的费用。每个托管的 Kubernetes 集群都捆绑了控制面板,因此它甚至在使用计算资源之前就为业务经营成本设定了底线。
当 GCP 增加这项费用时,引起了轩然大波,因为这导致使用单独集群的用户成本成倍增加。
虽然 Azure 是唯一的仍然拥有免费控制面板的主要提供商,但我发现其托管产品的功能比另外两家要少。所以,我就不指望供应商的控制面板永远免费了。
虽然 GCP 仍然在单个区域提供免费控制面板,但是它主要用于 beta 测试。
另外,这种单区域支持与专用服务器一样,使用裸机,虽然用户可以在任何托管设备的区域进行托管,但是如果该区域有问题(或者如果底层服务器有问题),那就不走运了。
如果要在裸机上进行多区域负载均衡,那么硬件成本就会增加一倍。
不过即使有管理费用,设置托管集群仍然比使用云特定产品自行搭建集群要便宜得多。
GCP 上的内部负载均衡系统每月花费 18 美元,推出 4 个服务来创建内部 DNS 网格将超出控制面板的成本。而我从头开始的原型只是使用了谷歌产品的简单组合,成本就接近了 18 美元。
目前我在做的是,开始在这个免费的区域集群中托管所有项目。到目前为止,它完全可靠,没有停机。
如果没有机器学习相关计算资源,平均每天的成本约为 6 美元。其中大部分(85%)用于 3 个 CPU、11.25GB 内存集群的计算、内存和存储资源。剩余的费用用于存储和 Docker 镜像托管。
第四天:了解术语的时候
我发现 Kubernetes 一个有用的心智模型是将容器编排视为针对不同原语(primitive)的操作。
一些原语包括如下:
- pod 是 Docker 容器或容器化的应用程序逻辑;
- 服务控制同一个 pod 的重复副本,因此如果一个 pod 在托管期间崩溃,可以确保冗余;
- 入站控制器定义外部连接(比如常规 Web 请求)如何流入集群、流向正确的服务;
- 节点是物理硬件,是运行 1 个以上 pod 的实际服务器;
- 集群是一组有相似特征的一个或多个节点,表明了哪些 pod 放置在哪里等信息。
还有一些操作:
- 获取(get)一个原语下可用的实例列表;
- 描述(describe)这些元素的定义,并查看当前状态;
- 查看活跃或失败对象的日志(log);
一切都围绕这些逻辑对象,这些实体中大多数都有与之关联的相同 CLI 行为:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
backend-deployment-6d985b8854-45wfr 1/1 Running 0 18h
backend-deployment-6d985b8854-g7cph 1/1 Running 0 18h
backend-deployment-6d985b8854-mqtdc 1/1 Running 0 18h
frontend-deployment-5576fb487-7xj5r 1/1 Running 0 27h
frontend-deployment-5576fb487-8dkvx 1/1 Running 0 27h
frontend-deployment-5576fb487-q6b2s 1/1 Running 0 27h
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
backend ClusterIP 10.171.351.3 <none> 80/TCP 34h
frontend ClusterIP 10.171.334.28 <none> 80/TCP 34h
kubernetes ClusterIP 10.171.310.1 <none> 443/TCP 4d23h
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
正因如此,所以每个命令将根据逻辑对象来显示不同的元数据。
如果检查 pod 的话,将会显示运行中 Docker 容器的版本和运行状况;部署动作将显示镜像的当前冗余和集群的健康状况;所谓服务,将为其他服务提供访问内部负载均衡系统的 DNS 地址。
这种逻辑对象方法的优点在于,它使 API 发现变得可访问。我们只要记住操作(get、describe)和对象(pods、服务),就可以避免组合爆炸。
Kubernetes 提供的原语对服务器如何运行有自己的观点。比如说,pod 和服务有单独的概念,因为服务提供了跨多个 pod 的某种内置负载均衡。
我们可以使用单独的 pod 轻松编写此逻辑,并探测本地 Kubernetes API,寻找同一组中 pod 的 IP 地址。但是因为服务被广泛使用,因此 Kubernetes 将其重构为单独的对象类型。
一般来说,抽象是最常见的服务器功能上的小插件(shim),它可以:运行进程、接受入站连接、运行守护进程。
这使我们可以轻松地在新工具中执行已经在执行的操作。虽然这些术语学起来有点费劲,但核心概念学起来并不费劲。
第五天:结识朋友
我最欣赏 Kubernetes 的地方是,它抽象了跨云主机服务器的概念。无论我们使用 GCP、AWS 还是 Azure 进行托管,都将拥有想要运行容器(pod)和偶尔一次性脚本(作业)的原始计算(节点)。
尤其是对托管 Kubernetes 配置而言,云提供商负责编写 Kubernetes 逻辑对象和物理硬件之间的转换层。
如果我们想启动新设备,只需要为集群中的新节点推送 helm 配置,而云提供商将负责余下的工作。
虽然这不会使云迁移完全顺畅无阻,但确实可以将云托管视为一种更大众化的实用服务,避免因为编写自定义云集成代码而被某个供应商锁定。
一切都归结为 Kubernetes API。根据我的经验,无论我们的系统是什么规模,这个接口层都处于完美的抽象级别。
我们不必担心本身引导磁盘或机器的底层管理实用程序。API 遵循明确的弃用计划,这将使我们可以可靠地将业务逻辑集成到更大的管道中,比如在 CI 中推送部署变更,通过计划任务启动节点等。
一切都是程序化的。连 kubectl(本地集群管理实用程序)都是集群内托管 API 上的抽象层。
这意味着我们可以编写任何可以手动执行的操作,只要可以对其进行编程,就可以将其自动化。
我之前管理的服务器已经有大约 95% 实现了自动化。有一个主要的 bash 脚本可以完成大部分环境设置,不过尽管如此,我仍然需要做一些手动工作,比如更新 Nginx 文件系统配置、配置 iptables 等。
由于每个 Linux 版本都略有不同,因此定期升级底层操作系统时,需要更改该脚本。
API 让我们的操作手册(runbook)变得完全程序化。无需编写说明文档对已知问题进行分类或对集群运行命令,就可以让工程师/SRE 程师随叫随到并遵循程序指南。
一个常见的需求是检查 git 分支是否已成功部署,如果我们要用 sha 标记 Docker 镜像,简单的方法是检查 sha 是否一样。
这通常需要一番远程 ssh/Docker 处理,或者在某个 API 端点中公开托管的 sha。可以自动化吗?当然。很简单?不是很简单。
相反,Kubernetes API 使得编写这样的逻辑、并将它们捆绑到可安装的软包中变得极其容易。这使得 on-call 常见分类操作可以被发现,并引导用户自行完成命令。
这是我的实用程序中的一个例子:
$ on-call
Usage: on-call [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
check-health Check service health
bootstrap-database Run initial database setup
...
$ on-call check-health --app frontend
1. Checking uptime (`yes` to confirm):
> yes
The application has been stable for 465 minutes.
2. Checking sha matches (`yes` to confirm):
> yes
Latest github sha: 86597fa
Latest deployed sha: 8d3f42e
Mismatching shas...
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
第六天:劳动专业化
从架构上来说,我仍然认为单体架构是可行的方法,但那是另一个话题。
就连微服务拥趸也会承认,有时我们需要将服务绑定到底层计算,这在 GPU 上进行硬件加速的机器学习发行版中尤其如此。
我们通常只能为每个硬件设备安装一个 GPU 集群,因此约束存在于节点层面,而不是针对 pod。
结合污点(taint)和容差,很容易将一个 pod 分配给独立的硬件实例。默认情况下,pod 将放置在任何有适当内存和 CPU 空间的地方。
我将污点视为油和水,因为它们与默认的 pod 分配不相容。而容差更像油和油,它们可以允许 pod 在带有污点的那些节点上启动。以正确的方式配置污点和容差可以将 1 个 pod 锁定到 1 个节点。
这让我们完全回到了裸机托管一项服务的范式中。在极端情况下,它让我们可以只使用 Kubernetes 作为连接服务的负载均衡层和管理 API 的抽象层。我们可以更轻松地保留单体架构,并委派其他任务。
结语
我仍然看好裸机和功能强大的单体架构,但是在初步了解 Kubernetes 之后,我很想说,当我们需要多个服务器协同工作时,就应该使用 Kubernetes。
我们始终都要对 Kubernetes 进行管理,因为这不可避免地带来可靠性问题。但担心 Kubernetes 大材小用的想法似乎是多余的,因为编写微服务架构方面的更底层问题才是值得担忧的关键所在。
不过如果将两者分开来,或许我们可以获得两全其美的效果。一个是简单的代码库,优先考虑功能交付;另一个是灵活但可编程控制的硬件架构。
Kubernetes 可以比我预期的更轻松地融入后台,这让我们可以专注于交付主要的价值:我们的应用程序。
标题:Falling for Kubernetes
作者:Pierce Freeman,译者 | 布加迪