为什么需要k8s?

当你需要管理很多容器时,K8s 帮你自动化处理它们的部署、扩缩容、故障恢复和网络连接等复杂工作。

![image-20251223171300491](/Users/mag1code/Library/Application Support/typora-user-images/image-20251223171300491.png)

![image-20251223171340700](/Users/mag1code/Library/Application Support/typora-user-images/image-20251223171340700.png)

核心组件 (管理层和员工内部的“部门”)

简单了解一下这些“部门”的名字和职能,你之后会经常听到它们:

在控制平面 (大脑) 里:

  • API Server: 集群的唯一入口,像公司的“前台总机”,所有沟通都必须经过它。
  • etcd: 集群的“数据库/档案室”,存储了集群所有状态信息。
  • Scheduler: “人力资源调度员”,当需要新容器时,它会决定把这个任务分配给哪个最合适的 Worker Node。
  • Controller Manager: “项目经理/监工”,持续监控集群状态,确保实际状态和期望状态一致(比如发现一个容器挂了,它就会启动一个新的)。

在每个工作节点 (员工) 上:

  • Kubelet: “车间小组长”,直接与容器运行时(如 Docker)打交道,确保分配到这个节点上的容器都按要求正常运行。
  • Kube-proxy: “网络管理员”,负责处理节点上的网络规则,让容器之间以及内外都能正常通信。

kubectl常用操作

# 查看列表
kubectl get <资源对象类型> [-o wide] [-n <namespace名称>]

# 查看详情
kubectl describe <资源对象类型> <资源对象名称> [-n <namespace名称>]

# 通过 YAML 文件部署
kubectl apply -f <资源对象类型的YAML文件>

# 删除资源对象
kubectl delete <资源对象类型> <资源对象名称> [-n <namespace名称>]
kubectl delete -f <资源对象类型的YAML文件>

# 实时编辑资源对象,进入编辑器,保存后立即生效
kubectl edit <资源对象类型> <资源对象名称> [-n <namespace名称>]

深入剖析Kubernetes

问题?:docker只是通过namespace障眼法,实际上还是在宿主机上的进程是吗?是的话那是如何一下子运行一个很大的项目或者说一个“容器”?实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环境。从这个结构中我们不难看出,一个正在运行的 Linux 容器,其实可以被“一分为二”地看待

  1. 一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;

  2. 一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。

简易摘录:

  • 但是,到了 Kubernetes 项目里,这样的成组调度问题就迎刃而解了:Pod 是 Kubernetes 里的原子调度单位。这就意味着,Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。;不过,Pod 在 Kubernetes 项目里还有更重要的意义,那就是:容器设计模式。
  • kubectl apply跟 kubectl replace 命令有什么本质区别吗?实际上,你可以简单地理解为,kubectl replace 的执行过程,是使用新的 YAML 文件中的 API 对象,替换原有的 API 对象;而 kubectl apply,则是执行了一个对原有 API 对象的 PATCH 操作。类似地,kubectl set image 和 kubectl edit 也是对已有 API 对象的修改。更进一步地,这意味着 kube-apiserver 在响应命令式请求(比如,kubectl replace)的时候,一次只能处理一个写请求,否则会有产生冲突的可能。而对于声明式请求(比如,kubectl apply),一次能处理多个写操作,并且具备 Merge 能力。
  • Worker Node 上的“组件”之所以看起来少,是因为 K8s 的设计理念是 Microkernel(微内核) 思想:
    • Node 只保留最核心的“执行器”:只有 kubelet、kube-proxy 和 Runtime 是必须“死在”机器上的守护进程。
    • 其他一切皆 Pod:DNS 是 Pod、Ingress 是 Pod、甚至网络插件 (Calico) 也是 Pod。
    • 其他一切皆 Data:Secret、Service、ConfigMap 只是组件们传递的“小纸条”。

YAML文件的4 个顶层必填字段

一个标准的 K8s YAML 文件,必须包含 4 个顶层必填字段(Required Fields)。我们可以把它看作**“四大金刚”**:

1. apiVersion (API 版本)

  • 作用:告诉 K8s API Server:“我要用哪个版本的说明书来创建这个对象”。因为 K8s 在不断升级,不同版本的字段定义可能不一样。
  • 常用值:v1:最核心、最稳定的资源(Pod, Service, ConfigMap, Secret, Node, Namespace)。apps/v1:应用编排相关的资源(Deployment, ReplicaSet, StatefulSet, DaemonSet)。batch/v1:批处理任务(Job, CronJob)。networking.k8s.io/v1:网络相关(Ingress)。

