这门课会从一个能运行的应用开始,逐步把它交给 Kubernetes 管理。贯穿全课的项目叫“TaskBoard 云原生任务板”:它提供 HTTP API,用 Redis 保存任务,最终会拥有稳定入口、持久化数据、健康检查、扩缩容、安全边界和可重复的部署清单。
这一节先把全景搭起来。我们暂时不执行命令,而是回答三个问题:Kubernetes 接管了什么工作,一份 YAML 为什么能驱动集群,以及后面每个对象会怎样进入 TaskBoard。
进入本章时,我们手里只有一个抽象需求:让 TaskBoard 长期、稳定地提供服务。还没有集群、镜像或 YAML,也不假设你已经理解容器。完成本章后,你应当能从“Linux 中运行的一个进程”一直解释到“控制器持续维护的一组 Kubernetes 对象”,并知道后续每次实操究竟在这条链路的哪一层工作。
这一步不能省。若直接从 kubectl apply 开始,命令也许能运行,但一遇到 Pod 重启、地址变化或镜像拉取失败,就容易把 Kubernetes 当成一台行为古怪的远程服务器。先建立对象、状态和控制循环的模型,后面的输出才有含义。
在操作系统看来,Python、Redis、Nginx 或 Java 服务首先都是进程。进程执行指令,申请内存,打开文件和网络端口。最朴素的部署方式,是登录一台服务器,复制程序,安装依赖,然后启动进程。
这种方法在对象很少时并没有错。问题出在规模和变化:同一台服务器上的两个程序可能要求不同版本的依赖;一个进程可能耗尽内存,拖累其他程序;程序发布后留下的文件和配置很难逐项还原;换一台服务器时,又要重新回答“到底安装过什么”。我们需要的不是一个更长的启动脚本,而是清楚的隔离边界和可重复的交付物。
Linux namespace 可以让一组进程看到与其他进程不同的系统视图。例如 PID namespace 让进程看到独立的进程编号空间,mount namespace 隔离挂载点,network namespace 隔离网卡、路由和端口。容器中的进程看到自己像在一套独立系统里运行,宿主系统却仍把它当作普通 Linux 进程调度。
这里的“隔离”不能理解为创建了一台新的物理机器。namespace 主要改变进程能看见的资源视图,所有进程仍共享同一个 Linux 内核。Linux man-pages 项目 对这类用户空间可见的内核接口有正式说明,其中 namespaces(7) 进一步区分了各类 namespace 隔离的全局资源。
只隔离视图还不够。如果一个进程仍能占满全部 CPU 或内存,其他程序照样会受影响。cgroup 把进程组织成层级,并让内核控制和统计这些进程使用的资源。Linux 内核的 cgroup v2 文档 将它定义为按层级组织进程,并受控分配系统资源的机制。
可以用一句不严格但好记的话区分两者:namespace 回答“这个进程看见哪个世界”,cgroup 回答“这个进程能从世界里拿走多少资源”。实际的容器还会组合文件系统层、权限能力、系统调用限制等机制。容器工具把这些底层能力封装成一致的创建和运行接口。
容器不是轻量虚拟机的同义词。虚拟机通常虚拟硬件并运行自己的内核;Linux 容器隔离的是进程,通常共享宿主内核。两者都能形成边界,但边界的位置、启动成本和安全模型不同。
如果容器只是一次受隔离的进程,镜像就是启动这次进程所需的只读模板。它包含应用文件、用户空间依赖和默认启动配置,并由内容层组成。镜像不会包含一个正在运行的 Python 进程;容器运行时根据镜像创建可写层、网络和进程,才得到容器实例。
这解释了课程为什么先构建 taskboard:1.0.0,再运行它。源码回答“程序逻辑是什么”,Dockerfile 回答“怎样形成交付物”,镜像回答“交付的确切内容是什么”,容器回答“这份交付物正在怎样运行”。四者不是可以随意互换的词。

