第 2 章 从裸机到编排:Kubernetes 要解决的第一个问题

墨:老哥,咱们这章不碰任何 K8s 概念。我先问你一个更土的问题——你写的 Go 服务,最终是怎么跑到机器上、还跑得稳的? 你五年里踩过的每一个坑,其实就是这一章的全部内容。


2.1 先承认一个事实:部署这件事,从来都不简单

很多人以为”部署”就是 go build 然后跑二进制。但只要你服务要给人用,“跑起来”只是灾难的开始。真实世界里你要面对的是:

  • 这台机器挂了怎么办?(可用性)
  • 流量翻三倍,一台扛不住怎么办?(扩缩容)
  • 新版本有 bug,怎么快速回退?(发布与回滚)
  • 10 个服务互相调,IP 和端口谁记着?(服务发现)
  • 配置改了,是每台登上去改,还是有个地方统一管?(配置管理)
  • 一台机器上跑 5 个服务,一个吃了 100% CPU,其余四个被拖死怎么办?(隔离)

K8s 的每一个子系统,追到根上,都是在回答上面某一行。 这一章我们顺着历史走一遍:人类是怎么一步步被这些麻烦逼到”必须有个编排系统”的。


2.2 物理机时代:人肉即正义(也即是灾难)

最早的部署,简单粗暴:买(或租用)一台物理服务器,SSH 上去,编译/拷贝二进制,跑。配置写在 /etc 下,日志扔 /var/log,进程用 nohup 或 screen 兜着。

墨:你觉得这种方式最大的问题是什么?是”慢”吗?

很多人第一反应是”慢”——买机器要钱、要上架、要配网络。但慢只是表象。物理机时代真正要命的有三件事:

2.2.1 资源利用率极低(“一台机器只干一件事”的奢侈)

因为隔离手段弱(顶多靠不同用户、不同端口),为了不让服务互相污染,运维的本能是一个服务一台机器,或者”重要服务独占机器”。结果呢?一台 32 核 128G 的机器,上面跑个 Java 服务常年占用 2 核 4G,剩下 90% 在空转。云时代的账单出来,老板血压直接拉满。

这不是浪费,是被迫的浪费——因为你不隔离,A 服务内存泄漏就能把 B 服务一起拖死。

2.2.2 配置漂移(Configuration Drift)

10 台机器,你手工 vim 改了其中 8 台的 nginx 配置,另外 2 台忘了。或者你改了一台,没记文档。三个月后,你完全不知道”生产环境到底长啥样”。

📦 案例: 某团队一次故障,排查 6 小时,最后发现是某台边缘节点上 ulimit -n 被人手动改小过,重启后没生效,连接数一高就 too many open files。而”标准镜像”里这个值是对的。也就是说,生产环境的真实状态和”应该的状态”已经不一致了,而且没人知道。

这个现象有个专有名词叫 配置漂移。它是人肉运维的宿命——只要状态靠人手维护,状态就一定会和记录分叉。

2.2.3 故障转移靠人

机器挂了,告警响,人爬起来,把服务迁到别的机器,改 DNS/IP,祈祷依赖连得上。RTO(恢复时间目标)取决于你睡得有多沉。

墨:你品品这三件事的共同点。

共同点是——它们全是因为”状态靠人维护、资源靠人分配、故障靠人救”而产生的。 只要”人”是系统里那个关键的搬砖工,规模一大就崩。这不是你不够努力,是架构里没有自动化闭环。


2.3 虚拟机时代:隔离有了,但代价不小

虚拟机(VM)的出现,第一次让”一台物理机跑多个互相隔离的服务”变得正经。Hypervisor(Xen、KVM)把硬件虚拟化,每个 VM 有独立内核、独立 IP、独立文件系统。你可以把 10 个服务放 10 个 VM,哪怕一个崩了也不波及别人。

VM 解决了物理机时代最痛的隔离和密度问题。但它自己又带来了新麻烦:

2.3.1 太重了

每个 VM 要跑一整个 Guest OS 内核。一个跑着 nginx 的 VM,可能光操作系统就吃掉 500MB 内存、几十秒才启动。你要扩个容?等 VM 起来再说——启动慢 = 扩缩容慢 = 扛不住突发流量。

2.3.3.2 镜像臃肿、环境不一致仍在

VM 镜像动辄几 GB,“传个镜像”比”传个二进制”慢一个数量级。而且镜像是整个磁盘的快照,里面照样能漂——你基于一个旧镜像起了 100 台,里面某个 agent 版本其实早该升了,但快照里还是老的。

墨:所以 VM 把”隔离”做对了,但”轻、快、不可变”没解决。你觉得下一个想要的东西是什么?

