Skip to content

Kubernetes 从入门到精通

作者:Atom
字数统计:15.8k 字
阅读时长:56 分钟

本文面向已经熟悉 Docker 与 Nginx、但尚未系统接触 Kubernetes 的开发者。全文遵循一条由浅入深的主线: 先从单机容器的真实痛点引出 Kubernetes 解决的问题, 再铺垫必要的预备知识, 然后逐个核心对象动手实践, 每个新名词在首次出现时都会就地解释清楚, 并尽量与你已经熟悉的 Docker、Nginx、docker-compose 做类比。掌握核心用法后, 再逐步深入控制循环、调度、网络、安全等底层机制, 最终走向生产实践与进阶生态。每一节都配有可以直接动手运行的示例, 跑一遍胜过读十遍。

一、从 Docker 的尽头说起

在理解 Kubernetes 是什么之前, 先要理解它到底解决了什么问题。假设你用 Docker 把一个 Node 服务打成镜像, 在一台服务器上这样跑起来:

bash
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) 的——你下达一条条具体指令, 系统照做:

bash
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 (这是新手最常见的报错来源)。
  • 键: 值 表示一个属性, 冒号后要有一个空格。
  • - 开头表示列表项 (数组的一个元素)。

一个最小的例子, 对照右侧注释理解:

yaml
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 环境可以直接复用。

bash
# 安装 kind 与 kubectl (Kubernetes 的命令行客户端)
brew install kind kubectl

# 创建一个名为 dev 的集群 (底层会启动一个 Docker 容器作为节点)
kind create cluster --name dev

# 验证集群可用, 查看节点
kubectl get nodes
bash
# 下载安装 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

如果一切正常, 最后一条命令会输出类似:

text
NAME                STATUS   ROLES           AGE   VERSION
dev-control-plane   Ready    control-plane   60s   v1.29.0

看到 Ready, 说明你的第一个 Kubernetes 集群已经跑起来了。

预备知识: 读懂 kubectl 命令结构

kubectl 是与集群交互的命令行客户端, 你后面会敲成百上千次。它的命令结构高度规整, 记住这个套路, 大部分命令不用查文档:

text
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 (进容器执行命令)。

配好别名和补全, 后面省力

bash
alias k=kubectl
# 开启命令补全 (以 zsh 为例, bash 把 zsh 换成 bash)
source <(kubectl completion zsh)

之后敲 k get po 再按 Tab 就能自动补全, 效率提升明显。

四、Namespace: 集群里的分区

正式部署应用前, 先认识一个贯穿始终的概念: Namespace (命名空间)

你之前用 kubectl get pods 看到的其实只是默认分区里的内容。一个集群里会同时跑很多东西: 你的业务应用、平台自身组件、监控、日志等。如果全堆在一起, 很快就会乱成一团。Namespace 就是集群内部的逻辑隔离分区, 类比一台服务器上用不同目录隔离不同项目, 或者数据库里的多个 database。

bash
# 查看集群里有哪些命名空间
kubectl get namespaces

# 你会看到一些内置的:
# default          你不指定时, 资源默认放这里
# kube-system      Kubernetes 自身组件 (DNS、kube-proxy 等) 所在地, 别乱动
# kube-public      公开信息
# kube-node-lease  节点心跳

几乎所有 kubectl 命令都可以加 -n <命名空间> 来指定操作哪个分区, 不加就是 default:

bash
# 创建一个自己的命名空间
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

最快的方式是命令式直接拉起, 适合临时验证:

bash
# 运行一个 nginx Pod
kubectl run my-nginx --image=nginx:alpine

# 查看 Pod 状态
kubectl get pods

# 看详细信息与事件 (排错第一手段)
kubectl describe pod my-nginx

kubectl get pods 会展示 Pod 的关键状态:

text
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:

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 和标签 labelslabels 是后面 Service、Deployment 找到这个 Pod 的依据, 务必留意, 第六、七章会反复用到。
  • spec: 期望状态的具体描述 (spec 是 specification 的缩写)。这里声明了要跑一个名为 nginx、镜像为 nginx:alpine、监听 80 端口的容器。

应用并验证:

bash
# 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 execkubectl logs 和 Docker 的 docker execdocker logs 几乎一一对应, 上手并不陌生。

Pod 是脆弱的, 不要直接管理它

Pod 被设计成"用完即弃"(临时性的): 它没有自愈能力, 删了就没了; 节点宕机, 上面的 Pod 也不会自动迁移; 它的 IP 在重建后还会变。所以生产中我们从不直接创建裸 Pod, 而是用下一章的 Deployment 来管理它们。直接写 Pod 仅用于学习和临时调试。