到这里,我们解决了单个应用如何封装和隔离的问题,却还没有解决长期运行。接下来才是 Kubernetes 出场的原因。
容器把应用代码、运行时和依赖放进一个可重复交付的镜像。启动镜像后得到容器,应用进程就在容器的文件系统、网络和进程空间中运行。
只运行一个容器时,人工操作还能应付:启动进程、映射端口、查看日志,出错后再重启。应用进入长期运行阶段,问题会变多:
Kubernetes 处理的是这些跨容器、跨节点的协调工作。它不会替我们编写业务代码,也不会自动判断业务逻辑是否正确。我们仍然要提供镜像、配置和验收标准,Kubernetes 负责让声明的运行状态持续成立。
把这段职责边界说得再具体一些:Kubernetes 能观察“Pod 的容器进程退出了”,却不知道“TaskBoard 把任务标题保存错了”;它能根据就绪探针暂时摘掉一个副本,却不知道你的 /readyz 是否真的代表业务可以接收流量。平台负责执行机制,应用和运维人员负责给出正确意图。课程后面每次配置探针、资源请求或选择器,其实都是在完善这份意图。
容器解决应用如何被打包和隔离的问题,Kubernetes 解决多个容器如何被部署、连接、更新和恢复的问题。两者处在不同层次。
Kubernetes 可以运行单体 Web 应用、后台任务、定时任务、数据服务和分布式系统。是否拆成微服务是应用架构选择,不是使用 Kubernetes 的前提。
TaskBoard 会保留两个清楚的进程边界:任务 API 处理请求,Redis 保存任务。这个规模足以讲清网络、配置和持久化,又不会让业务代码盖过 Kubernetes 本身。
脚本擅长描述一次操作顺序:先启动 A,再等待五秒,然后启动 B。可在长期运行的系统中,故障发生的时间和顺序并不固定。节点可能在发布中途失联,旧实例也可能尚未退出,新实例的就绪检查又可能失败。脚本必须穷举大量分支,执行中断后还要判断从哪里恢复。
控制器采用不同思路:反复读取当前状态,再把它推进到期望状态。无论偏差来自手工删除、节点故障还是一次未完成的更新,下一轮控制循环都从当前事实出发。这样的操作更容易重复,也更适合面对持续变化。
不过,“更适合”不等于“任何应用都应立刻迁移”。只有一个低流量进程、停机成本很低、由少数人维护时,虚拟机加进程管理器可能更简单。Kubernetes 引入 API、网络、存储、权限和控制器等新层次。选择它,应当因为确实需要一致部署、自愈、弹性、滚动发布或跨节点调度,而不是因为它流行。
Kubernetes 并不是从一份 YAML 规范凭空产生的。它出现之前,Linux 已经提供进程隔离和资源控制,容器镜像又让应用交付更一致;真正缺少的是怎样在一组机器上持续管理大量容器。
Google 很早就要在大型集群中同时运行在线服务和批处理任务。内部系统 Borg 负责接纳作业、选择机器、启动任务、维持可用性并提高资源利用率。Google Research 的 Borg 论文 介绍了它在多个大型集群中管理大量作业的经验,也明确提到声明式作业描述、名称服务、监控和故障恢复等能力。
Borg 给后来的系统留下了几条重要经验:用户应描述作业需要的状态,而不是手工挑机器;长期服务需要稳定名称,不能依赖某个实例地址;调度器要考虑资源和故障相关性;系统必须假设机器和任务会失败。你会在 Kubernetes 的 Deployment、Service、资源请求和控制循环中看到相似问题,但不能因此把 Kubernetes 简化成“开源版 Borg”。两者的 API、实现和开放生态并不相同。
随着集群和调度需求变复杂,单体调度器也会遇到扩展和迭代速度问题。Google Research 在 2013 年发表的 Omega 论文 探索了多个调度器围绕共享集群状态并行工作,并用乐观并发控制处理冲突。
Omega 不是今天运行 Kubernetes 必须安装的组件。它的价值在于展示了另一条演化线:集群状态可以成为多个独立控制过程协调的中心,而不是把所有决策塞进一个巨型程序。Kubernetes 里 API 对象和多个控制器围绕共享状态工作,也能从这段工程经验中找到来路。
Google Research 后来在 《Borg, Omega, and Kubernetes》 中专门比较了三套系统积累的经验。历史脉络告诉我们,Kubernetes 的关键不是“能启动容器”这一件事,而是把调度、声明式配置、服务发现和控制循环组合成开放的集群 API。
Kubernetes 官方历史文章记载,原型工作在 2013 年末启动,项目于 2014 年 6 月对外发布;经过约一年迭代,Kubernetes 1.0 在 2015 年发布。可参考 Kubernetes 官方博客的 项目历史回顾。
这里要把“宣布捐赠”和“正式接收”两个时间点分开。2015 年 Kubernetes 1.0 发布和 CNCF 成立时,Google 已宣布把 Kubernetes 捐赠给新基金会,Kubernetes 是基金会计划接收的种子技术;CNCF 技术监督委员会随后在 2016 年 3 月 10 日正式接受 Kubernetes 为第一个托管项目,并宣布将项目知识产权转移给基金会。Kubernetes 的 官方历史回顾、CNCF 的 正式接受公告 和 项目旅程资料 分别记录了这两个阶段。把项目放入基金会治理,意味着 API、实现和发展方向不再只是某一家公司的内部平台决策,云厂商和使用者可以围绕共同的上游项目协作。
这段历史与学习方式直接相关。我们今天使用 apiVersion、控制器和标准对象,不是在背一套偶然形成的命令;它们是长期集群管理问题逐步沉淀出的接口。后面遇到复杂概念时,先问“它替谁持续处理了哪类变化”,通常比先记字段更有效。
课程讲历史不是为了背年份。我们真正要得到三个因果关系:进程隔离催生可交付容器,容器规模扩大催生编排需求,大型集群经验推动了声明式 API 与控制循环。
Kubernetes API 中的 Pod、Deployment 和 Service 都是对象。对象通常由 YAML 清单描述,其中包含身份、期望状态和对象之间的关联。
以后我们会提交类似这样的意图:
运行 taskboard:1.0.0 镜像
保持两个 API 副本
每个副本监听 8080 端口
只把已经就绪的副本接入流量这段描述没有规定“先在节点 A 启动,再去节点 B 检查进程”。它只说明结果应该是什么。Kubernetes 观察实际状态,并把实际状态向期望状态调整,这就是声明式管理。
“对象”是 Kubernetes API 中有身份、可以被读取和修改的资源记录。大部分对象都包含四类信息:
apiVersion 指明用哪个 API 组和版本解释字段。kind 指明资源类型,例如 Pod 或 Deployment。metadata 保存名称、Namespace、标签、注解和对象 UID 等身份信息。spec 描述使用者期望的状态;许多对象还由系统写入 status,描述观察到的实际状态。可以把 spec 和 status 理解成同一张工单的“目标”和“进度”。使用者通常修改 spec,控制器根据事实更新 status。两者暂时不一致并不一定是错误,因为创建、调度和启动都需要时间;真正需要观察的是差距是否正在缩小,或被什么条件阻挡。
YAML 只是表达对象的一种序列化格式。Kubernetes 真正管理的是 API 对象,并不“运行 YAML”。kubectl 会把清单转换成请求发给 API Server;同样的对象也可以由客户端库或控制器创建。以后看到 kubectl apply -f deployment.yaml,应在脑中翻译成“把文件描述的期望对象提交到 API”。
可以把整个过程理解成一条持续运行的证据链:
kubectl 把对象提交给 API Server。API Server 完成认证、授权、准入检查和字段校验,再保存对象。
控制器观察到新的期望状态。例如 Deployment 控制器发现需要两个 TaskBoard 副本,就创建相应的 ReplicaSet。
ReplicaSet 创建 Pod。Scheduler 为尚未分配节点的 Pod 选择工作节点,并把调度结果写回 API。
目标节点上的 kubelet 看到 Pod 后,通过容器运行时创建容器,并持续上报容器和 Pod 状态。
这条链路里有两个很容易混淆的词。
调度只负责决定 Pod 应该放到哪个节点,并把绑定结果写回 API;Scheduler 不会亲自启动容器。调谐是控制器不断比较期望与现实并采取动作的过程,创建 ReplicaSet 或补足 Pod 都属于调谐。真正调用容器运行时的,是目标节点上的 kubelet。
API Server 也不是简单的文件接收器。它是所有状态读写的统一入口:请求会经过认证,确认“你是谁”;经过授权,确认“你能否做这件事”;经过准入处理,检查或修改即将保存的对象;再按 API 结构校验字段。Kubernetes 官方的 控制器文档 将控制器描述为观察集群状态并让当前状态接近期望状态的控制循环。课程后面遇到 RBAC 拒绝和 Pod Security Admission 拒绝时,我们会回到这条请求链定位。
这里最值得记住的是“持续”。kubectl apply 不是遥控器上的一次启动按钮。对象保存到 API 后,控制器会继续守住它描述的状态。