想想你日常最烦 VM 的哪一点——是”我就想跑个进程,凭什么要先养个操作系统”。这就引出了容器。


2.4 容器时代:轻、快、不可变,但”单容器”管不过来

容器(以 Docker 的普及为标志)用 Linux 的 namespace + cgroup 做隔离,但不虚拟化内核——所有容器共享宿主机内核,只是各自以为自己独占了一亩三分地。

这带来三个质变:

  1. 轻:起一个容器是毫秒到秒级,内存开销就是应用本身那点;
  2. 快:镜像只是分层文件系统(overlayfs)的叠加,传输只传增量层;
  3. 不可变(Immutable):镜像一旦构建,到处跑都一样。“在我机器上能跑”这个梗,被容器基本杀死了——因为”你机器”和”生产机器”跑的是同一个镜像。

墨:听起来完美了?那 Docker 自己为什么没变成 K8s? 你先想,再往下看。

容器解决了”单个进程怎么干净地跑”,但它没有解决”一堆容器怎么协同地跑在一堆机器上”。这正是关键分水岭。

你用 docker run 起一个容器很爽。那我问你:

  • 我要在 50 台机器上各跑 2 个 nginx 容器,你怎么下命令?一个个 SSH 登?(调度/分发)
  • 某台机器挂了,它上面的容器没了,谁来在别的机器重建?(故障转移)
  • 这 100 个 nginx 前面要一个统一入口,IP 老变,调用方记哪个?(服务发现/负载均衡)
  • 我要把 100 个容器从 v1 滚动到 v2,不能全停,怎么编排?(发布)
  • 一个容器要挂一块持久盘,它漂到另一台机器,盘怎么跟过来?(存储)

你看,容器把”原子的运行单位”做漂亮了,但”原子怎么组成系统”这件事,容器引擎自己不管。 这中间的空白,就是”编排(Orchestration)“要填的。

关键定义(请你刻进脑子): 编排 = 把一堆”运行单位”(容器/Pod)自动地调度到一堆机器上,并持续保证它们处于你期望的状态,包括扩缩、故障转移、发布、网络、存储。 容器是”零件”,编排是”流水线 + 厂长”。


2.5 那”编排”之前,人们拿什么凑合?

K8s 不是第一个想填这个坑的。看看前人的方案,你就知道 K8s 赢在哪、又继承了谁的优点。

2.5.1 配置管理工具(Puppet / Chef / Ansible):它们不是编排

墨:你用过 Ansible 吧?它和 K8s 最本质的区别是什么?

很多人把 Ansible 当”穷人版 K8s”,这是错的。区别在于心智模型:

  • Ansible 是**命令式(imperative)**的:你写剧本”去这台机器,装这个包,启动这个服务”。它做了一次,就完了。它不持续盯着”现在是不是还装着想装的东西”。
  • K8s 是声明式(declarative)的:你声明”我要 3 个 nginx 副本”,至于怎么到 3 个、中途挂了怎么补回 3 个,它持续保证。

📦 案例: 你 Ansible 剧本里写了”确保 nginx 在跑”。半夜 nginx 被别人 kill 了,Ansible 不会自己跳起来重启它——除非你有个定时任务反复跑剧本。而 K8s 的控制循环,每一刻都在问”实际还是 3 个吗?不是就补”。

所以配置管理工具解决的是”怎么把一堆机器弄成某个样子”,而且只做一次;编排解决的是”怎么让一堆机器永远保持某个样子”。一字之差,天壤之别。这部分我们第 3 章会掰开揉碎讲。

2.5.2 Apache Mesos:两级调度,太底层

Mesos 是比 K8s 早的”数据中心操作系统”思路。它的亮点是两级调度(two-level scheduling):Mesos 把资源offer给上层框架(如 Marathon、Spark),框架自己决定怎么用。这很适合混部多种计算框架(批处理、长服务)。

但它的问题是”太底层、太通用”。 它给了你调度的骨架,却没给你”应用该怎么描述、怎么升级、怎么发现”的成套原语。你得在它上面自己搭一套。结果就是上手成本极高,普通团队玩不转。K8s 反过来:它先把”应用”这件事用一组对象(Pod/Deployment/Service)定义得明明白白,调度只是其中一块。

2.5.3 Docker Swarm:简单,但能力天花板低

Swarm 是 Docker 官方的编排,最大的优点是和 docker 命令一脉相承,五分钟上手。但它赢在简单,也输在简单:

  • 调度策略弱(亲和、拓扑、抢占都不够灵活);
  • 应用模型单薄(没有 StatefulSet 那种”有状态”的一等公民);
  • 扩展机制几乎为零(你没法优雅地往里塞自定义逻辑);
  • 大规模下的稳定性和社区生态被 K8s 全面碾压。

