AI时代的Kubernetes教程36招-第1招-Kubernetes v1.37怎么快速跑起来,为什么不直接用Docker Compose或K3s?
容器编排选型:Compose、K3s 与 kubeadm 的决策边界
选型不是选功能表,是选谁来承担哪一层的决策
[读者] 从单机 Compose 走向多节点集群的后端 / 平台工程师
[痛点] 本地全跑通,一上集群发现网络、存储、HA 全是空白
[现在读] 在动手前把决策归属分清楚,避免建完再推倒
[读完] 能给出选型依据、用 kind 验证 API、并规划 kubeadm 落地
[旧方案] 单机 Docker Compose,一台宿主机跑全部容器
|
v
[新需求] 多节点、自愈、滚动发布、可观测、可升级
|
v
[冲突] Compose 的抽象边界是一台机器,没有调度与期望状态
|
v
[后果] 迁移被迫在做完业务之后重做,网络与存储方案推倒重来
我是老李,做过不少先上线再补架构的项目,也收拾过不少这样的残局。这几年最常见的场景是:一个四五个服务的应用,用一份 docker-compose.yml 在单机上跑得很稳,团队因此认为编排问题已经解决了。直到业务要三节点、要不停服发布,才发现真正的编排问题一个都还没回答。
容器编排这件事,容易被讲成一份功能对照表:Compose 有什么、K3s 有什么、kubeadm 有什么。但功能表回答不了工程问题,因为功能表不告诉你这一层的决策由谁做、什么时候做、以后能不能换。
本篇按先选型、再动手的顺序走一遍:先用 Compose 看清抽象边界,再用 K3s 和 kind / minikube 在真实 Kubernetes API 上验证,最后给出 kubeadm 真实集群的落地顺序。本篇按话题给定的 v1.37.0 作为基线参照,具体小版本号与默认组件需按当前官方文档核验,官方入口包括 Kubernetes 的 releases 与 production-environment 文档、Kubernetes Release 页、Docker Compose 文档、K3s 文档,以及 kind 与 minikube 文档。
配套的项目文档位于 docs/01-selection-and-quickstart.md,本篇内容与该文档的选型章节保持一致。
一台宿主机上的 Compose 明明运行良好,为什么一换到多节点就处处是坑?
在同样的业务需求下,应该把要不要上 Kubernetes 当成一个能力问题,还是当成一个决策归属问题?
如果一开始选错了边界,后面究竟要付出什么代价——是改几个配置,还是把网络、存储和发布流程整个重做?
01、故事
现场是一台 8C16G 的云主机,上面跑着四个容器:web、api、redis、postgres。部署方式是 docker compose up -d,回滚方式是 git checkout 上一个版本的 compose 文件再起一遍。团队的日常发布节奏是每周两次,凌晨执行。
业务侧的需求变化发生在一次续约评审上:客户把可用性要求从月度不可用时间可容忍,改成发布过程不允许中断请求,同时把节点预算压到三台。也就是说,我们要的是多节点、自愈、不停服发布。
任务是把这个四容器应用从单机 Compose 迁到能在多节点上调度、能自愈、能滚动发布的运行环境,并且迁移过程本身不能长时间停机。
第一反应当然是把 compose 文件往集群里搬。真动手才发现,compose 文件里没有任何节点概念:ports 是宿主机端口,volumes 是宿主机路径,depends_on 是启动顺序,restart: always 是进程级重启。这些在单机上都是好设计,因为它们假设只有一台机器。一旦有多台,谁来选择在哪台机器上跑?谁来保证某台机器挂了以后服务还在?谁来把流量只发给已经就绪的副本?
这时候回头看,Compose 并不是简化版的 Kubernetes,它是另一个抽象层:它的边界画在宿主机上。K3s 和 kubeadm 的边界画在集群上,区别只是默认值替你决定多少、你被要求显式决定多少。这一层区别,才是选型真正要回答的东西。
一台机为界
四容器同舟
想扩先问界
界外无调度
02、问题
旧方案在三个方面失效。
第一是故障域。宿主机宕机等于全站不可用,因为四个容器共享同一个内核与同一块磁盘。这不是配置问题,是拓扑问题:没有任何一层能感知另一台机器还可以承接。
第二是扩容方式。加机器只能靠再起一份 compose、改端口,前面挂个反向代理手工分流。一旦要改配置,就得在每台机器上同步 .env、清理本地卷、重启进程,配置漂移不可避免。
第三是发布方式。Compose 的发布粒度是整个项目:docker compose down 再 up -d。即使只改一个服务,中间也存在一个容器数为零的窗口。所谓不停服发布,在 Compose 的抽象里没有对应的原语。
业务影响是具体的:发布窗口被限制在凌晨,一次磁盘写满导致整站挂掉,运维要人工把服务往另一台机器上搬。
技术表现也同样具体:容器端口与宿主机端口强绑定,两个实例无法共用同一端口;数据卷写死在宿主机路径上,换机器等于换数据;健康判断依赖进程是否退出,而进程活着、依赖没就绪的情况完全覆盖不到。
那么,什么叫做迁移完成?本篇给出四条可测的判据,具体阈值由业务与团队共同定义并实测:
- 删除任意一个业务 Pod 后,控制器能把它重新拉起,期间服务不中断;
- 滚动发布过程中,请求失败率不高于双方约定的阈值;
- 新增一个工作节点后,无需手工修改应用配置即可承接流量;
- 集群版本升级有明确的来源、顺序与回滚预案。
这四条判据里,没有一条是用了 Kubernetes。它们描述的是行为,不是技术栈。选型如果从技术栈出发,很容易在这四条上给出模糊答案。
顺序非就绪
重启非调度
端口即枷锁
判据要可测
03、原理
编排系统的第一性原理只有两条:把期望状态声明出来,然后让控制循环不断把实际状态拉向期望状态。
replicas: 3 是期望状态,当前两个就绪是实际状态,中间的差值就是控制器要消除的对象。Compose 里也有类似的东西,比如 restart: always,但它是进程级看护,不是状态收敛:它不知道应该有三个,也不知道只有两个在别的机器上。
从这里可以推出第一条反直觉判断:Compose 的简单,不是少做了决定,而是把决定藏进了宿主机。端口冲突由宿主机解决,数据落盘由宿主机解决,重启由 docker daemon 解决。当宿主机只有一台时,这些决定看起来不存在;当宿主机变成三台,它们就全部浮出来了。
第二条反直觉判断:K3s 的轻,不是能力弱,而是默认值已经替你决定。它把 containerd、CNI、CoreDNS、Ingress 控制器、本地存储 provisioner 以及默认数据存储一并做了选择,让你用一条命令拿到一个可用的集群。真正的差异不在于能力多少,而在于这些默认值是否可见、是否可替换。具体默认组件与替换方式需按当前 K3s 官方文档核验。
第三条反直觉判断:kubeadm 的复杂,不是功能更多,而是它拒绝替你决定。它负责把一个符合规范的 Control Plane 装起来,但 CNI、CSI、Ingress、etcd 拓扑(stacked 还是外部)、多 Control Plane 的负载均衡与证书分发,全部交回给平台团队。这是把已经存在的决策显式化,而不是新增了能力。
第四条反直觉判断:选型的第一性问题不是哪个功能强,而是哪些决策必须现在回答、由谁回答、以后还能不能改。三个方案在这条问题上的答案完全不同,而这直接决定了后面架构、故障和优化的形态。
对后续章节最重要的三个概念,也在这里对齐:
- CRI / CNI / CSI 是三条插件边界:容器运行时、Pod 网络、块与文件存储。任何跳过这三个词的选型结论,都属于没答完题。
- etcd 是期望状态的唯一权威来源,它的拓扑决定了集群的故障恢复方式。
- 调度器决定每个 Pod 落在哪个节点,而调度成立的前提是节点可被注册、可被标记、可被驱逐。
简单藏决定
复杂显权责
默认非无择
择者须知名
04、架构
[输入] 期望状态声明(Deployment / Service / Ingress)
|
v
[模块] Control Plane:API Server / Scheduler / Controller Manager
|
v
[数据/状态] etcd 中的期望状态与集群实际状态
|
v
[处理] kubelet + CRI(containerd) + CNI + CSI
|
v
[输出] Pod 运行、自愈收敛、流量分发
这张图的价值不在于描述 Kubernetes,而在于标出哪些环节是必须有人做决定的。三个方案的区别,就是这几个环节由谁决定、什么时候决定。
| 决策项 | Compose | K3s | kubeadm |
|---|---|---|---|
| 调度 | 无,固定宿主机 | 内置调度器 | 内置调度器 |
| CRI | Docker Engine | 默认 containerd | 需显式选择并配置 |
| CNI | 桥接网络,无集群语义 | 默认集成 | 需自行安装与选型 |
| CSI / 存储 | bind mount 本地路径 | 默认本地存储 | 需自行安装与选型 |
| Ingress | 反向代理自行维护 | 默认集成 | 需自行安装 |
| 数据存储 | 宿主机文件 | 默认轻量方案 | 需显式规划 etcd 拓扑 |
| HA | 无 | 多节点部署时可选 | 需显式规划并做前置负载均衡 |
| 升级 | 换镜像重启 | 单条命令升级 | 需按版本偏差规则逐级升级 |
表中 K3s 与 kubeadm 的默认项需按当前官方文档核验,尤其是默认 Ingress 控制器与默认数据存储。
边界很清楚:本篇要解决的是选哪条路径、每条路径各自欠下哪些决策,不解决具体用哪个 CNI、哪个 CSI。后两者应该在上面这张图的 CNI / CSI 行上被单独开题。
收益是显式的:一旦把决策项写在表里,选型就从感觉 K3s 更轻,变成我们能不能承担 CNI 选型的人力。代价是前期会更慢——你要在写第一行 YAML 之前先回答八个问题。适用条件是团队规模:一个人维护且机器数不超过三台,K3s 的默认值就是净收益;有专职平台团队且要接合规与多环境,kubeadm 的显式反而是必需的。
期望先声明
控制去收敛
插件分三界
边界要显式
05、实战一次
先把最小可用的一步跑通:用 kind 在本地拉起一个真实 Kubernetes API 的集群,把同一份业务改写成 Deployment + Service,并用几条命令验证自愈行为。这一步不涉及 HA、CSI 与升级路径,它的唯一目的是确认接口是真的。
环境与依赖(未在当前环境实测,以下为预期结果,版本请按官方文档核验):
# 依赖:Docker、kubectl、kind
docker version --format '{{.Server.Version}}'
kubectl version --client
kind version
建一个带两个 worker 的集群:
cat > kind-lab.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: lab
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kind create cluster --config kind-lab.yaml
kubectl get nodes
示例输出:
NAME STATUS ROLES AGE VERSION
lab-control-plane Ready control-plane 42s v1.xx.y
lab-worker Ready <none> 28s v1.xx.y
lab-worker2 Ready <none> 27s v1.xx.y
集群起来后,装一份最小工作负载:
# web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:stable-alpine
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
注意这里刻意加了两样 Compose 里没有的东西:readinessProbe 和 Service 的 selector。前者定义什么算就绪,后者定义哪些副本可以接流量。这两样正是第 02 章里 Compose 缺失的原语。
启动与验证:
kubectl apply -f web.yaml
kubectl rollout status deployment/web
kubectl get pods -o wide
kubectl get endpoints web
示例输出:
NAME READY STATUS RESTARTS AGE
web-6c8f7d9b4c-2m9xq 1/1 Running 0 35s
web-6c8f7d9b4c-7k4zp 1/1 Running 0 35s
NAME ENDPOINTS AGE
web 10.244.1.5:80,10.244.2.3:80 36s
endpoints 里出现两个地址,说明就绪探测已经通过并且两个副本都被纳入了服务后端。接着做两件事:删掉一个 Pod,看它是否被重新拉起;把副本数改成 4,看调度是否落在不同节点。
kubectl delete pod web-6c8f7d9b4c-2m9xq
kubectl get pods -w
kubectl scale deployment/web --replicas=4
kubectl get pods -o wide
作为对照,同一份业务在 Compose 里是这样写的:
# docker-compose.yml
services:
web:
image: nginx:stable-alpine
ports:
- '8080:80'
docker compose up -d
docker compose ps
这份对照能说明一件事:Compose 侧的 ports 是宿主机端口映射,一旦有两台机器,8080 这个端口就要由人来分配;Kubernetes 侧的 Service 是集群内虚拟地址,端口由集群统一分配。差别不在写法,在抽象边界。
V1 到这里跑通了,但它留下了三个明确的问题:kind 的节点其实是容器,回答不了 HA、etcd 拓扑和真实内核参数;镜像怎么进到节点里,直接影响后面发布是否可靠;集群从哪来(kind 还是 kubeadm)决定了升级路径是否存在。这三条会在第 06 章变成具体的故障。
先起假节点
再证真接口
就绪再收流量
副本自归来
06、排查
现象一:Compose 下 api 启动后立刻报数据库连接被拒。
怀疑是网络问题,因为容器之间不通的直觉太强。
检查方式:
docker compose ps
docker compose logs api --tail=50
docker inspect -f '{{json .State.Health}}' $(docker compose ps -q db)
证据:db 容器状态是 Up,但数据库进程仍在初始化;api 的日志时间是 db 启动后的第 1 秒。depends_on 只保证启动顺序,且在没有 condition 的情况下只保证容器已启动,不保证服务已就绪。
根因:把顺序语义当成了就绪语义。Compose 的依赖表达的是启动编排,不是健康协商。
错误尝试: 先把 depends_on 的条目删掉再手工加 restart: always,指望进程崩溃后重启能等到数据库就绪。这是错的,因为重启解决的是进程退出,而这里进程没退出,它只是在等一个还不存在的连接;重启策略既不能提前也不能延后,只会把失败日志刷得更多。
修复:给 db 加 healthcheck,把依赖改成 condition: service_healthy,让依赖关系表达真正的就绪条件:
services:
db:
image: postgres:16-alpine
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 3s
retries: 10
api:
depends_on:
db:
condition: service_healthy
现象二:kind 集群里改了代码,Pod 行为却没变。
现象是:本地已经重新 build 出镜像,kubectl rollout restart 之后 Pod 里的行为还是旧的。
怀疑是 rollout 没生效。
检查方式:
kubectl get deployment web -o jsonpath='{.spec.template.spec.containers[*].image}'
kubectl describe pod -l app=web | grep -A3 'Image:'
kind load docker-image web:latest --name lab
证据:本地 docker images 里有新镜像,但 kind 节点是独立容器,节点的 containerd 镜像存储与宿主机 Docker 的镜像存储不是同一份;同时 web:latest 这个标签在两处都指向某个版本,无法区分新旧。
根因:镜像分发路径和标签语义同时出问题。本地镜像不会自动出现在集群节点里,而可变的 latest 标签让拉取无法判断是否需要更新。
修复:把镜像显式载入节点,并改用不可变标签(例如带提交短哈希的标签),让每次发布对应一个唯一产物:
docker build -t web:2025-01-01-abc1234 .
kind load docker-image web:2025-01-01-abc1234 --name lab
kubectl set image deployment/web web=web:2025-01-01-abc1234
(标签中的日期与哈希为占位示例,实际值由你的构建产物决定。)
现象三:kubeadm init 成功后节点一直 NotReady。
这是后面章节会遇到的问题,先在这里把排查链立起来。
怀疑是 kubelet 没起来。
检查方式:
kubectl get nodes
kubectl describe node <node-name> | grep -A5 Conditions
journalctl -u kubelet -n 100 --no-pager
kubectl get pods -n kube-system
证据:describe node 的 Conditions 里会出现网络插件未就绪类的提示,kubelet 日志中反复出现 CNI 相关告警,而 kube-system 里没有网络插件的 Pod。
错误尝试: 反复 kubeadm reset 再 kubeadm init,或者重装 kubelet。这是错的,因为 kubeadm 的职责边界里不包括安装 CNI——它会告诉你节点没就绪,但不会替你装网络插件。问题不在安装过程,在这一层的决策没人做。
根因:这是显式决策没有被显式回答。kubeadm 把 CNI 交回给平台团队,如果团队默认它应该自带,就会得到一批 NotReady 节点。
修复:按官方文档选择一个 CNI 插件并在 init 后安装,再确认节点转为 Ready;同时在选型阶段就把 CNI 写进决策清单,而不是等到排障时才想起来。
现象先留痕
怀疑要证伪
证据定根因
重装最无用
07、优化
第 06 章的三条链路给出的根因都很明确,V2 就按根因逐条改,不做额外发挥。
根因一:用启动顺序表达依赖。修改是把依赖改成就绪条件:Compose 侧用 healthcheck + condition: service_healthy,Kubernetes 侧用 readinessProbe 与 Service 后端收敛。原因相同——依赖关系描述的是能不能开始工作,不是有没有被启动。新行为是:副本只有在就绪探测通过后才进入服务后端,未就绪的副本不接流量。验证方式:
kubectl rollout status deployment/web
kubectl get endpoints web
预期是 endpoints 中的地址数量与就绪副本数一致(未在当前环境实测,以上为预期结果)。
根因二:镜像分发路径与标签语义不清。修改是三条:构建产物使用不可变标签;按环境选择分发方式(本地集群用 kind load docker-image,真实集群用镜像仓库);拉取策略与标签策略保持一致,配置变更时使用 kubectl set image 触发 rollout。原因是不变标签让这次要跑的是哪个产物变成可验证的事实。新行为是:每次发布对应唯一标签,回滚就是把标签指回上一个值。验证方式:
kubectl rollout history deployment/web
kubectl rollout undo deployment/web
kubectl get pods -l app=web -o jsonpath='{.items[*].spec.containers[*].image}'
根因三:关键决策没有被显式回答。修改不是写代码,而是补一张决策清单,并在动手前逐项确认:CRI 用哪个、CNI 用哪个、CSI 用哪个、Ingress 用哪个、etcd 用 stacked 还是外部、Control Plane 是否多副本、前置负载均衡怎么做、升级的版本路径是什么。原因是这些问题在 kubeadm 路径上全部是你必须回答,晚回答的代价是重做。新行为是:kubeadm init 之前清单为空就不开工。验证方式是把清单里的每一项都落成一个可检查的配置或命令。
kubectl get nodes
kubectl get pods -n kube-system
kubectl describe node <node-name> | grep -i network
V2 之后,同一份业务在两个环境里都有了明确语义:本地用 kind 验证接口与调度行为,真实集群用 kubeadm 承载 HA 与升级。这里没有新增任何能力,只是把 V1 里被跳过的决策补回来。
根因定改法
未验不称成
探针替顺序
标签替最新
08、演进
[同一输入]
|
+--[V1] Compose 单机:启动快、心智低 / 无调度、无自愈、发布有窗口
|
+--[V2] 集群路径:kind 验证接口 + kubeadm 承载生产 / 需回答八项显式决策
|
[Trade-off]
{得到:自愈、滚动发布、可扩展的调度与存储插件边界}
{失去:一条命令上线、默认值带来的低心智负担}
{适用边界:节点数与团队人力决定选 K3s 还是 kubeadm}
正确性上,V1 在单机范围内是对的,它的错误只出现在要求多节点之后;V2 的正确性依赖于几项显式决策是否被真正回答,如果 CNI、CSI、etcd 拓扑空着,V2 只是换了个地方出故障。
稳定性上,V1 的稳定性等于宿主机的稳定性,故障域不可分割;V2 引入了控制循环与多节点冗余,但也引入了新的故障面:控制面组件、etcd 的磁盘性能、网络插件的健康度。
复杂度上,V1 的文件是几行 YAML,V2 的复杂度分布在整个平台:声明式资源、插件、证书、版本偏差规则。这部分复杂度不会消失,只会从你写的时候移动到运维的时候。
成本上,V1 的机器成本最低但人力成本高在人工搬运;V2 的机器成本上升(至少三节点、可能的独立 etcd、前置负载均衡),人力成本从救火转为设计与维护。这部分需要按团队实际人数估算,不宜用统一比例套用。
适用范围上,单机开发、测试环境、演示型部署继续用 Compose 是合理的;边缘场景、单团队、机器数少,K3s 的默认值就是净收益;有合规要求、多环境、多团队共用、需要掌控升级节奏,则 kubeadm 的显式才是必要的。
遗留问题有三个:一是 kind 的节点是容器,无法验证真实内核与网络行为,HA 与 etcd 拓扑必须在真实 Linux 上验证;二是 CSI 与备份策略在本篇没有展开,属于下一层决策;三是升级路径需要按官方版本偏差规则规划,具体允许的跨度需按当前官方文档核验。
同源不同路
一得必一失
轻重要自认
边界随时移
09、洞见
9.1 选型的第一性问题是决策归属,不是功能清单
反直觉判断:把 Compose、K3s、kubeadm 摆在一起比功能,是问错了问题。三者不是同一个东西的三个版本,而是三条边界不同的路径。Compose 的边界在宿主机,K3s 的边界在集群但默认值由发行版决定,kubeadm 的边界在集群且默认值由平台团队决定。功能清单只能告诉你能力有没有,无法告诉你这个决定由谁做、什么时候做、以后能不能换。
工程结论是:选型会议的产出应该是一张决策归属表,而不是一张性能对比表。填不出这张表,就说明还没到选型阶段。
9.2 默认值的价值是速度,代价是可见性
反直觉判断:K3s 之所以能轻,是因为它替你做了很多正确的默认决定,而不是因为它做了更少的事。默认值本身是好东西——它把一个人的团队从八个问题里解放出来。真正的风险在于默认值不可见:当 CNI 需要换成支持某种网络策略的实现、当存储需要从本地卷切到网络卷时,你要先花时间搞清楚现在用的到底是什么。
工程结论是:无论选哪条路径,第一件事都是把当前生效的默认值枚举出来,写进决策清单并标注可替换性。可替换性比默认值本身更重要。
9.3 显式决策不能推迟到出故障时再补
反直觉判断:很多人认为先用最简单的方式跑起来、出问题再上 Kubernetes 是务实做法,但在编排这件事上,这个顺序恰恰会放大成本。因为编排影响的是网络、存储、发布这三个横切面,它们不是某个服务的实现细节,而是所有服务的运行地基。等地基上盖满了房子再换地基,代价不是改配置,是重做拓扑。
第 06 章的第三条链路就是这个模式的缩影:节点 NotReady 的根因不在安装过程,而在 CNI 这一层的决策没人做。
9.4 迁移的完成标准必须是行为,不是技术栈
反直觉判断:判断迁移是否完成,最没用的判据是我们已经用了 Kubernetes。有用的判据是删掉一个副本会怎样、发布会中断吗、新节点加进来要不要改配置。这些判据在任何方案下都成立,也因此在任何方案下都可验证。
工程结论是:把第 02 章的四条判据做成验收清单,先定义清楚再动手。选型可以变,判据不该变。
选型先选责
能力后对齐
默认要可见
替换要有路
10、系统落地
原来有什么:一份 docker-compose.yml,四个服务,一台宿主机,发布靠凌晨停机窗口,故障靠人工搬家。它成功地支撑了业务从零到一的阶段,这一点不应该被否定。
本篇新增了什么:一张决策归属表,把调度、CRI、CNI、CSI、Ingress、数据存储、HA、升级八项决策的归属写清楚;一套用 kind 在本地验证真实 API 行为的方法;一条 kubeadm 真实集群的落地顺序,从 Control Plane 到网络插件再到工作负载。
现在能做什么:在不改变业务代码的前提下,把工作负载改写为 Deployment + Service,获得副本自愈与滚动发布;能在本地用 kind 验证调度与就绪语义;能明确知道哪些能力是默认给的、哪些必须自己装。
还缺什么:真实的 HA 拓扑与 etcd 规划尚未验证;CNI 与 CSI 的具体选型尚未定;备份与恢复策略、可观测性接入、镜像仓库与流水线都还是空白;升级路径与版本偏差规则还需要按官方文档逐条确认。
下一步如何演进:在真实 Linux 节点上用 kubeadm 起第一个 Control Plane 并安装所选 CNI,确认节点 Ready;再加入第二个节点验证调度与网络;随后规划多 Control Plane 与前置负载均衡,最后才讨论 CSI 与备份。顺序不能颠倒,因为每一步都在为下一步提供验证前提。
原来一台机
现在一张网
缺的是存储
下一步高可用
11、小结
Q1 → 因为 Compose 的抽象边界是宿主机:没有调度、没有期望状态、没有就绪语义,多节点一上,这些决定全部暴露
Q2 → 它是决策归属问题:Compose、K3s、kubeadm 的差别在于哪些决定由谁做、什么时候做、能不能换
Q3 → 代价不是改配置,而是拓扑重做:网络、存储、发布流程都是横切面,晚决定的成本远高于早决定
状态 → 已能用 kind 验证真实 API 与自愈行为,并拥有一张可执行的决策清单,kubeadm 真实集群待后续章节落地
三问皆有答
一图定边界
可验方可交
未测不称成
12、作业
12.1 理解题:为什么 depends_on 不能替代健康检查?
参考答案:depends_on 表达的是启动顺序,它保证被依赖的容器先被启动,但不保证其中的服务已经可以接受连接。数据库从进程启动到可以接受连接之间存在一段时间窗口,API 在这个窗口内发起连接就会失败。健康检查表达的是就绪语义,只有探测通过才认为依赖可用。两者的差别不是严格程度,而是语义类型不同:一个是顺序,一个是状态。
12.2 实战题:用 kind 起一个三节点集群,部署两副本服务,并验证自愈
参考答案:按第 05 章的 kind-lab.yaml 创建集群,确认 kubectl get nodes 中三个节点均为 Ready;应用 Deployment + Service 清单,确认 kubectl get endpoints 中的地址数与就绪副本数一致;随后 kubectl delete pod <name> 删除其中一个副本,用 kubectl get pods -w 观察控制器拉起新副本;再 kubectl scale deployment/web --replicas=4,用 kubectl get pods -o wide 确认副本分布在不同节点。验收依据是行为,不是集群里有几个节点。
12.3 排障题:kubeadm init 成功但节点长期 NotReady,第一步该做什么?
参考答案:先确认根因层级,而不是重装。执行 kubectl describe node <node-name> 查看 Conditions,journalctl -u kubelet 查看 kubelet 日志,kubectl get pods -n kube-system 查看控制面组件与网络插件状态。如果日志指向网络插件未就绪且 kube-system 中没有 CNI 相关 Pod,即可判定为 CNI 未安装——这是 kubeadm 的显式决策边界,不是安装失败。修复方式是安装符合需求的 CNI 插件,并把这一项写入决策清单。重装 kubelet 或反复 reset 无法解决问题,因为被跳过的是决策,不是安装步骤。
12.4 架构判断题:一个三人团队,要维护 5 个微服务、3 台机器、无专职平台工程师,应该选 K3s 还是 kubeadm?
参考答案:倾向于 K3s。判断依据不是 K3s 更简单,而是决策归属:这个团队没有余力承担 CNI、CSI、Ingress、etcd 拓扑、HA、升级这六到八项显式决策的长期维护,K3s 的默认值把这些决策收敛成一套可用组合,人力可以放在业务上。但有两个前提必须同时满足:第一,把 K3s 当前生效的默认组件枚举出来并标注可替换性,避免以后想换却不知道换什么;第二,验证默认存储与默认 Ingress 是否能满足业务要求。如果未来出现合规、多环境隔离或升级节奏控制的硬要求,再评估迁移到 kubeadm,并把这个迁移当作一次有决策清单的独立项目,而不是一次技术升级。
学会先问界
动手再验证
判错也要留
证据最值钱
13、思考
回到本篇的核心冲突:Compose 明明运行良好,为什么换到多节点就处处是坑。答案不在 Compose 不好,而在于它的抽象边界就是一台机器。当需求没有越过这条边界时,它是最优解;一旦越过,缺的不是某个功能,而是整整一层由谁来调度、由谁来收敛、由谁来决定插件的决策。
把这件事抽出来,可以复用的工程判断是:任何基础设施选型,先把决策项列出来,标注每一项由谁决定、什么时候决定、以后能不能替换,再去比较功能与性能。这样做会慢一两天,但能避免在地基上盖满房子之后再换地基。
选型阶段最容易被忽略的一件事是默认值也要写进清单。默认值不是没有决定,而是别人替你做了决定,它同样需要可见、可核验、可替换。这一点在 K3s 上尤其明显:越省事的方案,越需要把省掉的那部分写清楚。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号