删掉这个裸 Pod, 进入正题:

bash
kubectl delete -f pod.yaml

六、Deployment: 让应用拥有自愈能力

如果说 Pod 类比 docker run 的单个容器, 那么 Deployment 就类比 docker-compose——但它远不止编排, 还附带了自愈、多副本和滚动更新。这是你在生产中部署无状态应用最常用的对象, 没有之一。

名词: 什么是无状态应用

无状态 (Stateless) 指应用自身不在本地保存数据, 任意一个副本都能处理任意请求, 副本之间可以随意替换。典型如 Web 服务器、API 服务。与之相对的是有状态 (Stateful) 应用, 如数据库, 每个实例有自己独立且不可替换的数据。无状态应用用 Deployment, 有状态应用用后面会提到的 StatefulSet。

Deployment 的职责是: 你声明"我要 N 个某种 Pod 一直运行着", 它就通过控制循环死死维持这个数量。

动手: 部署一个 3 副本的应用

新建 deployment.yaml:

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 在背后自动工作即可。这一点在第十一章讲滚动更新时会真正派上用场。

应用并亲眼见证自愈能力:

bash
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 个, 两种方式:

bash
# 方式一: 命令式直接改
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:

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, 只要它们带相同标签, 这在灰度发布时很有用。

bash
kubectl apply -f service.yaml

# 查看 service, 会看到一个 CLUSTER-IP
kubectl get svc web-svc

验证负载均衡。临时起一个带 curl 的 Pod, 在集群内部访问 Service:

bash
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 转发过去——这就是负载均衡的真相。

bash
# 亲眼看看这份隐藏的 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 环境专用的部署清单):

bash
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:

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 配置类比就是:

nginx
server {
    server_name web.local;
    location / {
        proxy_pass http://web-svc:80;
    }
}

是不是非常眼熟? 应用并测试:

bash
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:

bash
# 命令式创建 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 声明 (推荐, 便于纳入版本管理):

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  API_URL: http://api-svc
  LOG_LEVEL: info

在 Deployment 中, 有两种主流方式把它们注入 Pod。

方式一: 作为环境变量注入

yaml
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.confapplication.yml):

yaml
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:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes:
    - ReadWriteOnce        # 访问模式: 可被单个节点读写挂载
  resources:
    requests:
      storage: 1Gi         # 申领 1Gi 空间

accessModes 常见三种: ReadWriteOnce (单节点读写, 最常用)、ReadOnlyMany (多节点只读)、ReadWriteMany (多节点读写, 需要特定存储后端支持)。

在 Pod 中挂载它:

yaml
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-0mysql-1, 而不是随机后缀)、固定的启动与伸缩顺序、以及每个 Pod 独享一块由模板自动创建的 PVC。入门阶段了解其存在即可, 真正部署数据库时再深入。生产中也常用 Operator (第十八章) 来管理这类复杂有状态应用。

十一、健康检查与优雅运维

让应用"跑起来"只是第一步, 生产环境真正考验的是"出问题时能否自愈"和"发版时能否不中断"。这一章是从入门迈向实战的关键。

探针: 让 Kubernetes 读懂应用的健康状态

Kubernetes 怎么知道一个 Pod 是真的健康、能对外服务? 容器在 Running 不代表里面的应用真的就绪 (可能还在加载、或已死锁假活)。判断依据是你配置的探针 (Probe)。有三种, 各司其职:

  • livenessProbe (存活探针): 检测应用是否还活着。失败则重启容器。用于自愈死锁、卡死的进程。
  • readinessProbe (就绪探针): 检测应用是否准备好接收流量。失败则把该 Pod 从 Service 的负载均衡清单 (Endpoints) 中摘除, 但不重启。用于应对启动慢、临时过载、依赖未就绪。
  • startupProbe (启动探针): 用于启动特别慢的应用 (如某些 Java 服务), 在它成功之前, 暂缓上面两个探针, 避免应用还在正常启动就被存活探针误杀。

存活与就绪的区别非常关键, 切勿配反

新手最容易混淆这两个。打个比方: 就绪探针失败, 像店员说"我先忙别的, 暂时别给我派单", 顾客 (流量) 会被引导到其他店员, 他本人还在岗; 存活探针失败, 像发现店员晕倒了, 直接换一个新人顶上 (重启容器)。一个调度流量、一个触发重启, 配反了后果很严重——比如把一个"启动慢"误配成存活探针, 应用会在启动期被反复杀死, 陷入无限重启。