2. kind (资源类型)

  • 作用:告诉 K8s:“我要创建个什么东西”。
  • 常用值:Pod:豌豆荚(最小单位)。Deployment:管理 Pod 的控制器(最常用)。Service:对外暴露服务的入口。PersistentVolumeClaim (PVC):申请存储。

3. metadata (元数据)

  • 作用:这是对象的**“身份证”。它定义了这个东西叫什么、属于哪个组、贴了什么标签。这部分信息不影响程序的运行逻辑**,只影响你如何找到它。
  • 常用参数:name:必填。这个对象的名字(同一 Namespace 下必须唯一)。namespace:属于哪个命名空间(默认是 default)。labels:核心字段。键值对标签(如 app: nginx, env: prod)。K8s 的 Service 和 Deployment 全靠这个标签来找到对应的 Pod。annotations:注解。给非标识性的元数据用的(比如记录谁在什么时候更新了这个对象),通常是给工具看的。

4. spec (规约/期望状态)

  • 作用:这是**“许愿单”。这是整个 YAML **最重要、最复杂**的部分。你在这里描述你**希望这个对象长什么样、怎么运行。
  • 注意:spec 下面的子字段,根据 kind 的不同而完全不同!如果是 Pod,spec 描述容器和卷。如果是 Service,spec 描述端口和选择器。

Volume存储类型

在那个文件中,有一个结构体叫 type VolumeSource struct,里面列出的几十个字段,就是 K8s 支持的所有 Volume 类型。

虽然看起来多得吓人,但在实际工作中,我们只需要掌握5 大类就足够应对 99% 的场景了。


第一类:临时卷 (Ephemeral Volumes)

特点: 随 Pod 生,随 Pod 死。Pod 重启或删除,数据就没了。
用途: 缓存、临时计算中间结果。

1. emptyDir (最常用)

  • 原理: K8s 在宿主机上创建一个空目录(或者使用内存),然后挂载进容器。
  • 场景: 比如你有一个爬虫 Pod,容器 A 负责下载网页存进去,容器 B 负责读取网页进行分析。
  • 配置示例:
    volumes:
    - name: cache-volume
    emptyDir: {} # 默认用磁盘,也可以设为 medium: "Memory" 用内存

第二类:配置与密钥卷 (Config & Secret Volumes)

特点: 将 K8s 的配置数据注入到 Pod 里。
用途: 配置文件、密码、证书。

2. secret / configMap / downwardAPI

  • Secret/ConfigMap: 把你存在 etcd 里的配置(如 nginx.conf 或 数据库密码)变成文件挂进去。
  • DownwardAPI: 把 Pod 自己的信息(如 Pod IP、Pod Name、CPU 限制)变成文件挂进去让程序读。
  • Projected (投射卷): 就是你刚才用的那个! 它是把上面这三个(Secret, ConfigMap, DownwardAPI)合并在一起挂载的“高级版”。

第三类:本地宿主机卷 (Local Volumes)

特点: 直接捅穿由于 Linux Mount Namespace 建立的隔离墙。
用途: 收集宿主机日志、监控宿主机、Minikube 调试。

3. hostPath (最简单粗暴)

  • 原理: 直接把宿主机(Node)上的 /var/log 映射给容器。
  • 风险: 非常不推荐在生产环境随意使用。 因为 Pod 如果被调度到了另一台机器上,它就找不到之前那台机器上的文件了。
  • 你的 Minikube 场景: 在 Minikube 这种单节点环境里,hostPath 非常好用,因为你就一台机器。

第四类:网络存储卷 (Network Volumes)

特点: 数据存在远程服务器上。Pod 无论跑在哪台机器,都能通过网络读到同一份数据。
用途: 数据库、文件共享服务器。

4. nfs (网络文件系统)

  • 经典的老牌共享存储。所有 Pod 都可以读写同一个远程文件夹。

5. csi (容器存储接口 - 现在的标准)

  • 以前 K8s 代码里塞满了各种厂商的驱动(AWS EBS, Azure Disk, Ceph 等),代码非常臃肿。
  • 现在 K8s 搞了个标准接口叫 CSI
  • 用法: 你想用阿里云盘?装个阿里云 CSI 插件。想用 Ceph?装个 Ceph CSI 插件。
  • 在 YAML 里,通常你不会直接写 csi: ...,而是通过 PVC 来使用它(见下文)。

第五类:抽象层 (PersistentVolumeClaim - PVC) —— 核心考点

这是 K8s 设计最精妙的地方。