假设 Deployment 声明两个副本,而其中一个 Pod 被删除。ReplicaSet 观察到实际只剩一个副本,就会创建新的 Pod。新 Pod 的名字、IP 和 UID 都可能改变,但副本数会回到两个。
因此,后面的实践不会只看命令是否返回 created。我们会继续检查 Ready 状态、控制器关系、日志和 HTTP 响应,确认期望状态真的成立。
Deployment 能补回一个被删掉的 Pod,因为副本数是它管理的状态。但新 Pod 不会继承旧 Pod 的 UID、临时文件或 IP。若业务数据只写在容器可写层里,“Pod 恢复了”与“数据恢复了”是两回事。后面 Redis 会通过 PVC 使用独立持久卷,正是为了把进程生命周期和数据生命周期拆开。
自愈也依赖可观察的信号。进程仍在但已经无法处理请求时,Kubernetes 需要就绪探针或存活探针才能知道。镜像本身有逻辑错误时,控制器只会忠实地反复运行这个错误版本。平台能恢复它看得见的状态,无法替业务定义正确性。
不要把 Running、Ready 和“业务正确”当成同一件事。Running 说明容器进程处于运行状态,Ready 说明就绪条件允许接收流量,业务是否产生正确结果还要用接口响应和业务验收确认。
一个 Kubernetes 集群分为控制平面和工作节点。学习阶段可以把它们放在少量节点中,生产集群则会根据可用性和容量要求部署更多节点。
控制平面中的核心组件包括:
kubectl、控制器和节点组件都通过 API 读写集群对象。控制平面负责做决定和保存决定。它不会直接替应用处理 HTTP 请求。
工作节点中的核心部分包括:
TaskBoard 的 API 容器和 Redis 容器会运行在工作节点的数据平面中。控制平面只管理它们应该以什么状态存在。
以后执行 kubectl get pods 时,kubectl 根据 kubeconfig 找到 API Server,并带上客户端凭据发出 HTTPS 请求。API Server 从持久状态中读取 Pod,再把结果返回终端。这个查询不会直接逐台登录节点。
创建 Pod 则多出后续过程:API Server 先保存一个尚未绑定节点的 Pod;Scheduler 观察它并选择节点;该节点的 kubelet观察到分配结果,调用容器运行时;网络插件准备网络;kubelet再把容器状态和 Pod 条件写回 API。kubectl get 所显示的表格,正是这些组件协作后的状态摘要。
这也是为什么排障要逐层取证。API 拒绝对象,要检查请求和准入;Pod 长期 Pending,要看调度事件;容器起不来,要看镜像、挂载和 kubelet 事件;容器已启动但接口错误,要查日志和应用依赖。把所有问题都归结为“集群坏了”,会丢失真正的故障边界。
后面 kind 会把控制平面和工作负载放在同一个节点容器中。这足以学习 API、调度、控制器、网络名和存储对象,却不能证明高可用。这个节点停止时,控制平面和应用都会停止;课程中的默认 StorageClass 也不是跨机房存储。
生产部署通常需要独立评估控制平面冗余、节点故障域、负载均衡、持久存储、备份恢复、升级策略、监控和容量。学习集群让对象行为可观察,并不等同于生产架构模板。
这门课会同时出现 Docker、kind、containerd 和 Kubernetes。它们的职责需要从一开始就分清。
Docker 在项目交付阶段负责把 app.py 和 Python 运行时构建成 taskboard:1.0.0 镜像。kind 随后用 Docker 容器创建隔离练习集群的节点。进入节点内部,kubelet 通过 CRI 与 containerd 通信,由 containerd 运行 TaskBoard 容器。
项目文件
│ docker build
▼
taskboard:1.0.0 镜像
│ kind load docker-image
▼
kind 节点的镜像存储
│ kubelet 通过 CRI 请求 containerd
▼
TaskBoard 容器进程CRI 是 Kubernetes 与容器运行时之间的接口。早期 Kubernetes 只支持 Docker Engine,后来用 CRI 抽象多种运行时;由于 Docker Engine 本身不实现 CRI,Kubernetes 曾通过 dockershim 适配。dockershim 在 v1.20 宣布弃用,并从 v1.24 移除,历史过程可参考 Kubernetes 官方的 dockershim 背景说明。因此不能再把“节点安装了 Docker”当成现代 Kubernetes 运行容器的默认模型。Docker 构建出的 OCI 镜像仍然可以由 containerd 等 CRI 运行时使用。
Docker 中已经存在的镜像不会自动出现在 kind 节点里。后面必须显式执行 kind load docker-image,把课程镜像送入隔离练习集群的节点镜像存储。
这些缩写常在安装文档中同时出现,职责却不同。
记忆它们不需要死背全称。先看关系:镜像要有共同格式,kubelet要有共同运行时接口,Pod 要有共同网络插件接口。Kubernetes 通过这些边界把具体实现解耦,集群因此可以选择不同的运行时和网络方案。
后面的命令会让这层关系变得可见:docker build 产生镜像;kind load docker-image 把镜像导入节点;Pod 事件显示 kubelet发现镜像已经存在;节点信息则显示真正的容器运行时是 containerd://2.3.1。
课程会逐步得到下面这套对象。每个对象只解决一类问题,组合后才是一套可以交付的应用。
访问者
│ HTTP
▼
Service: taskboard
│ 选择 Ready 的 API Pod
▼
Deployment: taskboard ──► ReplicaSet ──► 两个或更多 API Pod
│ │
│ 读取配置与身份 │ 调用 redis:6379
▼ ▼
ConfigMap / Secret / ServiceAccount Service: redis
│
▼
StatefulSet: redis
│
▼
PVC / PV后续还会加入三类运行保障:

Deployment 管理无状态 API 副本,StatefulSet 管理需要稳定身份和存储的 Redis。Service 提供稳定访问名,ConfigMap 和 Secret 把运行配置移出镜像。把这些内容塞进一个对象,会让更新、授权和故障定位互相牵连。
分开并不意味着零散。标签、选择器、名称和 Namespace 会把它们组织成同一个 TaskBoard 项目。
第一次看到对象列表时,不必一次记住所有字段。先建立四层模型:
对象之间的关系经常靠标签选择器建立,而不是靠文件顺序。Service 不会因为和 Deployment 写在同一文件里就自动连接它;它会持续查找标签满足选择器的 Pod。Deployment 也不因名称相似就管理 Pod,而是通过自己的选择器和 Pod 模板标签建立关系。后面的 Service 故障演练会故意写错选择器,让你看到“对象都存在但关系没有成立”的状态。
TaskBoard 对象会集中放入 taskboard Namespace。同一 Namespace 内,对象名称要保持唯一,命令也能按 Namespace 查询和删除。结课时删除 Namespace,可以让受它管理的大部分对象一并进入删除流程。
但 Namespace 本身不会自动阻断网络、限制资源或授予权限。网络隔离要看 NetworkPolicy 和网络插件,资源上限要看 ResourceQuota/LimitRange,API 权限要看 RBAC。把对象放进 Namespace 是建立边界的第一步,不是安全工作的终点。
后续每个命令都按“解释—执行—取证”推进。你可以把证据分成四层:
kubectl get 和 kubectl describe 证明 API 中存在什么对象,规格和条件是什么。kubectl exec 与探针证明容器里的进程以什么身份运行,是否能回应检查。单独一层往往不够。kubectl apply 返回 created,只证明 API 接受了对象;Pod 为 Running,也不证明 Service 选择器正确;接口返回 200,还可能只请求到旧版本。课程会保留真实输出,并指出动态字段,因为证据的目标是判断关系,不是逐字符模仿某次执行。
Pod IP、UID、名称后缀、镜像内容 ID 和对象 AGE 都可能随执行变化。它们不是无用信息,而是用来识别具体实例;只是不能把某一次值当成永久约定。稳定判断通常是:副本数等于期望值、Ready 条件为 True、版本字段为目标版本、EndpointSlice 中包含当前 Pod IP。
遇到输出不同,先分辨它是合理的动态变化,还是状态关系失败。比如 Pod IP 不同通常正常;READY 0/1 则表示容器尚未就绪,需要继续看 describe 事件和日志。
先问对象是否被 API 接受,再问是否被调度,然后看容器是否启动、是否 Ready、是否被 Service 选中,最后检查业务响应。每一步都用上一层证据缩小范围。反过来一开始就进入容器改文件,往往会掩盖清单错误,而且 Pod 重建后修改也会消失。
学完后,你应能独立完成以下工作:
kubectl 当前连接的是哪个 context。课程中的资源会连续保留到后续章节,避免每一节都重新搭建。每次新增对象前,我们会先解释它要解决的问题;执行后再读取实际状态,并说明输出证明了什么。
现在先记住一条主线:镜像描述应用能怎样运行,Kubernetes 对象描述应用应该怎样持续运行。下一节会准备工具并创建承载这些对象的隔离练习集群。
本章没有创建文件、容器或集群,因此没有遗留运行资源。我们形成的是后续实践共用的判断框架:
spec 表示期望状态,status 与事件提供实际状态证据,控制器持续缩小两者差距。进入下一章前,请确认你能不用术语堆砌地回答两个问题:为什么只会 docker run 还不够;为什么 kubectl apply 成功不等于应用已经可用。如果答案能落到“长期变化”和“分层证据”上,就可以开始创建集群。
控制器继续比较期望状态和实际状态。只要两者有差异,调谐过程就会再次发生。