墨:你觉得”简单”作为一种产品策略,什么时候会变成死穴?

当用户的麻烦超出你的简单模型时。Swarm 适合”我就想跑几个无状态容器”,但一旦你要”有状态、要灰度、要自定义 CRD、要对接一堆存储和网络插件”,它就不够了。K8s 的设计哲学是”默认复杂,但可被驯服”——它难,是因为它要覆盖的麻烦多。

2.5.4 Borg:K8s 的精神祖先(Google 内部)

这事得讲,因为 K8s 不是拍脑袋想的,它是 Google 把内部跑了十年的 Borg 系统”开源重写了一次”。

Borg 是 Google 内部管着几十万任务的编排系统。K8s 的许多核心人物(Brendan Burns、Joe Beda、Craig McLuckie 等)就来自 Borg 团队。所以你会看到 K8s 里很多设计的”基因”:

  • Borg 里有个 BorgMaster + Borglet,对应 K8s 的 API Server + kubelet;
  • Borg 的 Job / Service 概念对应 K8s 同名物;
  • Borg 的 alloc(一组共存的容器) 直接演变成了 K8s 的 Pod。

墨:既然 Borg 已经那么强,为什么 Google 不直接开源 Borg?

因为 Borg 是和 Google 内部基础设施(尤其是它的 RPC、存储、鉴权)深度绑死的,代码根本剥不出来。所以 K8s 是”用 Borg 的经验,重新设计一套干净、可移植、不绑定 Google 内部的东西”。这给了我们一个极重要的系统设计启示:

重写的理由,往往不是旧系统不好,而是旧系统和你的新约束(要开源、要跨云、要标准化)不兼容。演化常常以”重新实现”的形式发生。

这点我们在第 18 章”版本演进”里还会反复看到。


2.6 所以 K8s 到底解决的是什么”第一个问题”?

把上面串起来,K8s 要解决的第一个问题,可以精确表述成一句:

当”运行单位(容器)“和”机器”的数量都大到人肉无法有效分配和维护时,如何用一个自动闭环,持续把”实际状态”拉向”期望状态”,并且覆盖调度、网络、存储、发布、故障转移、扩缩容?

注意这句话里三个关键词:

  1. 自动闭环:不是人做,是系统自己反复做;
  2. 持续拉向期望:不是做一次,是永远做;
  3. 覆盖一整片:不是只管调度,是把应用活的方方面面都管了。

这就是为什么 K8s 是个”平台”而不是”工具”。 工具解决一个点,平台兜住一个面。你上的不是”一个部署工具”,是”一整套应用运行的操作系统”。


2.7 一个常被忽视的视角:K8s 解决的是”组织的 scalability”

墨:你猜,K8s 最难的部分是技术,还是人?

说个反直觉的:很多公司 K8s 上得不顺,不是因为技术难,是因为组织没准备好。K8s 把”部署、运维、网络、存储”这些原本分属不同团队的决策,收敛进了一套统一的 API 和一套 YAML。这意味着:

  • 开发要开始懂一点基础设施(至少懂 requests/limits、 readiness 探针);
  • 运维的角色从”手工搬砖”变成”维护平台 + 制定策略”;
  • 原本靠”某个人脑子里的拓扑”运转的东西,必须变成”声明在 Git 里的代码”。

K8s 本质上是把”组织该怎么协作运行软件”标准化了。 这也是它赢过 Swarm/Mesos 的深层原因:它不只解决了技术编排,还顺手解决了协作编排。GitOps(把集群状态存在 Git 里,靠声明驱动)就是这股逻辑的延伸。


2.8 本章演化线小结(先埋个伏笔)

这一章里其实已经藏着一条”演化”线索,你留意:

  • 隔离方案:物理机隔离 → VM 隔离 → 容器隔离(越来越轻);
  • 状态维护:人肉 → 命令式剧本(做一次)→ 声明式闭环(永远做);
  • 调度:人分配 → Mesos 两级(太底层)→ K8s 单级但插件化(第 6 章详讲);
  • 系统来源:闭源 Borg → 重写的开源 K8s。

每一代都不是”推翻重来”,而是在上一代够不着的地方补一刀。 这个模式,全书通用。


2.9 本章思考题

🔧 思考题 1

你说”容器引擎(如 Docker)不负责编排”。那请你具体列一下:如果一个电商系统有 30 个微服务、跑在 20 台机器上、用纯 Docker(没有 K8s),你手动要维护哪些东西会在规模下崩溃?至少列 5 项。