痛点:
作为一个开发人员,我写 Pod YAML 时,不想关心底层的存储到底是 AWS 的 EBS,还是 NFS,还是 Ceph。我只想要 “10GB 的读写盘”

解决方案:
K8s 发明了两个对象来解耦

  1. PV (PersistentVolume):由运维/管理员创建。比如 “这是一块 100G 的 NFS 硬盘”。
  2. PVC (PersistentVolumeClaim):由开发人员在 Pod 里定义。比如 “我申请一张 10G 的票”。

用法(这是你以后最常写的):

# 1. 你的 Pod 不写具体的 nfs 地址
volumes:
- name: my-data
persistentVolumeClaim:
claimName: my-10g-pvc # 拿着这张票去领磁盘

90%的pod都只有一个容器,那么pod的意义是什么?只为了那10%的情况考虑吗?

这是一个直击灵魂的问题,也是每一个从 Docker 转向 Kubernetes 的人必问的问题。

如果 K8s 当初设计成直接调度容器(Container),而不是 Pod,世界会不会简单很多?

答案是:对于简单的场景会简单,但对于复杂的编排场景,K8s 会崩溃。

设计 Pod 的核心原因,归结为三个关键词:原子性调度进程组概念、以及接口标准化


1. “进程组” (Process Group) 的回归

这是 Google 在设计 K8s(基于内部 Borg 系统)时最底层的理论基础。

  • 操作系统里:你打开一个 Word 文档,看起来是一个程序。但实际上后台可能有一个主进程,负责 UI 的进程,负责拼写检查的进程。这些进程属于同一个 “进程组”。它们共享内存、共享文件描述符。
  • 容器里:Docker 提倡“一个容器一个进程”。
  • 冲突点:如果你的应用必须由“主程序 + 日志搜集器”组成,在 Docker 眼里这是两个完全独立的“人”。

Pod 的出现,就是为了在云原生时代还原“进程组”的概念。
它告诉系统:“这一组容器是穿一条裤子的,要活一起活,要死一起死,不能拆散。”


2. 解决“成组调度” (Gang Scheduling) 的噩梦

假设没有 Pod,你告诉 K8s:“请帮我把 App 容器和 Log 容器部署到同一台机器上。”

如果没有 Pod,调度器会面临经典的“死锁”问题:

  1. 集群有 Node A(剩 1G 内存)和 Node B(剩 1G 内存)。
  2. App 容器需要 0.8G,Log 容器需要 0.5G。
  3. 调度器先把 App 调度到了 Node A。
  4. 此时 Node A 只剩 0.2G 了,Log 容器进不去。
  5. 调度器只能把 Log 容器调度到 Node B。
  6. 结果:App 和 Log 分家了,日志收集失败,系统部署失败。

有了 Pod (原子性调度):
K8s 调度器不看容器,只看 Pod。
Pod 说:“我一共需要 1.3G (0.8 + 0.5)”。
调度器一看:Node A 不够,Node B 不够。直接报错 Pending,等待资源扩容。
它绝不会把这对“夫妻”强行拆散到两地分居。Pod 保证了调度的原子性。


3. 网络模型的“锚点” (The Sandbox Anchor)

这涉及到了那个神秘的 pause 容器。

  • 如果直接跑容器:容器挂了重启,PID 会变,IP 地址通常也会变(除非你做复杂的配置)。
  • Pod 的设计
    Pod 启动时,先起一个 pause 容器占住网络 Namespace(分配一个 IP)。
    然后业务容器 A 启动,加入这个网络;业务容器 A 挂了,重启变成 A’,依然加入这个网络。
    只要 Pod 不死,IP 就不变。

Pod 是容器的“家”。铁打的营盘(Pod/Pause),流水的兵(业务容器)。 这种设计让 K8s 的网络模型变得非常稳定和容易管理。


4. 所谓的“90% 只有一个容器”其实在减少

虽然你现在手写的 YAML 大部分是一个容器,但在现代的 云原生架构 中,多容器 Pod 比你想象的更普遍:

  • Service Mesh (服务网格 - 如 Istio)
    一旦你开启了 Istio,它会自动往你的每一个 Pod 里注入一个 Envoy 代理容器。
    瞬间,你集群里 100% 的 Pod 都变成了多容器。 如果 K8s 本身不支持 Pod 概念,Istio 这种架构就根本无法实现。
  • Init Containers
    很多任务在主程序启动前需要做准备(比如改内核参数、初始化数据库 Schema)。这些也是容器,它们运行完就退出。Pod 完美地管理了这种“先执行 A,再执行 B”的生命周期。

5. 标准化接口 (Runtime Agnostic)