在容器中配置探针:

yaml
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 和内存。这直接关系到调度决策和集群稳定性:

yaml
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 始终可用, 从而实现零停机发布。

bash
# 触发更新: 修改镜像版本
kubectl set image deployment/web nginx=nginx:1.25-alpine

# 实时观察滚动更新过程
kubectl rollout status deployment/web

# 查看更新历史版本
kubectl rollout history deployment/web

整个过程像这样, 新旧副本交替进行, 总量不掉到水位线以下:

如果新版本有问题, 一条命令回滚到上一版, 这是 Deployment 最让人安心的能力之一:

bash
# 回滚到上一个版本
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:

bash
# 为 web 这个 Deployment 创建 HPA:
# 目标是让 CPU 平均利用率维持在 50%, 副本数在 2 到 10 之间自动伸缩
kubectl autoscale deployment web --cpu-percent=50 --min=2 --max=10

# 查看 HPA 状态
kubectl get hpa

对应的 YAML 写法:

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):

yaml
# ---------- 后端配置 ----------
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

一次性部署整套应用:

bash
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 的价值就具体地显现出来了。

清理整套环境也只需一条命令:

bash
kubectl delete -f app.yaml

十四、排错指南

部署不可能一帆风顺。掌握一套排错的常规手法, 比记住所有 YAML 字段更重要。下面是按使用频率排序的核心命令。

bash
# 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 状态含义优先排查方向
PendingPod 还没被调度到任何节点节点资源不足, 或 requests 设得过大; PVC 未绑定; 存在不满足的调度约束
ImagePullBackOff拉取镜像失败镜像名/Tag 拼错; 私有仓库未配凭证; 网络不通
CrashLoopBackOff容器反复启动又崩溃logs --previous, 多为应用自身报错或配置/环境变量缺失
OOMKilled容器内存超过 limit 被杀调高 memory limit, 或排查内存泄漏
RunningREADY 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 该放哪? 调度器分两阶段决策:

  1. 过滤 (Filtering): 排除掉所有不满足硬性条件的节点。比如剩余资源不够 (装不下这个 Pod 的 requests)、节点有 Pod 不能容忍的"污点"、不满足指定的节点选择条件。
  2. 打分 (Scoring): 在剩下的可行节点中, 按一系列规则打分 (如资源最空闲的得分高、镜像已缓存的得分高), 选分数最高的。

大多数时候用默认策略即可。但精细化运维时, 你会需要主动干预 Pod 的落点, 主要有三类工具:

nodeSelector 与节点亲和性

最简单的方式是 nodeSelector: 给节点打标签, 让 Pod 只去带特定标签的节点。比如把需要 GPU 的任务调度到 GPU 机器:

bash
# 先给节点打标签
kubectl label nodes <节点> gpu=true
yaml
spec:
  nodeSelector:
    gpu: "true"        # 这个 Pod 只会被调度到带 gpu=true 标签的节点

节点亲和性 (Node Affinity)nodeSelector 的增强版, 支持"硬性要求"和"软性偏好"两种力度, 表达力更强。

污点与容忍

名词: 污点 (Taint) 与容忍 (Toleration)

这是一对反向机制, 理解起来稍绕, 用类比说清: 污点是给节点贴的一张"拒绝标签", 比如给控制平面节点打上污点, 默认就没有普通 Pod 愿意去。容忍则是给 Pod 配的一张"通行证", 表示"我能容忍某种污点"。只有持有对应容忍的 Pod, 才能被调度到带该污点的节点上。

一句话: 污点是节点说"别来", 容忍是 Pod 说"我例外"。常用于保留专用节点 (如把某些机器只留给特定业务)。

bash
# 给节点打污点: 键=值:效果。NoSchedule 表示不容忍就别来
kubectl taint nodes <节点> dedicated=ml:NoSchedule
yaml
spec:
  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 把这些知识全部代码化后, 你只需声明:

yaml
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: 一条命令就能安装一整套复杂应用。

bash
# 添加仓库并安装一个 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=mypass

Chart 用模板语法把可变部分抽成参数 (放在 values.yaml), 同一套模板配不同参数即可部署到不同环境, 这是生产中管理应用的标准方式。

GitOps: 用 Git 驱动集群

第六章提到的 GitOps 在这里展开: 核心理念是让 Git 仓库成为集群期望状态的唯一真相来源。你不再手动 kubectl apply, 而是把所有 YAML 提交到 Git, 由 ArgoCDFlux 这类工具持续监听仓库, 自动把变更同步到集群, 并保证集群实际状态与 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 开始, 现在就动手吧。