【参考答案】 (1) 调度决策:新容器往哪 20 台里的哪台放?得自己算 CPU/内存余量,否则容易挤爆某台;(2) 故障转移:某台宕机,上面容器全没,得人工在其他机器重建并改依赖的地址;(3) 服务发现:容器 IP 随启停变,30 个服务互相调,地址注册/更新得自己搞(如 consul 或手写);(4) 发布编排:滚动更新要自己写脚本控制”起新停旧”的节奏,回滚同理;(5) 资源争抢:同机多容器无默认限额会互相拖死,得逐个设 cgroup;(6) 存储挂载:有状态容器漂走,盘要手动跟;(7) 健康检查与重启:Docker 的 --restart 只能本机重启,跨机恢复它不管。这 7 项里至少 5 项在 30×20 规模下必然让人崩溃——而这正是编排系统存在的理由。

🔧 思考题 2

“命令式做一次”和”声明式永远做”,除了重启进程,你还能举一个生活里”声明式”的例子吗?它为什么比命令式更适合”会出意外”的系统?

【参考答案】 例子:空调的恒温器 vs 你手动开关风扇。恒温器你设”26 度”(期望状态),它自己根据室温反复启停压缩机(控制循环)来维持;你手动扇风扇是”现在热了,开一下”(命令式),室温降了你忘了关就过冷,或者你睡着了它就不会再调。声明式适合”会出意外”的系统,因为意外(室温变化、进程被杀、节点宕机)是持续的、不可预测的,只有”持续观测+持续修正”的闭环能对抗,单次指令扛不住下次意外。这正好对应 K8s 控制循环的本质。

🔧 思考题 3

Mesos 的”两级调度”和 K8s 的”单级但插件化调度”,你觉得各自适合什么场景?为什么 K8s 选了后者作为主路径?

【参考答案】 两级调度(Mesos 把资源 offer 给框架,框架自己决定)适合多种异构计算框架混部——比如同一集群既要跑 Spark 批处理又要跑长驻服务,各框架按自己逻辑用资源,Mesos 只做资源仲裁。但它的代价是”应用层语义”(怎么升级、怎么发现、什么是有状态)Mesos 不管,得框架自己造,导致通用应用编排体验差。K8s 选单级调度但把调度逻辑做成可插拔的插件框架(第 6 章),是因为它的主战场是”以服务为主、且应用模型高度标准化”的云原生负载。它先把 Pod/Deployment 这套应用语义定死,调度只是”把这些标准对象放到节点”的决策,不需要把调度权下放给外部框架。代价是灵活性略低(不能像 Mesos 那样让外部框架完全自定义资源使用),但换来了极低的上手成本和极强的生态一致性——对一个要统治通用云原生场景的系统,这个权衡值。

🔧 思考题 4(进阶)

书里说”K8s 把组织协作标准化了”。如果你是一个 20 人团队的技术负责人,要推行 K8s,你会预期团队里的”开发”和”运维”各自的职责发生什么具体变化?哪些冲突最可能爆发?

【参考答案】 开发侧:从”只交付二进制”变成”交付声明(Deployment + 资源请求 + 探针 + 配置引用)“,要对自己的服务在集群里怎么活负责一部分。运维侧:从”手工部署/救火”变成”维护平台可用性 + 制定配额/网络/安全策略 + 提供脚手架”。最可能爆发的冲突:(1) 责任边界:Pod OOM 了算开发(limits 设小了)还是运维(节点不够)?容易扯皮;(2) 学习成本:开发不愿学基础设施概念,YAML 乱写导致事故;(3) 放权 vs 管控:开发想要自由(自定义 CRD、特权容器),运维要安全,RBAC 怎么收;(4) GitOps 推行阻力:把”手动改线上”变成”提 MR 改 Git”,老运维会觉得束手束脚。这些都不是技术问题,是推行前就要想清楚的治理问题。


2.10 小结

  • 物理机/VM/容器,每一代都在补上一代的隔离或轻量短板,但都没解决”一堆运行单位怎么协同”;
  • 容器把”原子运行单位”做漂亮了,留出的空白正是编排要填的;
  • 前人的方案(Ansible 命令式、Mesos 太底层、Swarm 太简单、Borg 闭源)各自有命门,K8s 继承了 Borg 的经验又重写成开放、标准化的形态;
  • K8s 解决的第一个问题本质是:用自动闭环,持续把实际状态拉向期望状态,覆盖应用运行的全生命周期;
  • 它难的深层原因之一是:它顺便把”组织怎么协作运维软件”也标准化了。

下一章,我们钻进 K8s 的灵魂——声明式 API 与控制循环。这是全书的脊椎,后面所有子系统都挂在它上面。建议你读下一章前,先想清楚:如果让你设计一个”永远把实际拉向期望”的系统,你第一个卡点会在哪?