Kubernetes 从入门到精通
本文面向已经熟悉 Docker 与 Nginx、但尚未系统接触 Kubernetes 的开发者。全文遵循一条由浅入深的主线: 先从单机容器的真实痛点引出 Kubernetes 解决的问题, 再铺垫必要的预备知识, 然后逐个核心对象动手实践, 每个新名词在首次出现时都会就地解释清楚, 并尽量与你已经熟悉的 Docker、Nginx、docker-compose 做类比。掌握核心用法后, 再逐步深入控制循环、调度、网络、安全等底层机制, 最终走向生产实践与进阶生态。每一节都配有可以直接动手运行的示例, 跑一遍胜过读十遍。
一、从 Docker 的尽头说起
在理解 Kubernetes 是什么之前, 先要理解它到底解决了什么问题。假设你用 Docker 把一个 Node 服务打成镜像, 在一台服务器上这样跑起来:
docker run -d --name api -p 3000:3000 my-api:1.0单机、单容器, 一切正常。但真实业务很快会提出新的要求:
- 这个容器半夜崩了, 谁来把它重新拉起来?
- 流量涨了, 需要同时跑 5 个一模一样的容器分摊压力, 它们之间如何分配请求?
- 一台机器扛不住, 要扩展到 10 台服务器, 新容器该放在哪台机器上?
- 要发布 2.0 版本, 如何做到不中断服务地逐个替换旧容器?
- 某台机器整个宕机了, 上面的容器如何自动迁移到健康的机器上?
这些问题, 用 docker run 加上一堆手写的 Shell 脚本与监控也能勉强应付, 但当规模上去之后, 维护成本会急剧膨胀, 而且极易出错。Kubernetes 就是为了系统化地解决这一类问题而生的容器编排平台。
名词: 什么是 Kubernetes, 为什么简称 K8s
Kubernetes 源自希腊语, 意为"舵手"。因为单词太长, 社区习惯把它简写为 K8s——取首字母 K 和尾字母 s, 中间的 8 个字母用数字 8 代替。所谓容器编排 (Container Orchestration), 就是自动管理大量容器在多台机器上的部署、调度、伸缩、网络与故障恢复。一句话: Docker 解决"把一个应用打包并运行起来", Kubernetes 解决"把成百上千个容器, 在一群机器上可靠地编排起来, 并持续维持健康状态"。
可以用一个类比贯穿全文: 如果说 Docker 镜像是"标准集装箱", 那么 Kubernetes 就是整个港口的自动化调度系统——它不关心集装箱里装的是什么, 只负责把它们放到合适的货轮上, 坏了就换、不够了就加、要换新货时逐个平滑替换。
二、理解 Kubernetes 的世界观
学习 Kubernetes 最大的认知门槛, 不是命令多, 而是思维方式的转变: 从命令式到声明式。这一点想通了, 后面所有对象都会变得顺理成章。
命令式 vs 声明式
你过去使用 Docker 的方式是命令式 (Imperative) 的——你下达一条条具体指令, 系统照做:
docker run ... # 启动一个
docker stop ... # 停止一个
docker rm ... # 删除一个每条命令描述的是"做什么动作"。如果容器挂了, 它就是挂了, 不会自己起来, 因为你没有再下达指令。
Kubernetes 是声明式 (Declarative) 的——你不描述动作, 而是描述"我期望系统最终是什么样子", 通常写在一个配置文件里。比如"我希望这个应用始终有 3 个副本在运行"。提交之后, Kubernetes 会持续不断地做一件事: 对比期望状态 (Desired State) 与实际状态 (Actual State), 一旦有差异就自动纠正。
挂掉一个副本, 实际状态变成 2, 与期望的 3 不符, Kubernetes 自动再拉起一个; 你手动多起了一个变成 4, 它会自动关掉一个。这个"持续对比并纠偏"的机制叫做控制循环 (Control Loop), 也叫调谐 (Reconcile), 是理解 Kubernetes 的钥匙, 后文还会反复提到它。
这意味着什么
你几乎不需要再写"如果挂了就重启"这类逻辑。你只负责声明目标, 维持目标的脏活累活由平台兜底。这也是为什么 Kubernetes 的配置以声明式文件为主, 而不是一串命令脚本。
预备知识: 看懂 YAML
Kubernetes 的声明文件几乎都用 YAML 格式编写。如果你还不熟悉, 这里花两分钟讲清楚, 后面看配置就不费力了。
名词: YAML 是什么
YAML 是一种数据格式, 作用和 JSON 一样, 用来描述结构化数据, 但它靠缩进表达层级, 没有大括号, 更适合人读写。Kubernetes 选它来写配置。三条核心规则:
- 缩进表示从属关系, 必须用空格, 严禁用 Tab (这是新手最常见的报错来源)。
键: 值表示一个属性, 冒号后要有一个空格。-开头表示列表项 (数组的一个元素)。
一个最小的例子, 对照右侧注释理解:
apiVersion: v1 # 字符串值
kind: Pod
metadata: # metadata 是一个对象, 下面缩进的都属于它
name: my-pod
labels: # labels 又是 metadata 里的一个对象
app: web
spec:
containers: # containers 是一个列表
- name: nginx # 列表的第一个元素 (- 开头)
image: nginx:alpine这段等价于 JSON 里的嵌套对象。看懂缩进层级, 就看懂了所有 Kubernetes 配置。
集群的整体架构
一个 Kubernetes 集群 (Cluster) 由两类角色的机器组成: 控制平面 (Control Plane) 负责决策, 工作节点 (Worker Node) 负责干活。
控制平面的四个核心组件:
- API Server: 整个集群唯一的入口。你的
kubectl命令、各组件之间的通信, 全部经过它。可以类比成公司总机, 所有请求都先到前台。它对外提供一套 REST API,kubectl本质就是这套 API 的命令行客户端。 - etcd: 一个高可靠的键值数据库 (Key-Value Store), 保存集群的全部状态——有哪些应用、各自期望几个副本、当前实际如何, 全在这里。它是集群唯一的事实来源 (Source of Truth), 类比整个集群的"账本"。
- Scheduler (调度器): 当有新容器需要运行时, 由它决定放到哪台 Worker 上, 依据是各机器的剩余资源、亲和性规则等。类比调度员派活。这部分的细节会在第十六章深入。
- Controller Manager (控制器管理器): 上一节说的"控制循环"就跑在这里。它内部运行着几十种控制器, 不断对比期望与实际并纠偏, 是声明式能力的真正执行者。
每个 Worker Node 上有三样东西:
- kubelet: 节点上的代理进程, 接收 API Server 的指令, 真正调用容器运行时去创建、监控容器, 并把节点和容器的状态汇报回去。类比工地上的工头。
- kube-proxy: 维护本节点的网络转发规则, 实现后面要讲的 Service 负载均衡。它干的活, 本质上就是你熟悉的 Nginx 反向代理与 upstream 负载均衡, 只不过是在集群内部、由平台自动维护的。
- 容器运行时 (Container Runtime): 真正运行容器的底层程序, 现在主流是 containerd, 早期直接用 Docker。也就是说, Kubernetes 底层依然在跑你熟悉的容器, 你构建的镜像完全通用。
一个常见误解: Kubernetes 取代了 Docker 吗
没有。Docker 负责"构建镜像"和"运行单个容器"这一层, Kubernetes 站在更高层做"编排"。早期 Kubernetes 直接调用 Docker 来跑容器, 后来改用更轻量的 containerd (它其实是从 Docker 中拆出来的核心组件), 但这对你毫无影响——你写的 Dockerfile、构建出的镜像, 在 Kubernetes 里可以原封不动地使用。
把概念对照到你已经会的
下面这张对照表很重要, 建议反复回看。左边是你已经熟悉的事物, 右边是 Kubernetes 中的对应物。
| 你已经熟悉的 | Kubernetes 对应 | 关键差别 |
|---|---|---|
| Docker 镜像 | 完全相同的镜像 | 通用, 无需改动 |
docker run 一个容器 | Pod | 最小调度单位, 可含一个或多个紧密协作的容器 |
docker-compose 编排多容器 | Deployment | 多了自愈、多副本、滚动更新、跨机调度 |
| Nginx upstream 负载均衡 | Service | 给一组 Pod 一个稳定地址并自动负载均衡 |
| Nginx 按域名/路径反向代理 | Ingress | 集群对外的 HTTP(S) 流量入口 |
docker volume 数据卷 | PV / PVC | 存储抽象, 可跨节点 |
.env 文件 / 挂配置 | ConfigMap / Secret | 配置、密钥与镜像解耦 |
| 项目按目录隔离 | Namespace | 集群内的逻辑隔离分区 |
接下来的章节, 就沿着这张表从上到下, 一个一个动手实践。
三、搭建本地实验环境
学习 Kubernetes 不需要一上来就准备多台云服务器。本地有几种轻量方案可以在单机上起一个完整集群:
- kind: Kubernetes IN Docker, 用 Docker 容器模拟节点, 启动快, 最适合学习与 CI。
- minikube: 老牌本地方案, 功能全, 内置 Dashboard 与各种插件。
- k3d / k3s: 轻量发行版, 资源占用小, 适合树莓派或边缘场景。
- Docker Desktop: 设置里直接勾选 "Enable Kubernetes" 即可, 对你来说可能是门槛最低的。
本文以 kind 为例, 因为它最贴合"用 Docker 跑 Kubernetes"的直觉, 你已有的 Docker 环境可以直接复用。
# 安装 kind 与 kubectl (Kubernetes 的命令行客户端)
brew install kind kubectl
# 创建一个名为 dev 的集群 (底层会启动一个 Docker 容器作为节点)
kind create cluster --name dev
# 验证集群可用, 查看节点
kubectl get nodes# 下载安装 kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind
# 安装 kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
# 创建集群
kind create cluster --name dev
kubectl get nodes如果一切正常, 最后一条命令会输出类似:
NAME STATUS ROLES AGE VERSION
dev-control-plane Ready control-plane 60s v1.29.0看到 Ready, 说明你的第一个 Kubernetes 集群已经跑起来了。
预备知识: 读懂 kubectl 命令结构
kubectl 是与集群交互的命令行客户端, 你后面会敲成百上千次。它的命令结构高度规整, 记住这个套路, 大部分命令不用查文档:
kubectl [动词] [资源类型] [资源名] [可选参数]
─────── ────────── ──────── ──────────
kubectl get pods # 列出所有 Pod
kubectl get pod my-nginx # 看某个 Pod
kubectl describe pod my-nginx # 看某个 Pod 的详情
kubectl delete pod my-nginx # 删除某个 Pod
kubectl logs my-nginx # 看某个 Pod 日志常用动词: get (列出)、describe (看详情)、apply (声明式创建或更新)、delete (删除)、logs (看日志)、exec (进容器执行命令)。
配好别名和补全, 后面省力
alias k=kubectl
# 开启命令补全 (以 zsh 为例, bash 把 zsh 换成 bash)
source <(kubectl completion zsh)之后敲 k get po 再按 Tab 就能自动补全, 效率提升明显。
四、Namespace: 集群里的分区
正式部署应用前, 先认识一个贯穿始终的概念: Namespace (命名空间)。
你之前用 kubectl get pods 看到的其实只是默认分区里的内容。一个集群里会同时跑很多东西: 你的业务应用、平台自身组件、监控、日志等。如果全堆在一起, 很快就会乱成一团。Namespace 就是集群内部的逻辑隔离分区, 类比一台服务器上用不同目录隔离不同项目, 或者数据库里的多个 database。
# 查看集群里有哪些命名空间
kubectl get namespaces
# 你会看到一些内置的:
# default 你不指定时, 资源默认放这里
# kube-system Kubernetes 自身组件 (DNS、kube-proxy 等) 所在地, 别乱动
# kube-public 公开信息
# kube-node-lease 节点心跳几乎所有 kubectl 命令都可以加 -n <命名空间> 来指定操作哪个分区, 不加就是 default:
# 创建一个自己的命名空间
kubectl create namespace demo
# 在 demo 命名空间里查 Pod
kubectl get pods -n demo
# 查看所有命名空间的 Pod (-A 等于 --all-namespaces)
kubectl get pods -A为什么要关心 Namespace
两个实际作用。其一是隔离: 不同团队或环境 (开发/测试) 可以用不同命名空间, 互不干扰, 名字还能重复 (两个命名空间里都可以有叫 web 的服务)。其二是权限与配额的边界: 后面要讲的 RBAC 权限控制、资源配额 (ResourceQuota), 都是以命名空间为单位划定的。当你排查问题 kubectl get pods 却什么都看不到时, 第一反应应该是"我是不是看错命名空间了"。
五、第一个 Pod
Pod 是 Kubernetes 中最小的部署与调度单位。 这是一个关键认知: 你不能直接运行一个容器, 而是把容器装进 Pod 里运行。
为什么不直接以容器为单位, 要多包一层 Pod? 因为有些场景下, 几个容器需要紧密协作、共享网络与存储, 把它们放在同一个 Pod 里最自然。最经典的是 Sidecar (边车) 模式: 主容器跑应用, 旁边一个辅助容器收集日志或做代理, 两者共享同一网络, 通过 localhost 互通, 就像挎斗摩托车的主车与边车。
名词: Pod 内的容器共享什么
同一个 Pod 内的所有容器, 共享同一个网络命名空间 (同一个 IP、同一组端口空间) 和可选的存储卷。它们之间用 localhost 就能互相访问, 就像运行在同一台主机上。不过大多数情况下, 一个 Pod 里只放一个业务容器, 别被"可以放多个"误导成"应该放多个"。
动手: 用命令快速跑一个 Pod
最快的方式是命令式直接拉起, 适合临时验证:
# 运行一个 nginx Pod
kubectl run my-nginx --image=nginx:alpine
# 查看 Pod 状态
kubectl get pods
# 看详细信息与事件 (排错第一手段)
kubectl describe pod my-nginxkubectl get pods 会展示 Pod 的关键状态:
NAME READY STATUS RESTARTS AGE
my-nginx 1/1 Running 0 10s逐列解读: READY 1/1 表示这个 Pod 里 1 个容器中有 1 个已就绪; STATUS Running 表示正在运行; RESTARTS 是重启次数 (频繁重启是危险信号); AGE 是存活时长。
动手: 用 YAML 声明一个 Pod
但生产中我们几乎不用命令式, 而是写 YAML, 这才是声明式的正道。新建 pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: my-nginx
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80逐字段解读, 这四个顶层字段几乎所有 Kubernetes 对象都有, 记住它们就掌握了通用结构:
apiVersion: 这个对象用哪个 API 版本来解析。不同对象归属不同的 API 组, Pod 属于最核心的组, 版本号就是v1; 后面 Deployment 属于apps组, 写成apps/v1。kind: 对象类型, 这里是Pod。metadata: 元数据, 包括名字name和标签labels。labels是后面 Service、Deployment 找到这个 Pod 的依据, 务必留意, 第六、七章会反复用到。spec: 期望状态的具体描述 (spec 是 specification 的缩写)。这里声明了要跑一个名为nginx、镜像为nginx:alpine、监听 80 端口的容器。
应用并验证:
# apply 是声明式的核心命令: 把 YAML 描述的期望状态提交给集群
kubectl apply -f pod.yaml
# 进入 Pod 内部 (类比 docker exec)
kubectl exec -it my-nginx -- sh
# 查看 Pod 日志 (类比 docker logs)
kubectl logs my-nginx
# 把 Pod 的 80 端口临时映射到本机, 用于调试
kubectl port-forward my-nginx 8080:80
# 此时浏览器访问 http://localhost:8080 能看到 nginx 欢迎页你会发现 kubectl exec、kubectl logs 和 Docker 的 docker exec、docker logs 几乎一一对应, 上手并不陌生。
Pod 是脆弱的, 不要直接管理它
Pod 被设计成"用完即弃"(临时性的): 它没有自愈能力, 删了就没了; 节点宕机, 上面的 Pod 也不会自动迁移; 它的 IP 在重建后还会变。所以生产中我们从不直接创建裸 Pod, 而是用下一章的 Deployment 来管理它们。直接写 Pod 仅用于学习和临时调试。
删掉这个裸 Pod, 进入正题:
kubectl delete -f pod.yaml六、Deployment: 让应用拥有自愈能力
如果说 Pod 类比 docker run 的单个容器, 那么 Deployment 就类比 docker-compose——但它远不止编排, 还附带了自愈、多副本和滚动更新。这是你在生产中部署无状态应用最常用的对象, 没有之一。
名词: 什么是无状态应用
无状态 (Stateless) 指应用自身不在本地保存数据, 任意一个副本都能处理任意请求, 副本之间可以随意替换。典型如 Web 服务器、API 服务。与之相对的是有状态 (Stateful) 应用, 如数据库, 每个实例有自己独立且不可替换的数据。无状态应用用 Deployment, 有状态应用用后面会提到的 StatefulSet。
Deployment 的职责是: 你声明"我要 N 个某种 Pod 一直运行着", 它就通过控制循环死死维持这个数量。
动手: 部署一个 3 副本的应用
新建 deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3 # 期望副本数: 始终维持 3 个 Pod
selector:
matchLabels:
app: web # 管理哪些 Pod: 标签为 app=web 的
template: # Pod 模板: 照这个样子创建 Pod
metadata:
labels:
app: web # 创建出的 Pod 会带上这个标签
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80这里出现了 Kubernetes 中极其重要的设计——标签 (Label) 与选择器 (Selector):
template.metadata.labels给生成的每个 Pod 打上app: web标签。标签就是贴在对象上的键值对小纸条, 可以任意自定义。selector.matchLabels声明这个 Deployment 负责管理所有带app: web标签的 Pod。选择器就是"按纸条筛选对象"的条件。
二者必须匹配。这种"通过标签松耦合地关联对象"的机制贯穿整个 Kubernetes——对象之间不靠硬编码的 ID 互相引用, 而是靠标签动态匹配, 非常灵活。Service 找 Pod 也是靠它。
名词: ReplicaSet, 以及它和 Deployment 的关系
你声明的是 Deployment, 但它并不直接管 Pod, 而是创建一个 ReplicaSet (副本集) 来维持副本数, ReplicaSet 再管 Pod。ReplicaSet 的唯一职责就是"保证某标签的 Pod 数量恒等于 N"。那 Deployment 多出来的价值是什么? 是版本管理: 每次你更新镜像, Deployment 会新建一个 ReplicaSet 并逐步切换, 旧的留着以备回滚。平时你只和 Deployment 打交道, ReplicaSet 在背后自动工作即可。这一点在第十一章讲滚动更新时会真正派上用场。
应用并亲眼见证自愈能力:
kubectl apply -f deployment.yaml
# 查看 3 个 Pod 都起来了 (-l app=web 表示只看带这个标签的)
kubectl get pods -l app=web
# 见证自愈: 手动删掉其中一个 Pod
kubectl delete pod <某个pod名字>
# 立刻再查, 会发现马上有一个新 Pod 被自动创建出来, 数量始终保持 3
kubectl get pods -l app=web删掉一个, 它立刻补一个——这就是声明式与控制循环的威力, 你什么额外逻辑都没写。背后发生的事是: ReplicaSet 控制器通过控制循环发现"实际 2 个 ≠ 期望 3 个", 立即创建一个新 Pod 补齐。
扩缩容只是改一个数字
想从 3 个扩到 5 个, 两种方式:
# 方式一: 命令式直接改
kubectl scale deployment web --replicas=5
# 方式二 (推荐): 改 YAML 里的 replicas: 5, 再 apply
kubectl apply -f deployment.yaml声明式的好处在这里体现得淋漓尽致
方式二意味着你的 YAML 文件就是系统状态的唯一真相。把它纳入 Git 管理, 任何变更都有记录、可回溯、可 code review。这套"用 Git 仓库描述集群期望状态, 由工具自动同步到集群"的实践, 后来发展成了一个专门的方法论叫 GitOps, 是目前生产环境的主流做法, 第十九章会再提到。
七、Service: 稳定的访问入口
现在有 3 个 Pod 在跑, 但有个棘手问题: Pod 的 IP 是不稳定的。 Pod 随时可能被销毁重建 (扩缩容、更新、故障迁移), 每次重建 IP 都会变。前端或其他服务该用哪个 IP 来访问它们? 总不能每次都去查一遍。
Service 就是来解决这个问题的: 它给一组 Pod 提供一个固定不变的虚拟 IP 和 DNS 名字, 并自动把请求负载均衡到背后健康的 Pod 上。 这正是你熟悉的 Nginx upstream 在做的事, 只不过 Service 会随着 Pod 的增删自动更新后端列表, 完全无需你手动维护。
Service 的三种常见类型
- ClusterIP (默认): 只在集群内部可访问, 用于服务间互相调用。最常用。
- NodePort: 在每个节点上开一个固定端口 (范围 30000-32767), 把外部流量导进来。多用于测试或简单暴露。
- LoadBalancer: 对接云厂商的负载均衡器, 分配一个公网 IP。生产对外暴露时使用 (本地集群一般不支持这种类型)。
动手: 给 Deployment 加一个 Service
新建 service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
type: ClusterIP
selector:
app: web # 关键: 把流量转发给所有 app=web 的 Pod
ports:
- port: 80 # Service 自身暴露的端口
targetPort: 80 # 转发到 Pod 容器的端口注意 selector: app=web——Service 同样是靠标签找到要转发的 Pod, 它和 Deployment 之间没有直接引用关系, 而是都通过 app=web 这个标签松耦合地关联起来。这意味着你甚至可以让一个 Service 同时指向多个 Deployment 的 Pod, 只要它们带相同标签, 这在灰度发布时很有用。
kubectl apply -f service.yaml
# 查看 service, 会看到一个 CLUSTER-IP
kubectl get svc web-svc验证负载均衡。临时起一个带 curl 的 Pod, 在集群内部访问 Service:
kubectl run tester --image=curlimages/curl -it --rm -- sh
# 在容器内执行, 多访问几次。注意可以直接用 service 名字作为域名
curl http://web-svc深入: Service 究竟如何找到 Pod
这里揭开一层底层机制, 帮你建立更扎实的心智模型。Service 并不是直接和 Pod 对话的, 中间还有一个你平时看不见的对象 Endpoints (端点)。
机制是这样的: 你创建 Service 后, 一个专门的控制器会持续扫描所有匹配标签的健康 Pod, 把它们的真实 IP 维护成一份清单, 存进与 Service 同名的 Endpoints 对象里。Pod 增删或健康状态变化时, 这份清单实时更新。而每个节点上的 kube-proxy 监听这份清单, 把它翻译成本机的网络转发规则。当你访问 Service 的固定 IP 时, 内核根据这些规则随机挑一个清单里的 Pod IP 转发过去——这就是负载均衡的真相。
# 亲眼看看这份隐藏的 IP 清单
kubectl get endpoints web-svc名词: CoreDNS 与集群内 DNS
你能直接用 http://web-svc 而不用记 IP, 是因为 Kubernetes 内置了一个 DNS 服务, 实现叫 CoreDNS (跑在 kube-system 命名空间里)。每创建一个 Service, CoreDNS 就自动为它登记一条 DNS 记录, 完整形式是 服务名.命名空间.svc.cluster.local。同命名空间内直接用服务名即可, 跨命名空间则要带上命名空间名 (如 web-svc.demo)。这套机制叫服务发现 (Service Discovery), 让你在代码里写死 http://web-svc 这样的地址就行, 无需关心 IP。
八、Ingress: 集群对外的流量网关
ClusterIP 只能内部访问, NodePort 又只能按端口暴露、不够灵活。当你需要按域名和路径把外部 HTTP 流量分发到不同服务时, 就轮到 Ingress 出场了。
Ingress 干的事, 几乎就是你天天写的 Nginx 反向代理配置: 把 api.example.com 路由到后端 API 服务, 把 example.com/admin 路由到管理后台, 顺便统一处理 HTTPS 证书。
关键区分: Ingress 资源和 Ingress Controller 是两回事
这是新手必踩的坑。Ingress 只是一份"路由规则"的声明 (一个 YAML 对象), 它本身不处理任何流量。真正干活的是 Ingress Controller (入口控制器)——一个常驻集群的反向代理程序, 它读取你写的所有 Ingress 规则并据此转发流量。最常见的实现就是 ingress-nginx, 底层正是一个 Nginx。所以你必须先在集群里装一个 Ingress Controller, Ingress 规则才会生效, 否则你写的规则只是一堆没人执行的纸面声明。
动手: 配置基于域名的路由
先安装 ingress-nginx 控制器 (kind 环境专用的部署清单):
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# 等待控制器就绪 (wait 会阻塞直到条件满足或超时)
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s新建 ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: web.local # 访问的域名
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc # 转发到第七章创建的 Service
port:
number: 80解释两个可能让你卡住的字段:
annotations(注解): 和 labels 一样是键值对, 但用途不同。labels 用于筛选对象, annotations 用于附加配置信息, 通常是给某个控制器读的。这里的nginx.ingress.kubernetes.io/rewrite-target: /就是专门写给 ingress-nginx 看的指令, 含义是"转发前把请求路径重写为/"。pathType: Prefix: 路径匹配方式,Prefix表示前缀匹配 (/能匹配所有路径)。
这份 YAML 表达的规则, 用你熟悉的 Nginx 配置类比就是:
server {
server_name web.local;
location / {
proxy_pass http://web-svc:80;
}
}是不是非常眼熟? 应用并测试:
kubectl apply -f ingress.yaml
# 因为 web.local 不是真实域名, 用 curl 手动指定 Host 头来模拟访问
curl -H "Host: web.local" http://localhost至此, 你已经把"外部请求 → Ingress → Service → Pod"这条完整的流量链路打通了。回顾一下, 它和传统架构里"Nginx → upstream → 应用服务器"几乎一一对应, 只是每一层都变成了可声明、可自愈、可自动伸缩的对象。
九、ConfigMap 与 Secret: 配置与代码解耦
镜像应该是"一次构建、到处运行"的, 不该把数据库地址、密钥这类随环境变化的配置硬编码进去 (否则开发、测试、生产就得各构建一个镜像)。你在 Docker 时代可能用 .env 文件或 -e 参数注入。Kubernetes 把这件事标准化成两个对象:
- ConfigMap: 存放非敏感的配置, 如服务地址、功能开关、整个配置文件内容。
- Secret: 存放敏感信息, 如密码、Token、TLS 证书。用法和 ConfigMap 几乎一样, 只是值会做 Base64 编码并可配合加密存储。
名词: Base64 不是加密, Secret 默认并不安全
Base64 是一种把任意数据转成纯文本字符的编码方式, 目的是方便传输, 它是公开可逆的, 任何人都能一键解码——它和"加密"毫无关系。Secret 的值默认只做了 Base64 编码, 所以不要因为名字叫 Secret 就以为它天然安全。生产环境需要额外开启 etcd 静态加密 (Encryption at Rest), 或使用外部密钥管理方案 (如 HashiCorp Vault、云厂商 KMS), 才能真正保护敏感数据。
动手: 注入配置到容器
创建一个 ConfigMap 和一个 Secret:
# 命令式创建 ConfigMap (--from-literal 表示直接给字面值)
kubectl create configmap app-config \
--from-literal=API_URL=http://api-svc \
--from-literal=LOG_LEVEL=info
# 命令式创建 Secret
kubectl create secret generic app-secret \
--from-literal=DB_PASSWORD=s3cr3t也可以用 YAML 声明 (推荐, 便于纳入版本管理):
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
API_URL: http://api-svc
LOG_LEVEL: info在 Deployment 中, 有两种主流方式把它们注入 Pod。
方式一: 作为环境变量注入
spec:
containers:
- name: app
image: my-app:1.0
env:
- name: API_URL
valueFrom:
configMapKeyRef: # 从 ConfigMap 取值
name: app-config
key: API_URL
- name: DB_PASSWORD
valueFrom:
secretKeyRef: # 从 Secret 取值
name: app-secret
key: DB_PASSWORD容器内通过 process.env.API_URL (Node) 或对应语言的方式即可读到, 和你用 .env 的体验完全一致。
方式二: 作为文件挂载
把整个 ConfigMap 挂成一个目录, 每个 key 变成一个文件, 适合注入完整的配置文件 (如 nginx.conf、application.yml):
spec:
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: config-volume
mountPath: /etc/config # 挂载到容器内这个目录
volumes:
- name: config-volume
configMap:
name: app-config挂载后, 容器内 /etc/config/API_URL 文件的内容就是 http://api-svc。
何时用环境变量, 何时用文件挂载
零散的几个配置项用环境变量最简单; 如果是一整个配置文件 (比如要替换 Nginx 的 default.conf), 用文件挂载更合适。一个关键差异: 环境变量在容器启动时就固定了, 之后改 ConfigMap 不会自动生效, 必须重启 Pod; 而文件挂载方式下, ConfigMap 更新后挂载的文件会被自动同步更新 (但应用是否重新读取这个文件, 取决于程序自身的实现)。
十、存储: 让数据活得比 Pod 久
Pod 是短暂的, 容器内写的文件随 Pod 销毁而消失。对于数据库、用户上传文件这类需要持久保存数据的场景, 必须把数据存到 Pod 之外。Kubernetes 用一组存储抽象来解决, 名词较多, 逐个拆解:
- Volume (卷): 最基础的概念, 即挂载到容器里的一块存储。它有很多种类型, 最简单的
emptyDir生命周期跟 Pod 绑定 (Pod 删了数据就没了, 适合临时缓存); 其他类型可以对接外部持久存储。 - PersistentVolume (PV, 持久卷): 一块实际的存储资源, 比如一块云硬盘、一个 NFS 网络目录。它的生命周期独立于任何 Pod, Pod 没了它还在。
- PersistentVolumeClaim (PVC, 持久卷申领): 应用对存储的"申请单"——我要 10Gi、需要可读写。Pod 不直接使用 PV, 而是通过 PVC 来申领。
- StorageClass (存储类): 实现存储的动态供给——PVC 一提交, 就根据 StorageClass 自动创建出对应的 PV, 无需管理员手动准备。
PVC 与 PV 的关系, 可以类比成"你向供电局提交一张用电申请 (PVC), 供电局给你接通一条实际线路 (PV)"。应用只管提交申请, 不关心背后是哪块物理盘, 这就是解耦: 开发者声明需求, 存储细节交给平台。
动手: 为应用申领一块持久存储
新建 pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce # 访问模式: 可被单个节点读写挂载
resources:
requests:
storage: 1Gi # 申领 1Gi 空间accessModes 常见三种: ReadWriteOnce (单节点读写, 最常用)、ReadOnlyMany (多节点只读)、ReadWriteMany (多节点读写, 需要特定存储后端支持)。
在 Pod 中挂载它:
spec:
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: data
mountPath: /var/lib/app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvc这样, 即使 Pod 被删除重建, /var/lib/app/data 里的数据依然保留在 PVC 绑定的存储中。
名词: 有状态应用要用 StatefulSet
本文用的 Deployment 适合无状态应用 (任意副本可互相替代)。但数据库、消息队列、ZooKeeper 这类有状态应用, 每个副本有自己稳定的身份和独立存储, 需要用专门的 StatefulSet 来管理。它相比 Deployment 多保证三件事: Pod 有固定且有序的名字 (如 mysql-0、mysql-1, 而不是随机后缀)、固定的启动与伸缩顺序、以及每个 Pod 独享一块由模板自动创建的 PVC。入门阶段了解其存在即可, 真正部署数据库时再深入。生产中也常用 Operator (第十八章) 来管理这类复杂有状态应用。
十一、健康检查与优雅运维
让应用"跑起来"只是第一步, 生产环境真正考验的是"出问题时能否自愈"和"发版时能否不中断"。这一章是从入门迈向实战的关键。
探针: 让 Kubernetes 读懂应用的健康状态
Kubernetes 怎么知道一个 Pod 是真的健康、能对外服务? 容器在 Running 不代表里面的应用真的就绪 (可能还在加载、或已死锁假活)。判断依据是你配置的探针 (Probe)。有三种, 各司其职:
- livenessProbe (存活探针): 检测应用是否还活着。失败则重启容器。用于自愈死锁、卡死的进程。
- readinessProbe (就绪探针): 检测应用是否准备好接收流量。失败则把该 Pod 从 Service 的负载均衡清单 (Endpoints) 中摘除, 但不重启。用于应对启动慢、临时过载、依赖未就绪。
- startupProbe (启动探针): 用于启动特别慢的应用 (如某些 Java 服务), 在它成功之前, 暂缓上面两个探针, 避免应用还在正常启动就被存活探针误杀。
存活与就绪的区别非常关键, 切勿配反
新手最容易混淆这两个。打个比方: 就绪探针失败, 像店员说"我先忙别的, 暂时别给我派单", 顾客 (流量) 会被引导到其他店员, 他本人还在岗; 存活探针失败, 像发现店员晕倒了, 直接换一个新人顶上 (重启容器)。一个调度流量、一个触发重启, 配反了后果很严重——比如把一个"启动慢"误配成存活探针, 应用会在启动期被反复杀死, 陷入无限重启。
在容器中配置探针:
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health # 应用需自己提供一个健康检查接口
port: 3000
initialDelaySeconds: 5 # 容器启动后等 5 秒再开始探测
periodSeconds: 10 # 之后每 10 秒探一次
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20资源请求与限制
每个容器都应当声明它需要多少 CPU 和内存。这直接关系到调度决策和集群稳定性:
spec:
containers:
- name: app
image: my-app:1.0
resources:
requests: # 请求量: 调度器据此决定把 Pod 放到哪个节点
cpu: "100m" # 100 毫核 = 0.1 个 CPU 核心
memory: "128Mi" # 128 兆字节
limits: # 限制量: 容器最多能用这么多
cpu: "500m"
memory: "256Mi"名词: 毫核 (m) 与内存单位
CPU 用毫核 (millicore) 计量, 1000m = 1 个 CPU 核心, 所以 100m 就是 0.1 核, 500m 是半核。内存用 Mi (Mebibyte, 1Mi=1024Ki) 或 Gi 计量。
requests 是"我至少需要这么多", 调度器拿它和节点剩余资源做匹配, 决定能不能放下。limits 是"我最多用这么多", 超过内存 limit 容器会被强制杀掉 (状态显示 OOMKilled, Out Of Memory), 超过 CPU limit 则会被限流 (throttle) 而非杀死。
不设资源限制的隐患
如果不设 limits, 一个有内存泄漏的容器可能吃光整台节点的内存, 拖垮同节点上的其他无辜 Pod, 引发连锁雪崩。生产环境务必为关键应用设置合理的 requests 和 limits, 这是集群稳定性的基本盘。
滚动更新与回滚
发布新版本时, Deployment 默认采用滚动更新 (RollingUpdate): 逐个用新版本 Pod 换旧版本, 全程保持一定数量的 Pod 始终可用, 从而实现零停机发布。
# 触发更新: 修改镜像版本
kubectl set image deployment/web nginx=nginx:1.25-alpine
# 实时观察滚动更新过程
kubectl rollout status deployment/web
# 查看更新历史版本
kubectl rollout history deployment/web整个过程像这样, 新旧副本交替进行, 总量不掉到水位线以下:
如果新版本有问题, 一条命令回滚到上一版, 这是 Deployment 最让人安心的能力之一:
# 回滚到上一个版本
kubectl rollout undo deployment/web
# 回滚到指定版本
kubectl rollout undo deployment/web --to-revision=2深入: 为什么能秒级回滚
还记得第六章说的 ReplicaSet 吗? 现在它的价值兑现了。每次更新, Deployment 会创建一个新的 ReplicaSet 承载新版本, 同时把旧 ReplicaSet 的副本数逐步缩到 0, 但并不删除它。所以集群里其实留着每个历史版本对应的 (副本数为 0 的) ReplicaSet。回滚时, 只需把旧 ReplicaSet 的副本数重新拉起、把当前的缩回去即可, 无需重新拉镜像或重建配置, 因此非常快。这就是 Deployment 在 ReplicaSet 之上额外管理"版本"这件事的全部意义。
十二、自动扩缩容
手动 kubectl scale 终究是被动的。Kubernetes 提供 HorizontalPodAutoscaler (HPA, 水平 Pod 自动伸缩器), 根据 CPU、内存或自定义指标自动调整副本数, 流量高峰自动扩容、低谷自动缩容。
名词: 水平伸缩 vs 垂直伸缩
水平伸缩 (Horizontal) 指增减副本数量 (多开几个 Pod), 这是云原生的主流做法。垂直伸缩 (Vertical) 指调整单个 Pod 的资源规格 (给它更多 CPU/内存)。HPA 做的是水平伸缩。
HPA 依赖 Metrics Server 这个组件来获取 Pod 的实时资源用量 (集群默认不一定装了它, 需先安装)。然后创建 HPA:
# 为 web 这个 Deployment 创建 HPA:
# 目标是让 CPU 平均利用率维持在 50%, 副本数在 2 到 10 之间自动伸缩
kubectl autoscale deployment web --cpu-percent=50 --min=2 --max=10
# 查看 HPA 状态
kubectl get hpa对应的 YAML 写法:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web # 伸缩哪个 Deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # 目标: 平均 CPU 利用率 50%HPA 生效的前提条件
HPA 按 CPU 利用率伸缩, 而利用率的计算公式是 实际用量 / requests。所以容器必须设置了 resources.requests.cpu, HPA 才能算出这个百分比, 否则它不知道分母是多少, 直接失效。这正是第十一章的资源配置与本章自动扩缩容的衔接点——前面的铺垫不是白做的。
十三、实战: 部署一个完整的前后端应用
把前面所有零件组装起来, 部署一个真实的两层应用: 一个后端 API (无状态) 加一个前端 (Nginx 托管静态资源), 通过 Ingress 统一对外。这个示例几乎涵盖了日常部署一个 Web 应用所需的全部对象, 是对前十二章的总检验。
整体架构:
把所有资源写进一个 app.yaml, 用 --- 分隔多个对象 (这是 YAML 的多文档语法, 一个文件里放多个对象, 类比 docker-compose 里的多个 service):
# ---------- 后端配置 ----------
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
LOG_LEVEL: info
---
# ---------- 后端 Deployment ----------
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: hashicorp/http-echo:latest # 一个极简的回显服务, 便于演示
args:
- "-text=hello from api"
- "-listen=:5678"
ports:
- containerPort: 5678
envFrom:
- configMapRef:
name: api-config # 把整个 ConfigMap 注入为环境变量
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "200m"
memory: "64Mi"
readinessProbe:
httpGet:
path: /
port: 5678
initialDelaySeconds: 3
periodSeconds: 10
---
# ---------- 后端 Service ----------
apiVersion: v1
kind: Service
metadata:
name: api-svc
spec:
selector:
app: api
ports:
- port: 80
targetPort: 5678
---
# ---------- 前端 Deployment ----------
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:alpine
ports:
- containerPort: 80
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "200m"
memory: "64Mi"
---
# ---------- 前端 Service ----------
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
---
# ---------- 统一 Ingress ----------
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.local
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80一次性部署整套应用:
kubectl apply -f app.yaml
# 查看所有资源状态 (一条命令查多种类型)
kubectl get deploy,svc,ingress,pods
# 测试前端
curl -H "Host: app.local" http://localhost/
# 测试后端
curl -H "Host: app.local" http://localhost/api一条 kubectl apply 就拉起了 4 个 Pod、2 个 Service、1 个 Ingress 和配置, 并且它们全部具备自愈、可滚动更新、可水平扩展的能力。对比你过去在一台服务器上手动配 Nginx、起进程、写守护脚本的工作量, Kubernetes 的价值就具体地显现出来了。
清理整套环境也只需一条命令:
kubectl delete -f app.yaml十四、排错指南
部署不可能一帆风顺。掌握一套排错的常规手法, 比记住所有 YAML 字段更重要。下面是按使用频率排序的核心命令。
# 1. 全局扫一眼, 找出状态不对的 Pod
kubectl get pods -A
# 2. 看某个 Pod 的详细信息, 重点看底部 Events 事件区
kubectl describe pod <pod名>
# 3. 看应用日志
kubectl logs <pod名>
kubectl logs <pod名> -f # 实时跟踪 (类比 tail -f)
kubectl logs <pod名> --previous # 看上一个挂掉的容器的日志, 排查崩溃极有用
# 4. 进入容器内部排查
kubectl exec -it <pod名> -- sh
# 5. 看命名空间内的事件流 (按时间排序)
kubectl get events --sort-by='.lastTimestamp'最常见的几种 Pod 异常状态及排查方向:
| STATUS 状态 | 含义 | 优先排查方向 |
|---|---|---|
Pending | Pod 还没被调度到任何节点 | 节点资源不足, 或 requests 设得过大; PVC 未绑定; 存在不满足的调度约束 |
ImagePullBackOff | 拉取镜像失败 | 镜像名/Tag 拼错; 私有仓库未配凭证; 网络不通 |
CrashLoopBackOff | 容器反复启动又崩溃 | 看 logs --previous, 多为应用自身报错或配置/环境变量缺失 |
OOMKilled | 容器内存超过 limit 被杀 | 调高 memory limit, 或排查内存泄漏 |
Running 但 READY 0/1 | 在跑但就绪探针未通过 | 检查 readinessProbe 的路径与端口; 看应用是否真的起好了 |
排错的通用思路
遇到问题别慌, 固定按这个顺序走: get pods 看状态 → describe pod 看事件 (Events 区域往往直接用大白话写明了失败原因, 比如"内存不足无法调度""镜像拉取失败") → logs 看应用日志。九成的问题在这三步内就能定位。describe 的 Events 是最容易被新手忽略、却最有价值的信息源。
到这里, 你已经掌握了 Kubernetes 日常使用与运维的全部核心能力, 足以独立部署和维护大多数 Web 应用。下面的章节进入"精通"部分, 带你看清那些平时被平台默默处理掉的底层机制——理解它们, 才能在复杂故障和架构决策面前不慌。
十五、深入控制平面: 一次 apply 背后发生了什么
前面我们一直在用 kubectl apply, 现在把它拆开, 看清一个对象从你敲下命令到真正运行起来, 整个控制平面是如何协作的。这是理解 Kubernetes "声明式"本质的核心一课。
以"创建一个 3 副本的 Deployment"为例, 完整链路如下:
这里藏着 Kubernetes 最精妙的设计思想, 值得拆解:
- 一切通过 API Server, 一切存于 etcd: 没有任何组件直接互相调用。
kubectl、控制器、调度器、kubelet 之间从不直接通信, 全部围绕 API Server 读写状态。这种中心化的设计让系统极其解耦。 - 声明与执行分离: 你 apply 之后, API Server 只是把"期望"存进 etcd 就立即返回了, 此刻什么都还没运行。真正干活的是各个控制器和 kubelet, 它们各自异步地把现实朝期望推进。
- 基于监听的事件驱动: 各组件都在"监听 (watch)"自己关心的对象变化。Deployment 控制器盯着 Deployment, 调度器盯着"没分配节点的 Pod", kubelet 盯着"分给本节点的 Pod"。每个组件只做自己那一小步, 然后把结果写回 API Server, 触发下一个组件。
声明式的深层威力: 自愈是免费的
理解了上面的链路, 就理解了为什么 Kubernetes 能自愈。控制器不是"创建完就完事", 而是永不停止地循环对比期望与实际。Pod 挂了, 实际状态变化被写回 etcd, ReplicaSet 控制器立刻监听到"少了一个", 于是再创建一个——它根本不区分"首次创建"和"故障恢复", 对它而言都只是"让实际等于期望"这一件事。自愈不是一个额外功能, 而是声明式架构的自然结果。
十六、深入调度: Pod 是如何被放到合适节点的
第十五章里 Scheduler 那一步"计算后绑定到节点", 内部其实很有讲究。当你有几十台机器时, 一个新 Pod 该放哪? 调度器分两阶段决策:
- 过滤 (Filtering): 排除掉所有不满足硬性条件的节点。比如剩余资源不够 (装不下这个 Pod 的 requests)、节点有 Pod 不能容忍的"污点"、不满足指定的节点选择条件。
- 打分 (Scoring): 在剩下的可行节点中, 按一系列规则打分 (如资源最空闲的得分高、镜像已缓存的得分高), 选分数最高的。
大多数时候用默认策略即可。但精细化运维时, 你会需要主动干预 Pod 的落点, 主要有三类工具:
nodeSelector 与节点亲和性
最简单的方式是 nodeSelector: 给节点打标签, 让 Pod 只去带特定标签的节点。比如把需要 GPU 的任务调度到 GPU 机器:
# 先给节点打标签
kubectl label nodes <节点名> gpu=truespec:
nodeSelector:
gpu: "true" # 这个 Pod 只会被调度到带 gpu=true 标签的节点节点亲和性 (Node Affinity) 是 nodeSelector 的增强版, 支持"硬性要求"和"软性偏好"两种力度, 表达力更强。
污点与容忍
名词: 污点 (Taint) 与容忍 (Toleration)
这是一对反向机制, 理解起来稍绕, 用类比说清: 污点是给节点贴的一张"拒绝标签", 比如给控制平面节点打上污点, 默认就没有普通 Pod 愿意去。容忍则是给 Pod 配的一张"通行证", 表示"我能容忍某种污点"。只有持有对应容忍的 Pod, 才能被调度到带该污点的节点上。
一句话: 污点是节点说"别来", 容忍是 Pod 说"我例外"。常用于保留专用节点 (如把某些机器只留给特定业务)。
# 给节点打污点: 键=值:效果。NoSchedule 表示不容忍就别来
kubectl taint nodes <节点名> dedicated=ml:NoSchedulespec:
tolerations: # Pod 声明能容忍这个污点
- key: "dedicated"
operator: "Equal"
value: "ml"
effect: "NoSchedule"Pod 间亲和与反亲和
还能控制 Pod 与 Pod 之间的相对位置。反亲和 (Anti-Affinity) 最常用: 让同一个应用的多个副本尽量分散到不同节点, 这样单台机器宕机不会让服务全挂, 显著提升可用性。这正是生产环境保障高可用的关键配置之一。
十七、深入网络与安全
这两块是从"会用"走向"精通"绕不开的深水区, 这里建立框架性认知, 知道有哪些概念、分别解决什么, 便于日后按需深入。
集群网络模型
Kubernetes 对网络有一条硬性规定: 每个 Pod 拥有独立 IP, 且所有 Pod 之间可以不经 NAT 直接互通, 无论它们在不在同一台机器。这套"扁平网络"极大简化了应用通信 (Pod 之间就像在同一个局域网里), 但 Kubernetes 自己并不实现它, 而是定义了一套接口, 交给插件去实现。
名词: CNI 与网络插件
CNI (Container Network Interface, 容器网络接口) 是一套标准接口, 规定了"如何给 Pod 配置网络"。具体实现由第三方网络插件完成, 常见的有 Flannel (简单)、Calico (功能强、支持网络策略)、Cilium (基于 eBPF, 高性能)。你装集群时必须选一个 CNI 插件, 否则 Pod 之间无法通信, 节点会一直处于 NotReady。这也解释了为什么有时 Pod 卡在 Pending/ContainerCreating——可能是 CNI 没装好。
默认情况下所有 Pod 可以互相访问, 这在生产中往往不安全。NetworkPolicy (网络策略) 让你像配防火墙一样, 声明"哪些 Pod 允许访问哪些 Pod", 实现微隔离 (注意: NetworkPolicy 需要 CNI 插件支持才生效, 如 Calico/Cilium)。
CIDR: 看懂网段表示法
名词: CIDR
你会频繁看到 10.244.0.0/16 这样的写法, 这叫 CIDR (无类别域间路由) 表示法, 用来描述一个 IP 网段。斜杠后的数字表示"前多少位是固定的网络前缀", 剩下的位可分配给主机。/16 意味着前 16 位固定, 后 16 位可变, 即约 6.5 万个地址。集群会划定两个关键网段: Pod CIDR (所有 Pod IP 从这里分配) 和 Service CIDR (所有 Service 的 ClusterIP 从这里分配), 部署集群时需要规划好它们不与现有网络冲突。
访问控制: RBAC 与 ServiceAccount
集群不能任由所有人和所有程序为所欲为, 必须有权限控制。
名词: RBAC 与 ServiceAccount
RBAC (Role-Based Access Control, 基于角色的访问控制) 是 Kubernetes 的权限模型, 由三部分组成: Role (角色, 定义"能对哪些资源做哪些操作", 如"能读取 Pod")、Subject (主体, 即"谁", 可以是用户或程序)、RoleBinding (绑定, 把角色授予主体)。
ServiceAccount (服务账号) 是给程序/Pod 用的身份 (区别于给真人用的 User Account)。每个 Pod 运行时都关联一个 ServiceAccount, 当 Pod 内的程序需要调用 Kubernetes API 时 (比如一个需要管理其他 Pod 的控制器), 就用这个身份, 并受 RBAC 规则约束。最小权限原则在这里至关重要: 只给程序授予它确实需要的权限。
一个典型的 RBAC 关系:
十八、扩展 Kubernetes: CRD 与 Operator
到这里你可能会问: Kubernetes 内置的 Pod、Deployment、Service 这些对象是固定的吗? 能不能定义我自己的对象? 能, 这正是 Kubernetes 最强大的特性之一, 也是它能成为"云原生操作系统"的根本原因。
名词: CRD (自定义资源定义)
CRD (Custom Resource Definition) 允许你向 Kubernetes 注册一种全新的对象类型。注册之后, 你就能像使用内置对象一样, 用 YAML 声明这个自定义对象, 并用 kubectl get 查询它。比如你可以定义一种叫 Database 的资源, 然后写 kind: Database 来声明"我要一个数据库"。CRD 只是定义了"这种对象长什么样", 但光有定义, 声明了也没人理它——还需要一个控制器来赋予它实际行为。
这就引出了 Operator 模式——Kubernetes 进阶的集大成者。
Operator 的核心思想是: 把人类运维专家的知识, 编写成一个自动化的控制器程序。 它 = CRD (定义自定义对象) + 自定义控制器 (实现对该对象的控制循环)。
举个具体例子: 运维一个 MySQL 主从集群需要大量专业知识——如何初始化、如何配置主从复制、主库挂了如何自动故障切换、如何定时备份。一个 MySQL Operator 把这些知识全部代码化后, 你只需声明:
apiVersion: mysql.example.com/v1
kind: MySQLCluster
metadata:
name: my-db
spec:
replicas: 3
version: "8.0"
backupSchedule: "0 2 * * *" # 每天凌晨 2 点自动备份提交后, MySQL Operator (它本身也是跑在集群里的一个 Pod) 就会监听到这个对象, 自动完成创建 3 个有状态 MySQL 实例、配置主从、设置定时备份等一系列复杂操作, 并持续维护——主库挂了它自动切换, 完全无需人工介入。
这就是复杂平台的实现底座
你之前接触的 Kubeflow, 以及各种"一键部署"的数据库、消息队列、监控系统, 底层几乎都是用 CRD + Operator 实现的。理解了这个模式, 你看待 Kubernetes 生态的视角会完全不同——它不是一堆固定功能的集合, 而是一个可以被无限扩展的平台, 任何复杂系统的运维逻辑都能被封装成声明式的对象。这是"精通"与"会用"的分水岭。
十九、生产化与进阶生态
掌握了核心对象和底层机制后, 走向真实生产环境, 还有一层工具化与工程化的能力需要补齐。这一章为你画出后续的学习地图。
Helm: Kubernetes 的包管理器
当 YAML 文件越来越多、还要为开发/测试/生产维护不同参数时, 手工管理会非常痛苦 (大量重复、易出错)。Helm 把一组相关的 YAML 打包成可复用、可参数化的 Chart, 类比前端世界的 npm 或 Linux 的 apt: 一条命令就能安装一整套复杂应用。
# 添加仓库并安装一个 Redis, 一条命令搞定原本几百行的 YAML
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-redis bitnami/redis
# 通过参数定制, 无需改模板本身
helm install my-redis bitnami/redis --set auth.password=mypassChart 用模板语法把可变部分抽成参数 (放在 values.yaml), 同一套模板配不同参数即可部署到不同环境, 这是生产中管理应用的标准方式。
GitOps: 用 Git 驱动集群
第六章提到的 GitOps 在这里展开: 核心理念是让 Git 仓库成为集群期望状态的唯一真相来源。你不再手动 kubectl apply, 而是把所有 YAML 提交到 Git, 由 ArgoCD 或 Flux 这类工具持续监听仓库, 自动把变更同步到集群, 并保证集群实际状态与 Git 始终一致。好处是: 所有变更可审计、可回滚 (就是 git revert)、可 code review, 与声明式哲学完美契合。
可观测性
生产系统必须"看得见"。这块通常由三大支柱构成, 建议作为独立专题深入:
- 指标监控 (Metrics): Prometheus 采集指标 + Grafana 可视化, 是云原生监控的事实标准。
- 日志 (Logging): 集中收集所有 Pod 的日志, 常见方案如 Loki、EFK (Elasticsearch + Fluentd + Kibana)。
- 链路追踪 (Tracing): 追踪一个请求跨多个微服务的完整调用链, 如 Jaeger、OpenTelemetry。
继续深入的方向清单
- 服务网格 (Service Mesh): Istio、Linkerd, 把流量治理 (灰度、熔断、加密) 从应用代码下沉到基础设施层。
- 调度进阶: 拓扑分布约束、优先级与抢占、自定义调度器。
- 安全加固: Pod 安全标准、镜像签名与准入控制 (Admission Webhook)、密钥管理 (Vault)。
- 多集群管理: 当单集群不够用时的联邦与多集群方案。
学习建议
不要试图一次吃透所有内容。最有效的路径是: 先用前十四章的核心对象把自己的真实项目跑起来, 在解决实际问题的过程中, 按需深入对应的专题。Kubernetes 的知识体系确实庞大, 但日常 80% 的工作只用到其中 20% 的核心概念。把核心用熟, 再带着真实问题去啃底层机制和进阶生态, 比一开始就埋头啃理论高效得多。
结语
回顾全文, 我们从 Docker 单机运行容器的局限出发, 理解了 Kubernetes 声明式的世界观, 铺垫了 YAML 与 kubectl 的基础, 然后沿着 Namespace、Pod、Deployment、Service、Ingress、ConfigMap、存储这条主线逐个动手, 补上健康检查、滚动更新、自动扩缩容这些生产必备能力, 用一个完整的前后端应用把知识点串联起来; 再往深处, 我们拆解了一次 apply 背后控制平面的协作、调度器的决策逻辑、网络与安全模型, 最后理解了 CRD 与 Operator 这一让 Kubernetes 得以无限扩展的核心机制, 并画出了通往生产化的进阶地图。
Kubernetes 的复杂度是真实存在的, 但它的复杂换来的是确定性: 你声明想要的状态, 平台负责让现实持续逼近它。从 Pod 的自愈, 到 Operator 自动运维一整套数据库集群, 背后都是同一个"控制循环"思想在不同层次上的展开。理解了这一核心, 再庞杂的对象和概念, 都只是这一思想的具体投影。
最后还是那句话: 动手把本文的每个示例亲手跑一遍, 远胜过反复阅读。从 kind create cluster 开始, 现在就动手吧。