这是一个更高维度的原因。

  • Docker 只是容器的一种。 还有 containerd, CRI-O, Kata Containers, gVisor, 甚至是 WebAssembly。
  • 有些“容器”是虚拟机(如 Kata),有些是进程(如 Docker)。
  • K8s 定义了 Pod 这个抽象概念。
    • 如果你底层用 Docker,Pod 就是一组 Docker 容器。
    • 如果你底层用 Kata,Pod 就是一个轻量级虚拟机。

Pod 帮 K8s 屏蔽了底层的差异。 开发人员只需要关心“我要一个 Pod”,而不需要关心底层到底是用什么技术跑起来的。

总结

Pod 并不是为了“多此一举”,它是为了:

  1. 即使只有一个容器:它提供了一个稳定的网络身份(IP)和存储环境,让容器重启也不丢失身份。
  2. 如果有多个容器:它解决了“必须在一起”的调度难题。
  3. 对于系统架构:它通过抽象层,让 K8s 能够适配未来任何新的容器技术,而不被 Docker 绑定。

可以把 Pod 理解为 “在云上运行的一台逻辑主机”,而容器只是这台主机里跑的进程。既然是主机,哪怕只跑一个进程,这台主机的壳子也是必须存在的。

StatefulSet剖析

容器持久化存储

PV 描述的,是持久化存储数据卷。这个 API 对象主要定义的是一个持久化存储在宿主机上的目录,比如一个 NFS 的挂载目录。而 PVC 描述的,则是 Pod 所希望使用的持久化存储的属性。比如,Volume 存储的大小、可读写权限等等。

通常情况下,PV 对象是由运维人员事先创建在 Kubernetes 集群里待用的。比如,运维人员可以定义这样一个 NFS 类型的 PV,如下所示:

apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.244.1.4
path: "/"

发人员可以声明一个 1 GiB 大小的 PVC,如下所示:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs
spec:
accessModes:
- ReadWriteMany
storageClassName: manual
resources:
requests:
storage: 1Gi

​ 而用户创建的 PVC 要真正被容器使用起来,就必须先和某个符合条件的 PV 进行绑定。这里要检查的条件,包括两部分:

  • 第一个条件,当然是 PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须满足 PVC 的要求。
  • 而第二个条件,则是 PV 和 PVC 的 storageClassName 字段必须一样。

这个机制我会在本篇文章的最后一部分专门介绍。在成功地将 PVC 和 PV 进行绑定之后,Pod 就能够像使用 hostPath 等常规类型的 Volume 一样,在自己的 YAML 文件里声明使用这个 PVC 了,如下所示:

apiVersion: v1
kind: Pod
metadata:
labels:
role: web-frontend
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
volumeMounts:
- name: nfs
mountPath: "/usr/share/nginx/html"
volumes:
- name: nfs
persistentVolumeClaim:
claimName: nfs
flowchart TD
U["用户 / 浏览器"] --> DNS["DNS: exam.com"]
DNS --> ING["Ingress Controller\nNginx Ingress"]

subgraph K8S["Kubernetes Cluster"]
ING --> S1["Service: web-svc"]
ING --> S2["Service: api-svc"]

S1 --> W1["Pod: web-1"]
S1 --> W2["Pod: web-2"]

S2 --> A1["Pod: api-1"]
S2 --> A2["Pod: api-2"]
S2 --> A3["Pod: api-3"]

A1 --> RS["Service: redis-svc"]
A2 --> RS
A3 --> RS

A1 --> MS["Service: mysql-svc"]
A2 --> MS
A3 --> MS

RS --> R1["Pod: redis"]
MS --> M1["Pod: mysql"]

M1 --> PVC["PVC"]
PVC --> PV["PV / Storage"]

CM["ConfigMap"] -.注入配置.-> A1
CM -.注入配置.-> A2
CM -.注入配置.-> A3

SC["Secret"] -.注入密码.-> A1
SC -.注入密码.-> A2
SC -.注入密码.-> A3

HPA["HPA"] -.扩缩容.-> API_D["Deployment: api"]
API_D --> A1
API_D --> A2
API_D --> A3

WEB_D["Deployment: web"] --> W1
WEB_D --> W2
end

subgraph CP["Control Plane"]
API["API Server"]
ETCD["etcd"]
SCH["Scheduler"]
CTRL["Controller Manager"]
end

subgraph NODE["Worker Nodes"]
K1["kubelet"]
K2["kube-proxy"]
CRI["Container Runtime"]
end

API --> ETCD
CTRL --> API
SCH --> API
API --> K1
K1 --> CRI
K2 --> S1
K2 --> S2