Kubernetes 温习记录(Personal Notes)

KubernetesNotes

总览

1. Kubernetes 基础概念

Kubernetes 可以看作是这么一个模型: 用户通过 API Server 告诉 Kubertenes 期望的状态, API Server 将这个期望状态存储在 etcd 中 然后 Kubernetes 的 Controller 会不断地监视当前状态和期望状态之间的差异, 并采取必要的行动来使当前状态逐渐接近期望状态 如何采取行动呢? 主要是通过 Scheduler (调度器), 得到当前集群中有哪些节点, 哪些节点满足当前的需求 然后 Scheduler 会通过 API Server 更新调度结果(例如绑定到某个节点), 最终再由 API Server 持久化到 etcd 运行在每个节点上的 Kubelet 会不断地 watch API Server 上和本节点相关的 Pod 变化, 当发现有新的 Pod 被调度到这个节点上时, 就会创建这些资源

2. Kubernetes 组件

  • API Server: Kubernetes 的核心组件, 负责处理所有的 REST 请求, 并将数据存储在 etcd 中。
  • etcd: 一个分布式键值存储系统, 用于存储 Kubernetes 的所有数据。
  • Controller Manager: 负责运行各种控制器, 这些控制器负责监视 Kubernetes 的状态并采取必要的行动来使当前状态逐渐接近期望状态。
  • Scheduler: 负责将 Pod 分配到合适的节点上。
  • Kubelet: 运行在每个节点上的代理, 负责管理节点上的 Pod 和容器。
  • Kube Proxy: 负责维护 Kubernetes 服用于存储 Kubernetes 的所有数据。
  • Controller Manager: 负责运行各种控制器, 这些控制器负责监视 Kubernetes 的状态并采取必要的行动来使当前状态逐渐接近期望状态。
  • Scheduler: 负责将 Pod 分配到合适的节点上。务的网络规则, 使得服务能够被访问。

3. Kubernetes 对象(Resources)

主要可以分为以下几类:

  • 容器编排对象: Pod, ReplicaSet, Deployment, StatefulSet, DaemonSet, Job, CronJob
  • 服务发现和负载均衡对象: Service, Ingress
  • 存储对象: PersistentVolume, PersistentVolumeClaim, StorageClass
  • 配置和密钥对象: ConfigMap, Secret
  • 命名空间对象: Namespace
  • 其他对象: Node, Namespace, ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding

Kubernetes 资源

容器编排对象

1. Pod

定义

Pod 是 Kubernetes 中最小的可调度单元, 通常包含一个或多个容器, 这些容器共享网络和存储资源。

我们知道容器是通过创建自己独立的 Linux Namespace 实现的资源隔离的, 在简单的 Docker Container 中, 每个容器都有自己的 PID Namespace, Network Namespace, Mount Namespace 等等

但是在 Kubernetes 中, Pod 中的多个容器共享同一个如下 Namespace:

  • Network Namespace: 这意味着同一个 Pod 中的容器可以通过 localhost 进行通信, 并且共享同一个 IP 地址和端口空间
  • IPC Namespace: 这意味着同一个 Pod 中的容器可以共享 System V IPC 和 POSIX 消息队列
  • PID Namespace(可选): 这意味着同一个 Pod 中的容器可以共享进程 ID 空间, 这使得它们可以看到彼此的进程, 并且可以使用 PID 1 来管理整个 Pod 的生命周期
  • UTS Namespace: 这意味着同一个 Pod 中的容器可以共享主机名和域名

注意这些 Namespace 虽然他们是共享的, 但是和主机是隔离的, 这意味着同一个 Pod 中的容器虽然共享同一个 Network Namespace, 但是他们和主机上的其他容器是隔离的, 他们只能通过 localhost 进行通信, 并且不能直接访问主机上的其他容器

Pod类似逻辑主机,在逻辑主机中运行的进程共享诸如CPU、RAM、网络接等资源

但是每个容器都有自己的 Mount Namespace, 这意味着它们可以有不同的文件系统视图, 这使得它们可以使用不同的卷来存储数据

Spec 文件

用户可以通过以下方式定义一个 Pod:

由于 Pod 就是几个共享某些资源的容器的集合, 所以我们定义 Pod 的时候主要也是在定义这个 Pod 中有哪些容器, 以及这些容器需要哪些资源, 以及这些资源是如何共享的

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  label:
    app: my-app
  annotations:
    description: "这是一个示例 Pod"
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    ports:
    - containerPort: 80
  - name: my-sidecar
    image: busybox
    command: ["sleep", "3600"]
 

在这个例子中, 我们定义了一个名为 my-pod 的 Pod, 这个 Pod 中有两个容器: my-containermy-sidecar my-container 使用了 nginx 镜像, 并且暴露了 80 端口 my-sidecar 使用了 busybox 镜像, 并且执行了一个简单的命令来保持容器运行

因此我们可以进入 busybox 这个容器, 通过 localhost 来访问 nginx 这个容器, 这就是 Pod 中的容器共享 Network Namespace 的一个例子

然后我们可以给这些资源都定义一些标签, 这样我们就可以通过标签来选择这些资源, 这样当我们有一大堆资源的时候, 我们就可以通过标签来选择我们需要的资源

kubectl get pods -l app=my-app

也可以通过注解来给这些资源添加一些额外的信息, 这样我们就可以通过注解来存储一些不适合放在标签中的信息, 例如一些描述性的文本, 这样我们就可以通过注解来存储一些额外的信息

kubectl get pods -o jsonpath='{.items[0].metadata.annotations.description}'

同时也可以用 namespace 来将这些资源进行分组, 这样我们就可以通过 namespace 来将这些资源进行分组, 这样当我们有一大堆资源的时候, 我们就可以通过 namespace 来选择我们需要的资源

kubectl get pods -n my-namespace

生命周期

Pod 和 Container 不同的地方, 除了 Pod 是 Container 的集合之外

还有一个重要的区别是 Pod 有更丰富的生命周期

...

用户交互流程

kunectl apply -f my-pod.yaml

当我们使用这么一行命令之后, 这个 Pod 资源就会在一定的时间内被创建出来

有如下流程:

  1. 用户通过 kubectl apply 命令将 Pod 的定义文件提交给 API Server
    • kubectl 实际上就是一个客户端, 它会将这个定义文件转换成一个 REST 请求, 发送给 API Server, 而且这个 kubectl 会加入自己的一些认证信息, 例如 token, 这样 API Server 就可以验证这个请求的合法性
  2. API Server 接收到这个请求之后, 会对这个请求进行认证和授权, 如果认证和授权通过了, 那么 API Server 就会将这个 Pod 的定义存储在 etcd 中, 这样这个 Pod 的定义就被持久化了
    • etcd 对于一个 Pod 的存储, 会保存他在定义文件中定义的所有信息, 包括他的 metadata, spec, status 等等
    • 其中 metadata 中的 name, namespace, labels, annotations 等等都是用户定义的, 而 spec 中的 containers, volumes, restartPolicy 等等也是用户定义的, 但是 status 中的 phase, conditions, hostIP, podIP 等等都是 Kubernetes 根据当前的状态自动生成的
    • status 对于调度很重要, 他会保存当前的 Pod 的状态, 这个时候 status.phase 就是 Pending, status.phase.nodeName 也是空的, 这意味着这个 Pod 还没有被调度到任何一个节点上, 还没有被创建出来
  3. 这个时候 Kubernetes 的 Scheduler 会通过 watch API Server 观察到新的未绑定 Pod, 然后开始调度这个 Pod
    • Scheduler 会查看当前集群中有哪些节点, 哪些节点满足这个 Pod 的资源需求, 例如这个 Pod 需要多少 CPU, 多少内存, 需要哪些标签等等, 然后 Scheduler 就会选择一个合适的节点来调度这个 Pod, 具体这些调度 perference 信息在下文再说
  • Scheduler 会通过 API Server 更新这个 Pod 的绑定结果(例如 spec.nodeName=node1), 然后由 API Server 持久化到 etcd; 此时可观察到 Pod 的 PodScheduled 条件为 True
  1. 这个时候运行在 node1 上的 Kubelet 会通过 watch API Server 看到有新的 Pod 被调度到了这个节点上, 然后开始创建这个 Pod
    • Kubelet 会根据这个 Pod 的定义来创建这个 Pod, 例如这个 Pod 中有两个容器, 那么 Kubelet 就会创建这两个容器,并且将这两个容器放在同一个 Network Namespace 中, 这样这两个容器就可以通过 localhost 来进行通信了
  • Kubelet 会通过 API Server 上报这个 Pod 的状态, 最终由 API Server 持久化到 etcd, 例如这个 Pod 的 status.phase 会变成 Running, status.hostIP 会变成 node1 的 IP 地址, status.podIP 会变成这个 Pod 的 IP 地址

Cheetsheet

kubectl get po # Display all container group information
kubectl get po -o wide
kubectl describe po
kubectl get po --show-labels # View the labels of the container group
kubectl get po -l app=nginx
kubectl get po -o yaml
kubectl get pod [pod_name] -o yaml --export
kubectl get pod [pod_name] -o yaml --export > nameoffile.yaml
# Export container group information to yaml file in yaml format
kubectl get pods --field-selector status.phase=Running
# Use the field selector to filter out container group information

2. ReplicationController

Pod 只是共享某些资源的容器的集合, 仅此而已

如果我们想要实现一些更复杂的功能, 例如自动扩缩容, 自动重启等等, 那么我们就需要使用 ReplicationController 来实现这些功能

定义

ReplicationController 有这么一个资源, 同时也有一个同名的控制器

先说这个资源定义了一些什么信息, ReplicationController 资源定义了一个期望的状态, 以及如果不满足这个状态, 那么应该创建什么样的 Pod 来使当前状态逐渐接近期望状态

  • 期望状态: 这个 ReplicationController 资源期望有多少个 Pod 副本在运行, 这里期望资源的定义主要是通过 label selector 来定义的, 比如说期望 label app=my-app 的 Pod 有 3 个副本在运行
  • 模板: 这个 ReplicationController 资源还定义了一个 Pod 模板, 这个模板定义了如果当前状态不满足期望状态, 那么应该创建什么样的 Pod 来使当前状态逐渐接近期望状态, 例如如果当前的 Pod 副本数量不足了, 那么应该创建如下的 Pod

ReplicationController 控制器就是监控 ReplicationController 这个资源的状态, 当发现这个 ReplicationController 资源的期望状态和当前状态之间存在差异的时候, 就会采取必要的行动来使当前状态逐渐接近期望状态, 例如如果当前的 Pod 副本数量不足了, 那么 ReplicationController 控制器就会根据这个模板来创建新的 Pod, 这样就可以保证这个 ReplicationController 资源的期望状态得以满足了

Spec Example

apiVersion: v1
kind: ReplicationController
metadata:
  name: my-rc
  namespace: my-namespace
spec:
  replicas: 3
    selector:
      app: my-app
# 可以发现下面这里的内容就是 Pod Spec 的内容了, 这就是 ReplicationController 资源中定义的 Pod 模板了
  template:
    metadata:
      labels:
        app: my-app 
# 这个标签必须和上面 selector 中定义的标签一致, 这样 ReplicationController 控制器才能根据这个 selector 来选择这些 Pod 来满足这个 ReplicationController 资源的期望状态, 否则他就会一直认为当前状态不满足期望状态, 然后一直创建新的 Pod 来满足这个 ReplicationController 资源的期望状态了
    spec:
      containers:
      - name: my-container
        image: nginx
        ports:
        - containerPort: 80

Workflow

当我们使用 kubectl apply 命令将这个 ReplicationController 资源提交给 API Server 之后

这个 ReplicationController 资源会先提交给 API Server, 然后由 API Server 持久化到 etcd

这个时候 ReplicationController 控制器会通过 watch API Server 观察 ReplicationController 资源, 当发现有一个新的 ReplicationController 资源被创建出来了, 并且这个 ReplicationController 资源的期望状态和当前状态之间存在差异的时候

那么他就会根据这个 ReplicationController 资源中定义的 Pod 模板, 通过 API Server 创建新的 Pod 对象; Scheduler 会通过 watch API Server 观察这些未绑定 Pod, 然后把它们调度到合适的节点上, 这样就可以满足这个 ReplicationController 资源的期望状态了

所以说其实, ReplicationController 并不是关注他的这个 Pod Template 创建的 Pod 到底有几个, 仅仅是关注满足这些 label selector 的 Pod 到底有几个

3. ReplicaSet

ReplicaSet 其实和 ReplicationController 非常类似, 他们的区别主要在于 ReplicaSet 支持更多的 label selector 的语法

ReplicationController 的 label selector 只能使用 equality-based selector, 也就是说只能使用等于或者不等于来选择 Pod, 例如 app=my-app, app!=my-app

而 ReplicaSet 的 label selector 支持 set-based selector, 也就是说可以使用 in, notin, exists, doesnotexist 来选择 Pod, 例如 app in (my-app, my-app2), app notin (my-app, my-app2), app exists, app doesnotexist

4. DemonSet

DaemonSet 是 Kubernetes 中的一种资源, 他定义了一种特殊的 Pod 模板, 这个 Pod 模板会被 Kubernetes 调度到集群中的每一个节点上, 这样就可以保证这个 Pod 模板定义的 Pod 在集群中的每一个节点上都有一个副本在运行了

他比较适合用来运行一些集群级别的服务, 例如日志收集, 监控等等, 因为这些服务需要在每一个节点上都有一个副本在运行, 这样才能收集到每一个节点上的日志或者监控数据了

其中它和前面的 ReplicationController 和 ReplicaSet 的模型也是相似的

期望状态都是来自 label selector, 例如期望 label app=my-daemonset 的 Pod 在集群中的每一个节点上都有一个副本在运行了

然后有一个 Pod 模板, 这个 Pod 模板定义了如果当前状态不满足期望状态, 那么应该创建什么样的 Pod 来使当前状态逐渐接近期望状态, 例如如果当前的 Pod 副本数量不足了, 那么应该创建如下的 Pod

Spec Example

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: my-daemonset
  namespace: my-namespace
spec:
  selector:
    matchLabels:
      app: my-daemonset
  template:
    metadata:
      labels:
        app: my-daemonset # 这个标签必须和上面 selector 中定义的标签一致, 这样 DaemonSet 控制器才能根据这个 selector 来选择这些 Pod 来满足这个 DaemonSet 资源的期望状态, 否则他就会一直认为当前状态不满足期望状态, 然后一直创建新的 Pod 来满足这个 DaemonSet 资源的期望状态了
    spec:
      containers:
      - name: my-container
        image: nginx
        ports:
        - containerPort: 80

Workflow

当我们使用 kubectl apply 命令将这个 DaemonSet 资源提交给 API Server 之后

这个 DaemonSet 资源会先提交给 API Server, 然后由 API Server 持久化到 etcd; 这个时候 DaemonSet 控制器会通过 watch API Server 观察 DaemonSet 资源, 当发现有一个新的 DaemonSet 资源被创建出来了, 并且这个 DaemonSet 资源的期望状态和当前状态之间存在差异的时候

他就会根据这个 DaemonSet 资源中定义的 Pod 模板, 通过 API Server 创建新的 Pod 对象; Scheduler 会通过 watch API Server 观察这些 Pod, 然后将这些 Pod 调度到集群中的每一个节点上, 这样就可以满足这个 DaemonSet 资源的期望状态了

其中, 在检测期望状态和当前是否存在差异的时候, 前面的 Rc 和 Rs 主要关注匹配 selector 的 Pod 数量, 但是 DaemonSet 还需要额外考虑当前集群中有哪些节点

5. Job 和 CronJob

前面我们说的这些资源, 例如 Pod, ReplicationController, ReplicaSet, DaemonSet 这些资源, 他们都是用来运行一些长期运行的服务的, 例如 nginx, redis, mysql 这些服务, 因为这些服务需要一直在运行着的

可以理想的看作是一个这些 Pod 运行的任务是一个 Forever Task, 也就是说, 根本不存在这个 Task 是否运行成功的概念, 我们关注的只是让他能健康的运行着

但是有的时候, 我们需要定义这么一种任务, 他们有一个明确的开始和结束, 成功和失败, 例如我们需要定义一个任务, 这个任务需要执行一些数据处理的工作, 例如 ETL 任务, 数据清洗任务等等, 这些任务有一个明确的开始和结束, 我们关注的不是让他一直运行着, 而是让他能成功的运行一次就好了

如果是一个临时的、有限生命周期的任务, 那么我们就可以使用 Job 来定义这个任务(可以是一次, 也可以是多次完成); 如果是一个需要定期运行的任务, 那么我们就可以使用 CronJob 来定义这个任务

并且由于任务还存在成功和失败的概念, 所以我们通常会看 Job 的 status.conditions 中的 type=Complete 和 type=Failed 来判断执行结果; CronJob 本身主要负责按 schedule 创建 Job, 成败由它创建出来的 Job 来体现

既然是定义一个任务, 那么我们就能定义这些:

  • 重复次数: 这个 Job 需要成功运行多少次才算完成了, 例如我们定义这个 Job 需要成功运行 3 次才算完成了, 那么当这个 Job 创建出来的 Pod 成功运行了一次了, 那么这个 Job 就会将这个重复次数减一, 当这个重复次数减到零了, 那么这个 Job 就算完成了
  • 重试次数: 这个 Job 需要重试多少次才算失败了, 例如我们定义这个 Job 需要重试 3 次才算失败了, 那么当这个 Job 创建出来的 Pod 失败了一次了, 那么这个 Job 就会将这个重试次数减一, 当这个重试次数减到零了, 那么这个 Job 就算失败了

Spec Example

Job Spec Example
apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
  namespace: my-namespace
spec:
  template:
    metadata:
      labels:
        app: my-job
    spec:
      containers:
      - name: my-container
        image: busybox
        command: ["sh", "-c", "echo Hello World! && sleep 30"]
      restartPolicy: Never
CronJob Spec Example
apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-cronjob
  namespace: my-namespace
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app: my-cronjob
        spec:
          containers:
          - name: my-container
            image: busybox
            command: ["sh", "-c", "echo Hello World! && sleep 30"]
          restartPolicy: Never

Workflow

当我们使用 kubectl apply 命令将这个 Job 资源提交给 API Server 之后

这个 Job 资源会先提交给 API Server, 然后由 API Server 持久化到 etcd; 这个时候 Job 控制器会通过 watch API Server 观察 Job 资源, 当发现有一个新的 Job 资源被创建出来了

于是他就会根据这个 Job 资源中定义的 Pod 模板, 通过 API Server 创建新的 Pod 对象, 这样这些新的 Pod 就会被 Scheduler 通过 watch API Server 观察到, 然后 Scheduler 就会将这些 Pod 调度到合适的节点上

最后 Job 控制器就会监视这些 Pod 的状态, 当发现这些 Pod 都成功的运行了一次了, 那么他在内部就会将这个 Job 定义的完成次数减一, 直到这个 Job 定义的完成次数为零了, 那么这个 Job 就算成功了; 如果这些 Pod 中有一个失败了, 那么他就会根据这个 Job 定义的重试策略来决定是否需要重试了

如果成功了, 那么就通过 API Server 更新这个 Job 的状态, 例如将 status.conditions 中的 type=Complete 的 status 设置为 True, 这样我们就可以通过这个 Job 的 status.conditions 来判断这个 Job 是否成功了; 如果失败了, 那么就通过 API Server 更新这个 Job 的状态, 例如将 status.conditions 中的 type=Failed 的 status 设置为 True, 这样我们就可以通过这个 Job 的 status.conditions 来判断这个 Job 是否失败了

6. Deployment

我们在 Kubernetes 会部署各种各样的服务, 比如各种后端服务, 这些服务可能会有一些版本迭代, 例如我们现在有一个后端服务的版本是 v1 的, 但是我们现在需要升级这个后端服务的版本到 v2 的, 那么我们就需要使用 Deployment 来实现这个升级过程了

Deployment 资源其实是 ReplicaSet 的一个更高级的抽象, 他同样是通过 label selectore 定义期望状态, 以及通过 Pod 模板定义如果当前状态不满足期望状态, 那么应该创建什么样的 Pod 来使当前状态逐渐接近期望状态的

但是他还定义了一些其他的信息, 例如升级策略, 回滚策略等等, 这样我们就可以通过 Deployment 来实现一些更复杂的升级和回滚的功能了

既然是定义一个升级过程, 那么我们就可以定义这些:

  • 升级策略: 会有百分之多少的 Pod 可以同时被升级了, 以及在升级过程中最多有多少个 Pod 是不可用的, 例如我们定义升级策略是 maxUnavailable=1, maxSurge=1 的话, 那么在升级过程中最多有一个 Pod 是不可用的, 同时最多有一个新的 Pod 是被创建出来的
  • 保留历史版本: 这个 Deployment 资源会保留多少个历史版本了, 例如我们定义保留历史版本是 3 的话, 那么这个 Deployment 资源就会保留最近的三个版本了, 这样我们就可以通过这些历史版本来进行回滚

Spec Example

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  namespace: my-namespace
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-deployment
  template:
    metadata:
      labels:
        app: my-deployment
    spec:
      containers:
      - name: my-container
        image: nginx:1.14
        ports:
        - containerPort: 80
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1

Workflow

当我们使用 kubectl apply 命令将这个 Deployment 资源提交给 API Server 之后

这个 Deployment 资源会先提交给 API Server, 然后由 API Server 持久化到 etcd。Deployment 控制器会通过 watch API Server 观察 Deployment 资源, 并持续让系统当前状态向 Deployment 的期望状态收敛。

初次创建时, Deployment 控制器会基于当前 PodTemplateSpec 创建一个新的 ReplicaSet。ReplicaSet 再通过 API Server 创建 Pod, Scheduler 通过 watch API Server 观察这些未绑定 Pod 并完成调度。

当我们需要升级 Deployment(例如将镜像从 nginx:1.14 升级到 nginx:1.16)时, 更新后的 PodTemplateSpec 会触发创建一个新的 ReplicaSet(带新的版本哈希), 而旧的 ReplicaSet 不会立刻删除。

随后 Deployment 控制器会按 RollingUpdate 策略(例如 maxSurge 和 maxUnavailable)逐步扩容新 ReplicaSet、逐步缩容旧 ReplicaSet, 并在过程中持续检查可用副本数是否满足约束。直到新 ReplicaSet 达到目标副本且旧 ReplicaSet 缩容完成, 这次发布才算完成。

用户如何触发升级

最常见的两种方式:

  1. 修改 YAML 后重新 apply
# 把镜像从 nginx:1.14 改为 nginx:1.16
kubectl apply -f my-deployment.yaml
  1. 直接在线更新镜像
kubectl set image deployment/my-deployment my-container=nginx:1.16 -n my-namespace

动态更改 ReplicaSet 示例

下面这组命令可以直观看到“新 RS 扩容, 旧 RS 缩容”的过程:

# 1) 先看当前 Deployment 和 ReplicaSet
kubectl get deploy my-deployment -n my-namespace
kubectl get rs -n my-namespace -l app=my-deployment
 
# 2) 触发升级
kubectl set image deployment/my-deployment my-container=nginx:1.16 -n my-namespace
 
# 3) 观察滚动发布状态
kubectl rollout status deployment/my-deployment -n my-namespace
 
# 4) 持续观察 RS 的副本变化(新 RS up, 旧 RS down)
kubectl get rs -n my-namespace -l app=my-deployment -w

在第 4 步中你通常会看到类似变化:

  • 旧 ReplicaSet: DESIRED/READY3 逐步降到 0
  • 新 ReplicaSet: DESIRED/READY0 逐步升到 3

如果要回滚到上一个版本:

kubectl rollout undo deployment/my-deployment -n my-namespace

7. StatefulSet

StatefulSet 是有状态业务的核心编排对象。

Deployment 关注“无状态副本的一致发布”, StatefulSet 关注“副本身份稳定 + 存储稳定 + 启停有序”。

StatefulSet 解决什么问题

典型场景: MySQL/Redis/Kafka/ZooKeeper 等需要稳定网络身份和持久卷绑定的服务。

它提供三类稳定性:

  1. 稳定网络标识
  • Pod 名是有序且固定的, 例如 web-0, web-1, web-2
  1. 稳定存储绑定
  • 通过 volumeClaimTemplates 给每个副本生成独立 PVC
  • web-0 重建后仍绑定自己的那块卷
  1. 有序部署/更新/删除
  • 默认按序号顺序创建与终止

Headless Service 的关系

StatefulSet 通常需要一个 Headless Service(clusterIP: None)来提供稳定 DNS 记录。

例如:

  • web-0.nginx.default.svc.cluster.local
  • web-1.nginx.default.svc.cluster.local

这和前文普通 Service 的“统一 VIP”不同, 这里更强调“每个副本都有可寻址身份”。

它到底是怎么做到的(对象与控制器视角)

API Server 中几类对象被持续维护:

  1. StatefulSet 对象
  • 保存期望副本数、Pod 模板、volumeClaimTemplates、更新策略等
  1. Pod 对象
  • 保存每个有序副本的身份(例如 web-0)
  • PodSpec 里会记录它引用的 PVC 名称
  1. PVC/PV 对象
  • PVC 记录“我要多大存储”
  • PV/PVC 的 claimRef 与绑定状态记录“这块卷已经归谁”
  1. StatefulSet 状态字段
  • 例如 status.replicasstatus.readyReplicasstatus.currentRevision/updateRevision
  • 用来反映当前收敛进度

StatefulSet controller 会 watch StatefulSet/Pod/PVC, 然后不断把“当前状态”往“期望状态”收敛。

另外, StatefulSet 还会用 ControllerRevision 保存历史模板版本, 这就是它能做有序滚动更新和版本跟踪的基础之一。

Spec Example

Headless Service + StatefulSet Example
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: my-namespace
spec:
  clusterIP: None
  selector:
    app: nginx
  ports:
  - port: 80
    name: web
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
  namespace: my-namespace
spec:
  serviceName: nginx
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi

Workflow

当 StatefulSet 创建后, 典型过程是:

  1. 用户提交 Headless Service + StatefulSet
  • API Server 持久化这两个对象到 etcd
  1. StatefulSet controller watch 到新的 StatefulSet
  • spec.replicas 和序号规则, 先确保 web-0 存在, 再考虑 web-1...
  • 默认策略下是有序创建(OrderedReady)
  1. controller 先为每个序号副本生成 PVC
  • 基于 volumeClaimTemplates 生成命名确定的 PVC, 例如 data-web-0
  • PVC 对象里会记录容量/访问模式等声明
  1. 存储控制面完成 PVC 绑定
  • PV controller(或 CSI 动态供应器)把 PVC 绑定到某个 PV
  • 绑定关系会体现在 PVC/PV 的状态与引用字段中
  1. controller 创建对应 Pod
  • Pod 名固定为 web-0/web-1...
  • PodSpec 的 volumes[].persistentVolumeClaim.claimName 指向对应 PVC(例如 data-web-0)
  • 这一步其实就把“Pod -> PVC”关系写进了 API 对象
  1. Scheduler 进行调度
  • 除了 CPU/内存 requests, 还要考虑卷可达性(例如可用区/拓扑约束)
  • 调度成功后, kubelet 在节点上完成挂载并启动容器
  1. 状态回写与持续收敛
  • kubelet/控制器通过 API Server 持续上报状态
  • StatefulSet status 持续更新, controller 继续 watch 并收敛
  1. Pod 异常重建时的稳定性来源
  • 名字仍是同一个序号(如 web-0)
  • Pod 仍引用同名 PVC(data-web-0)
  • 因此可以挂回同一份持久化数据

你可以把它理解成: “身份稳定”与“存储稳定”都不是运行时临时行为, 而是被 API 对象名称与引用关系长期记录下来的。

更新时默认 RollingUpdate 也是有序推进, 这对有状态集群的主从切换和一致性更友好。

和 Deployment 的快速对比

  1. Deployment:
  • Pod 身份是可替换的
  • 适合无状态服务
  1. StatefulSet:
  • Pod 身份与存储绑定稳定
  • 适合有状态服务

8. Volume

我么之前说过 Pod类似逻辑主机,在逻辑主机中运行的进程共享诸如CPU、RAM、网络接等资源

但是每个容器都有自己的 Mount Namespace, 这意味着它们可以有不同的文件系统视图, 这使得它们可以使用不同的卷来存储数据

在某些场景下,我们可能希望新的容器可以在之前容器结束的位置继续运行,比如在物理机上重启进程时,进程会继续使用之前的文件系统和数据

Kubernetes 使用 Volume 来实现这个功能,值得注意的是 Volume 并不是一种顶级资源, 他是 Pod 的一部分, 并且和 Pod 的生命周期是绑定的, 也就是说当这个 Pod 被删除了, 那么这个 Volume 也会被删除了

那还有什么用呢?

  1. 共享数据: 同一个 Pod 中的多个容器可以通过 Volume 来共享数据, 这样这些容器就可以通过这个 Volume 来进行通信了
  2. 虽然说 Pod 删除了, Volume 也会删除, 但是这是对于 Pod 来说的, 如果只是 Pod 中的 Container 死亡了, 那么这个 Volume 是不会被删除的, 这样当这个 Container 被重启了之后, 他就可以继续使用之前的 Volume 来存储数据了

alt text Volume 是 Pod 的一部分

emptyDir

emptyDir 顾名思义, 就是一个空的 Volume, 主要用途就是在同一个 Pod 中的多个容器之间共享数据了

emptyDir Volume Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
    - name: my-container
      image: nginx
      volumeMounts:
        - name: my-volume
          mountPath: /usr/share/nginx/html
    - name: my-sidecar
      image: busybox
      command: ["sh", "-c", "echo Hello World! > /data/index.html && sleep 3600"]
      volumeMounts:
        - name: my-volume
          mountPath: /data
  volumes:
    - name: my-volume
      emptyDir: {}

在这个例子中, 我们定义了一个名为 my-pod 的 Pod, 这个 Pod 中有两个容器: my-containermy-sidecar

对于 volume 的部分, 我们可以这样理解, 我们通过 volumes 定义了一个名为 my-volume 的 Volume, 这个 Volume 是一个 emptyDir 类型的 Volume

然后我们通过 volumeMounts 将这个 Volume 挂载到这两个容器中

Workflow

当我们使用 kubectl apply -f my-pod.yaml 创建这个 Pod 之后, emptyDir 的生命周期和这个 Pod 绑定, 可以理解为:

  1. API Server 接收 Pod 定义并持久化, Scheduler 将 Pod 调度到某个节点
  2. 该节点上的 Kubelet 在启动 Pod 时, 先创建一个空目录作为 emptyDir, 再把它分别挂载到两个容器的挂载点
  3. 容器 A 写入的数据, 容器 B 可以立即读到(因为它们挂载的是同一个 emptyDir)
  4. 如果只是某个容器重启, emptyDir 不会丢失; 但只要 Pod 被删除并重建, emptyDir 会被重新初始化为空目录

其实就是根据 Pod 的定义的区别, 导致 Kubelet 在创建 Pod 的时候行为不同

可以用下面命令快速验证:

kubectl apply -f my-pod.yaml
kubectl exec -n my-namespace my-pod -c my-sidecar -- sh -c 'echo hello > /data/index.html'
kubectl exec -n my-namespace my-pod -c my-container -- cat /usr/share/nginx/html/index.html

GitRepo

GitRepo 是一种特殊的 Volume, 他会将一个 Git 仓库克隆到这个 Volume 中, 这样这个 Volume 就会包含这个 Git 仓库中的代码

GitRepo Volume Example
apiVersion: v1
kind: Pod
metadata:
    name: my-pod
    namespace: my-namespace
spec:
    containers:
    - name: my-container
      image: nginx
      volumeMounts:
      - name: my-volume
        mountPath: /usr/share/nginx/html
    volumes:
    - name: my-volume
      gitRepo:
        repository: https://github.com/example/repo.git
        revision: master

注意, 这个 GitRepo 不能实现自动更新, 也就是说当这个 Git 仓库中的代码发生变化了, 这个 Volume 中的代码是不会自动更新的, 只有当这个 Pod 被重新创建了之后, 这个 Volume 中的代码才会被更新了

除非你在这里再定义一个 sidecar 容器, 这个 sidecar 容器的作用就是不断地监视这个 Git 仓库中的代码是否发生变化了, 如果发生变化了, 那么就通过 API Server 来更新这个 Pod 的定义, 这样这个 Pod 就会被重新创建了, 这样这个 Volume 中的代码就会被更新了

Workflow

与上文的 emptyDir 类似, 只是 Kubelet 在创建 Pod 时, 先根据 gitRepo 定义克隆指定仓库到 Volume 中, 然后再挂载到容器; 后续这个 Volume 不会自动更新, 只有当 Pod 被删除重建时才会重新克隆最新代码

HostPath

大多数情况下, 我们都是认为 Kubernetes 是一个完全隔离的环境, 也就是说 Pod 应该忽略他的主机环境, 但是有的时候我们可能需要访问主机上的一些资源, 例如主机上的一些日志文件, 特别是一些系统级别的 Pod (比如一些通过 DaemonSet 创建的 Pod), 这样我们就可以通过 HostPath 来实现这个功能了

alt text

HostPath Volume Example
apiVersion: v1
kind: Pod
metadata:
    name: my-pod
    namespace: my-namespace
spec:
    containers:
    - name: my-container
      image: nginx
      volumeMounts:
      - name: my-volume
        mountPath: /usr/share/nginx/html
    volumes:
    - name: my-volume
      hostPath: 
      path: /usr/share/nginx/html
      type: Directory

Workflow

同理, 只是 Kubelet 在创建 Pod 时, 直接将主机上的 /usr/share/nginx/html 目录挂载到容器的 /usr/share/nginx/html 目录; 这样这个 Volume 中的数据就不随 Pod 的生命周期而变化了, 只要主机上的数据不变了, 那么这个 Volume 中的数据也不会变了

持久化存储(云厂商提供的存储服务/NFS/GlusterFS/CephFS 等等)

前文说了, 只有像 emptyDir 这类临时卷才会和 Pod 的生命周期严格绑定, 也就是说当这个 Pod 被删除了, 那么这个 Volume 也会被删除

这样似乎好像没办法实现, 数据的持久化存储了, 如果这个 Pod 是个数据库, 那么但他重启了, 那么之前存储在这个数据库中的数据就没了, 这显然是不可接受的

你可能会想用 hostPath 来实现这个功能, 但是你只能期望, 这个 Pod 永远都被调度到同一个节点上, 这样他就可以一直使用这个节点上的这个 hostPath 来存储数据了, 但是这显然也是不可接受的

针对这个问题, 我们可以使用一些云厂商提供的存储服务来实现这个功能了, 例如 AWS 的 EBS, GCP 的 Persistent Disk, Azure 的 Disk Storage 等等, 这些存储服务都是独立于 Kubernetes 的, 他们有自己的生命周期, 这样当这个 Pod 被删除了, 这个 Volume 也不会被删除了, 这样当这个 Pod 被重新创建了之后, 他就可以继续使用之前的 Volume 来存储数据了

也就是说, 对于这类外部持久化存储, 真正和数据生命周期绑定的通常是 PV/PVC 背后的存储资源, 而不是 Pod 本身。

alt text

这里用 GCE 作为例子

GCE Persistent Disk Volume Example
apiVersion: v1
kind: Pod
metadata:
    name: my-pod
    namespace: my-namespace
spec:
    containers:
    - name: my-container
      image: postgres
      volumeMounts:
      - name: cloud-volume
        mountPath: /var/lib/postgresql/data
    volumes:
    - name: cloud-volume
      gcePersistentDisk:
        pdName: my-gce-disk
        fsType: ext4

volumes 这里不同的云厂商可能不一样, 并且不同的云厂商可能还会有一些额外的参数, 例如 AWS 的 EBS 还会有 volumeID, Azure 的 Disk Storage 还会有 diskName 和 diskURI 等等, 而且可能需要提前创建好这些存储资源, 例如 AWS 的 EBS 需要提前创建好 EBS 卷, GCP 的 Persistent Disk 需要提前创建好 Persistent Disk 卷, Azure 的 Disk Storage 需要提前创建好 Disk Storage 卷等等

当然, Kubernetes 不是只能使用云厂商提供的存储服务来实现持久化存储的功能, 还可以使用一些分布式文件系统来实现这个功能了, 例如 NFS, GlusterFS, CephFS 等等, 这些分布式文件系统也是独立于 Kubernetes 的, 他们有自己的生命周期, 这样当这个 Pod 被删除了, 这个 Volume 也不会被删除了, 这样当这个 Pod 被重新创建了之后, 他就可以继续使用之前的 Volume 来存储数据了

直接使用 gcePersistentDisk 的 Workflow

当我们在 Pod 里直接写 gcePersistentDisk 时, 整体流程通常是:

  1. 先在云平台创建磁盘(例如 my-gce-disk), 并确认容量/分区/可用区
  2. 用户提交 Pod 定义到 API Server, 其中 volumes.gcePersistentDisk.pdName 指向现有磁盘
  3. Scheduler 在调度时会考虑卷约束(例如可用区), 尽量把 Pod 调度到可附加该磁盘的节点
  4. 节点上的 Kubelet 调用云厂商接口完成 attach/mount, 然后再启动容器
  5. 容器通过 mountPath 读写数据, Pod 重建后仍可挂载同一块磁盘(数据不随 Pod 删除)

常用观察命令:

kubectl apply -f my-gce-pod.yaml
kubectl get pod -n my-namespace -w
kubectl describe pod my-pod -n my-namespace

补充: 直接在 Pod 里写云厂商卷类型更适合学习和演示, 生产环境更推荐走 CSI + PVC 的方式。

PersistentVolume 和 PersistentVolumeClaim

前面我们说了, 想要持久化存储, 就不应该把数据存储在 Worker 节点的本地磁盘上, 而是应该把数据存储在一些独立于 Kubernetes 的存储系统上, 例如云厂商提供的存储服务, 例如 AWS 的 EBS, GCP 的 Persistent Disk, Azure 的 Disk Storage 等等, 也可以使用一些分布式文件系统来实现这个功能了, 例如 NFS, GlusterFS, CephFS 等等

Wait, 这也太多了吧, 显然不太方便迁移啥的, 而且对于一般的程序员(非 Kubernetes 管理员)来说, 也不太友好, 因为他们可能并不清楚这些存储系统的具体细节, 例如 AWS 的 EBS 需要提前创建好 EBS 卷, GCP 的 Persistent Disk 需要提前创建好 Persistent Disk 卷, Azure 的 Disk Storage 需要提前创建好 Disk Storage 卷等等

因此 Kubernetes 提供了 PersistentVolume 和 PersistentVolumeClaim 来抽象这些存储系统

想法很简单, 我们这里把程序员/部署人员 看作是 Kubernetes 的用户, 他们不需要关心底层的存储系统是什么, 他们只需要关心自己需要多少存储空间, 以及这个存储空间需要满足什么样的性能要求等等, 他们只需要填入这些信息就好了, 于是这些信息我们就叫做 PersistentVolumeClaim

相对的, Kubernetes 管理员需要关心底层的存储系统是什么, 以及这个存储系统需要满足什么样的性能要求等等, 他们可以根据当前的部署环境, 创建相关的存储卷, 例如在 AWS 上创建 EBS 卷, 在 GCP 上创建 Persistent Disk 卷, 在 Azure 上创建 Disk Storage 卷, 自建机房上创建 NFS 卷等等, 这些存储卷我们就叫做 PersistentVolume

PersistentVolume Example
# 我们这里假设部署在 AWS 上, 因此我们创建了一个 EBS 卷
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  awsElasticBlockStore:
    volumeID: vol-0abcd1234ef567890
    fsType: ext4
PersistentVolumeClaim Example
# 这是用户视角, 他只在乎自己需要多少存储空间, 以及这个存储空间需要满足什么样的性能要求等等, 他并不关心底层的存储系统是什么
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
  namespace: my-namespace
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
 
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: postgres
    volumeMounts:
    - name: my-volume
      mountPath: /var/lib/postgresql/data
  volumes:
  - name: my-volume
    persistentVolumeClaim:
      claimName: my-pvc

在这个例子中, 我们定义了一个 PersistentVolume, 这个 PersistentVolume 是一个 AWS 的 EBS 卷, 这个卷的 ID 是 vol-0abcd1234ef567890, 这个卷的容量是 10Gi, 这个卷的访问模式是 ReadWriteOnce

有如下访问模式:

  • ReadWriteOnce: 这个卷在同一时刻只能被一个节点以读写方式挂载, 也就是说它通常只能被一个工作负载的一个副本独占写入; 但这个 Pod 仍然可以在具备挂载条件的节点之间被调度, 具体可用性取决于底层存储实现和访问模式支持
  • ReadOnlyMany: 这个卷可以被多个节点以只读的方式挂载, 也就是说这个卷可以被多个 Pod 使用, 但是这些 Pod 只能以只读的方式使用这个卷
  • ReadWriteMany: 这个卷可以被多个节点以读写的方式挂载, 也就是说这个卷可以被多个 Pod 使用, 这些 Pod 可以以读写的方式使用这个卷

我们还可以定义回收的策略, 例如当这个 PersistentVolumeClaim 被删除了之后, 这个 PersistentVolume 应该被删除了, 还是应该被保留了。现在常见的策略主要是 Delete 和 Retain: 如果我们定义回收策略是 Delete 的话, 那么当这个 PersistentVolumeClaim 被删除了之后, 这个 PersistentVolume 就会被删除了; 如果我们定义回收策略是 Retain 的话, 那么当这个 PersistentVolumeClaim 被删除了之后, 这个 PersistentVolume 就会被保留了。Recycle 是更早期的策略, 现在一般不再作为常规选择了

Workflow

PV/PVC 的核心是“声明存储需求”和“解耦底层存储实现”, 典型流程如下:

  1. 管理员准备存储供给
  • 可以手动创建 PV
  1. 用户创建 PVC(声明需要多少空间、访问模式等)
  • API Server 持久化 PVC 后, 控制器会尝试把 PVC 和满足条件的 PV 绑定
  1. 绑定成功后, PVC 状态会变成 Bound
  2. 用户创建引用该 PVC 的 Pod
  • Scheduler 调度 Pod, Kubelet 在目标节点完成卷的 attach/mount, 然后启动容器
  1. Pod 即可通过挂载路径读写持久化数据
  2. 删除顺序和回收策略
  • 删除 Pod 不会删除 PV/PVC
  • 删除 PVC 后, PV 如何处理取决于 persistentVolumeReclaimPolicy(例如 Retain/Delete)

可以按这个顺序操作和观察:

# 1) 管理员创建 PV(或先创建 StorageClass)
kubectl apply -f my-pv.yaml
 
# 2) 用户创建 PVC
kubectl apply -f my-pvc.yaml
kubectl get pvc -n my-namespace -w
 
# 3) 绑定结果
kubectl get pv
kubectl get pvc -n my-namespace
 
# 4) 创建使用 PVC 的 Pod
kubectl apply -f my-pod-use-pvc.yaml
kubectl get pod -n my-namespace -w
kubectl describe pod my-pod -n my-namespace
 
# 5) 验证数据写入
kubectl exec -n my-namespace my-pod -- sh -c 'echo data > /var/lib/postgresql/data/test.txt'

StorageClass

前面我们说了, PersistentVolume 和 PersistentVolumeClaim 是 Kubernetes 提供的抽象, 他们的作用就是让用户不需要关心底层的存储系统是什么, 他们只需要关心自己需要多少存储空间, 以及这个存储空间需要满足什么样的性能要求等等, 他们只需要填入这些信息就好了, 存储的管理就交给 Kubernetes 管理员来手动创建 PersistentVolume 来实现了, 但是如果我们有很多用户, 那么我们就需要创建很多 PersistentVolume 来满足这些用户的需求了, 这显然也是不可接受的

幸运的是, Kubernetes 还提供了 StorageClass 来进一步抽象 PersistentVolume

StorageClass 是一个顶级资源, 他描述了如何动态创建 PV 的过程

例如, 如果我们是部署在自建的机房中的话, 背后的存储系统是 ceph, 那么管理员就可以写一个程序, 这个程序接收 PVC 中的一些参数, 例如容量, 访问模式等等, 然后这个程序就会根据这些参数来创建一个 ceph 卷, 然后再把这个 ceph 卷封装成一个 PV 对象, 这样当用户创建 PVC 的时候, Kubernetes 就会调用这个程序来动态创建 PV 来满足这个 PVC 的需求了

最后创建一个 StorageClass 来指向这个程序, 这样用户在创建 PVC 的时候, 就可以通过指定 StorageClass 来使用这个程序来动态创建 PV 来满足这个 PVC 的需求了

同时, 不同的云厂商也提供了他们自己存储服务的 StorageClass 的实现

StorageClass Example
# 如果是自己写的分配存储的程序, 那么 provisioner 就是这个程序的名字了, 例如我们这里就叫做 my-provisioner; parameters 中的参数就是这个程序需要的参数了, 例如我们这里就定义了一个 type 参数, 这个参数的值是 gp2, 这个参数可能是这个程序需要的参数了, 也可能不是这个程序需要的参数了, 这取决于这个程序的实现了
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: my-storage-class
provisioner: my-provisioner
parameters:
  type: gp2
 
---
# AWS EBS 的 StorageClass 实现
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-ebs
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
  fsType: ext4
使用 StorageClass 的 PVC Example
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
  namespace: my-namespace
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: my-storage-class

Workflow

StorageClass 的核心是“动态供应存储”, 典型流程如下:

  1. 管理员创建 StorageClass, 指定 provisioner 和相关参数
  2. 用户创建 PVC, 在 spec.storageClassName 指定 StorageClass
  3. 控制器检测到 PVC 需要动态供应, 调用对应 provisioner 来创建 PV
  4. 供应成功后, PVC 和新 PV 绑定, PVC 状态变为 Bound
  5. 用户创建引用该 PVC 的 Pod, 后续流程同前文 PV/PVC 部分
  6. 删除 PVC 后, StorageClass 定义的回收策略生效(例如删除 PV)

9. ConfigMap

开发一款应用的同时, 我们除了将一些配置直接写入程序本身, 还可以把这些配置放在外部, 这样就实现了动态的配置

这些配置可以用分别是配置文件和环境变量或者是运行的参数, 我们通过 Kubernetes 部署的应用如何实现这些呢?

命令行参数

给容器传入命令行参数 Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    args: ["--port=8080", "--env=prod"]
   #command: ["/bin/sh", "-c", "echo Hello World! && sleep 3600"] 

这里的 commmand 对应 Dockerfile 中的 ENTRYPOINT, args 对应 Dockerfile 中的 CMD, 他们的关系如下:

  • 如果只定义了 command, 那么这个 command 就会覆盖掉 Dockerfile 中的 ENTRYPOINT 和 CMD, 也就是说这个 command 就会成为这个容器的新的 ENTRYPOINT
  • 如果只定义了 args, 那么这个 args 就会覆盖掉 Dockerfile 中的 CMD, 也就是说这个 args 就会成为这个容器的新的 CMD
  • 如果同时定义了 command 和 args, 那么这个 command 就会覆盖掉 Dockerfile 中的 ENTRYPOINT, 这个 args 就会覆盖掉 Dockerfile 中的 CMD

环境变量

给容器传入环境变量 Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    env:
    - name: ENV
      value: prod

我们也能在 Env 中定义这些容器的环境变量

但是, 你会发现, 每次我们需要修改这些环境变量的时候, 都需要修改这个 Pod 的定义, 如果我们开发环境和部署环境的环境变量不一样了, 那么我们就需要维护两个 Pod 的定义了

这个显然太麻烦了, 让我们把这个 env 单独拿出来吧! Kubernetes 提供了 ConfigMap 来实现这个功能了

ConfigMap 定义

ConfigMap 实际上就是一个 K-V 键值对的数据结构, 我们提交这个资源给 API Server, 实际上就是把这个数据结构存储在 etcd 中了

这个 ConfigMap 在 Pod 中可以通过环境变量或者卷文件的方式使用 alt text

Spec Example

ConfigMap Example
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-configmap
  namespace: my-namespace
data:
  ENV: prod

作为环境变量

ConfigMap 作为环境变量 Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    env:
    - name: ENV
      valueFrom:
        configMapKeyRef:
          name: my-configmap
          key: ENV
# 表达这个容器中的什么环境变量来自哪个 ConfigMap 中的哪个 Key
---
# 我们也可以全盘接受一个 ConfigMap 的K-V作为所有的环境变量
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    envFrom:
    - configMapRef:
        name: my-configmap

作为卷文件

ConfigMap 作为卷文件 Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    volumeMounts:
    - name: config-volume
      mountPath: /etc/config
  volumes:
  - name: config-volume
    configMap:
      name: my-configmap
# 使用 subPath 还可以把 ConfigMap 中的某个 Key 挂载到容器中的某个路径下

Workflow

其实ConfigMap 的 Workflow 和前面我们说的 Volume 的 Workflow 是一样的, 只是 Kubelet 在创建 Pod 的时候, 他会先通过 API Server 获取这个 Pod 定义中的这个 ConfigMap 的定义, 然后再根据这个 ConfigMap 的定义来创建一个 Volume, 最后再把这个 Volume 挂载到容器中去, 这样这个容器就可以通过这个 Volume 来访问这个 ConfigMap 中的数据了

10. Secret

上述我们传递的都是一些常规的配置信息

但是配置信息往往还会包含一些敏感信息, 如一些 API 密钥, 邮件的认证信息等

这个时候, 我们应该使用 Secret 来替代 ConfigMap, 他本质和 ConfigMap 是一样的, 也是一个 K-V 键值对的数据结构, 也是通过 API Server 存储在 etcd 中的, 也是可以通过环境变量或者卷文件的方式使用的

不过, Secret 中的数据是经过 Base64 编码的, 这样就可以避免一些直接暴露敏感信息的问题了, 但是需要注意的是, Base64 编码并不是加密, 也就是说如果有人获取到了这个 Secret 的定义了, 那么他就可以通过 Base64 解码来获取到这个 Secret 中的敏感信息了

在 Kubernetes 1.6 之后, Secret 还支持了加密存储, 他在 etcd 中存储的时候是加密的

Spec Example

Secret Example
apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: my-namespace
type: Opaque
data:
  API_KEY: dGVzdC1hcGkta2V5LWFiY2QtMTIzNDU2

作为环境变量

Secret 作为环境变量 Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    env:
    - name: API_KEY
      valueFrom:
        secretKeyRef:
          name: my-secret
          key: API_KEY
-----
# 同理和 ConfigMap 一样, 也能全盘接受一个 Secret 的K-V作为所有的环境变量
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    envFrom:
    - secretRef:
        name: my-secret

作为卷文件

Secret 作为卷文件 Example
apiVersion: v1
kind: Pod
metadata: 
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: nginx
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secret
  volumes:
  - name: secret-volume
    secret:
      name: my-secret

这里和 ConfigMap 不同的是, 他挂载的卷并不会放在磁盘, 而是通过 tmpfs 的方式挂载在内存中的, 这样就可以避免一些敏感信息被泄露的问题了, 但是需要注意的是, 这种方式也不是绝对安全的, 因为如果有人获取到了这个 Pod 的定义了, 那么他就可以通过一些手段来获取到这个 Secret 中的敏感信息了

Workflow

Secret 的 Workflow 和 ConfigMap 的 Workflow 是一样的, 同样是 kubelet 创建 Pod 的时候, 先通过 API Server 获取这个 Pod 定义中的这个 Secret 的定义, 然后传入环境变量或者挂载卷之类的

隐藏的 Secret

在每个 Pod 中, Kubernetes 都会自动创建一个名为 default-token-xxxx 的 Secret, 这个 Secret 中包含了这个 Pod 的 ServiceAccount 的 token, 这样这个 Pod 就可以通过这个 token 来访问 Kubernetes API Server 了, 这个 Secret 是自动创建的, 也是自动挂载到这个 Pod 中的, 但是我们在定义 Pod 的时候是看不到这个 Secret 的定义的, 因此我们把这个 Secret 称为隐藏的 Secret

这个再之后的关于 Auth 的内容很重要

拉取私有镜像仓库

Pod 的 Image 是可以从私有仓库中拉取的, 如果这个仓库需要相应的 Token, 则通过 imagePullSecrets 定义, 他是通过引用一个 Secret 来实现的

拉取私有镜像仓库 Example
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-namespace
spec:
  containers:
  - name: my-container
    image: my-private-registry.com/my-image:latest
  imagePullSecrets:
  - name: my-registry-secret

前文说了, kubelet 在创建 Pod 的时候, 会先通过 API Server 获取这个 Pod 定义中的这个 imagePullSecrets 的定义, 然后再根据这个 imagePullSecrets 的定义来获取这个 Secret 中的敏感信息了, 最后再把这个敏感信息传递给容器来拉取这个私有仓库中的镜像了

结合 ServiceAccount 和 RBAC 管理私有仓库密钥

在实际环境里, 一般不希望每个 Pod 都手写 imagePullSecrets, 更常见是把它挂在 ServiceAccount 上, 让同类工作负载默认继承。

对象关系可以理解为:

  1. Secret(type: kubernetes.io/dockerconfigjson)保存仓库凭据
  2. ServiceAccount 在 imagePullSecrets 引用这个 Secret
  3. Pod 通过 serviceAccountName 继承对应拉镜像凭据

RBAC 在这条链路里的作用是限制敏感操作权限, 例如:

  1. 谁可以创建/更新 docker-registry Secret
  2. 谁可以 patch ServiceAccount 挂载 imagePullSecrets
  3. 谁可以读取 Secret

Workflow

  1. 管理员或 CI 创建私有仓库 Secret, API Server 持久化
  2. 更新 ServiceAccount 的 imagePullSecrets, API Server 持久化
  3. 用户创建引用该 SA 的 Pod
  4. Scheduler 完成调度后, kubelet 拉取 Pod 定义
  5. kubelet 读取 Pod/SA 关联的凭据, 向私有仓库认证并拉镜像
  6. 成功则启动容器, 失败常见 ErrImagePull/ImagePullBackOff

11. Service

你这个点提得非常对: Service 这块如果不先讲“子网和路由”会很抽象。

我们先建立一个非常实用的网络心智模型:

  1. 整个集群会规划两个大地址空间:
  • Pod 子网: 给 Pod 分配 IP, 例如 10.244.0.0/16
  • Service 子网: 给 Service 分配 VIP(ClusterIP), 例如 10.96.0.0/12
  1. 每个 Node 可以理解成“承载一批 Pod 网段的主机”, 在转发层面很像一个路由器/转发节点。
  2. Pod A 访问 Pod B 时, 先看是不是同节点; 跨节点时要么走 Overlay(VXLAN/Geneve), 要么走三层路由(BGP/underlay route)。
  3. Service 不是一个真实进程, 而是一组“虚拟 IP + 转发规则”。

Service 资源和 Endpoint 资源

Service 资源主要描述两件事:

  • 给一组 Pod 一个稳定入口(VIP + Port)
  • 用 selector 选后端

但 Service 不直接保存“后端 PodIP 列表”, 这个列表在 Endpoints/EndpointSlice 里:

  • Endpoints: 早期单对象模型
  • EndpointSlice: 新模型, 把后端切片存储, 更适合大规模 watch

你可以把它理解成:

  • Service = 门牌号(VIP)
  • EndpointSlice = 当前住户清单(PodIP:Port)

谁在维护 Endpoint

当 Service/Pod/Node 变化时, EndpointSlice controller 会 watch 这些对象并更新 EndpointSlice。

常见触发包括:

  1. Pod 新建/删除
  2. Pod Ready 状态变化
  3. Node 变化(影响可达性/拓扑信息)
  4. Service selector 变化

最终结果就是: EndpointSlice 始终尽量反映“当前可用后端 Pod 清单”。

kube-proxy 在做什么

kube-proxy 会 watch Service + EndpointSlice, 然后在每个节点写入数据面规则(iptables/IPVS/eBPF 路径)。

核心动作就是 DNAT:

  • ServiceVIP:Port 映射成某个 PodIP:TargetPort

也就是你说的那句本质: Service VIP 最终要被翻译成真实 Pod IP。

为什么要先 DNS

真实请求往往不是从 VIP 开始, 而是从 service name 开始, 比如 my-service

所以链路通常是:

  1. 先做 DNS 解析: my-service -> ServiceVIP
  2. 再做转发/NAT: ServiceVIP -> PodIP

Kubernetes 里 DNS 的实现通常是 CoreDNS, 但它本身也是一组 Pod。 为了稳定访问 DNS, 集群会提供一个 DNS Service(常见名 kube-dns)。

kubelet 如何把 DNS 写进容器

Pod 默认 dnsPolicy: ClusterFirst

kubelet 在创建容器时会写容器内的 /etc/resolv.conf, 典型会包含:

  • nameserver <DNS-Service-VIP>
  • search <ns>.svc.cluster.local svc.cluster.local cluster.local
  • options ndots:5

所以容器里直接访问 my-service 才能自动解析到 Service VIP。

不跨节点 vs 跨节点

下面是最关键的“路径感知”。

不跨节点(客户端 Pod 和后端 Pod 在同一个 Node)

  1. Pod A 解析 service name 得到 Service VIP
  2. 发包到 Service VIP
  3. 本机数据面规则 DNAT 到同机 Pod B
  4. 直接本机转发完成

特点:

  • 路径短
  • 不经过跨节点隧道/三层路由
  • 通常只需要 Service DNAT

跨节点(客户端 Pod 和后端 Pod 在不同 Node)

  1. Pod A 解析 service name 得到 Service VIP
  2. 本机先做 Service DNAT -> 远端 PodIP
  3. 包离开本节点, 经 CNI 网络到目标节点
  • 可能走 Overlay 封装(VXLAN/Geneve)
  • 也可能走纯路由(BGP/underlay)
  1. 到目标节点后再送到目标 Pod

特点:

  • 多一段节点间传输
  • 是否封装取决于 CNI 实现
  • Service 视角不变, 但底层转发路径变长

NodePort: 对 ClusterIP 的封装

NodePort 本质是“在 ClusterIP 基础上额外开一个 NodeIP:Port 入口”。

你可以这样记:

  • ClusterIP: 仅集群内可达
  • NodePort: 每个节点都暴露 NodeIP:NodePort

外部访问 NodePort 的典型路径(概念上两层 DNAT):

  1. Client -> NodeIP:NodePort
  2. 节点规则先把 NodeIP:NodePort DNAT 到 ServiceVIP:Port
  3. 再把 ServiceVIP:Port DNAT 到 PodIP:TargetPort

实现上不同数据面可能优化成一步命中后端, 但逻辑上可按这两层理解。

什么时候会有 SNAT:

  1. externalTrafficPolicy: Cluster
  • 可能转发到其他节点 Pod
  • 常见会 SNAT, Pod 侧可能看不到真实客户端 IP
  1. externalTrafficPolicy: Local
  • 只转发到本节点本地 Pod
  • 更容易保留客户端源 IP

LoadBalancer: 对 NodePort 的再封装

LoadBalancer 可以理解成“NodePort + 云厂商/机房 LB 能力”。

它会给你一个外部 IP, 典型链路是: Client -> LoadBalancerIP -> NodeIP:NodePort -> ServiceVIP -> PodIP

云厂商/自建 LB 通常会做这些事:

  1. 健康检查后端节点
  2. 在多个 Node 之间做负载均衡
  3. 可能在 LB 或节点侧发生地址转换(NAT), 具体取决于 LB 类型与转发模式

你提到的“LoadBalancerIP 到 NodeIP 的 SNAT”这个视角在很多实现里是成立的, 本质上就是 LB 在做连接代理/地址转换并承担健康检查与分发。

一句话收尾

Service 这一套可以压缩成 4 步:

  1. 名字解析: service name -> Service VIP(DNS)
  2. 后端维护: controller 根据 Pod/Node 变化更新 EndpointSlice
  3. 转发规则: kube-proxy watch Service/EndpointSlice 下发 DNAT 规则
  4. 跨节点可达: CNI 负责(overlay 或路由)

12. 认证与授权(RBAC / ServiceAccount)

前面我们一直在说“组件通过 API Server 交互”。

那就有一个核心问题: 谁可以调用 API? 可以做什么操作?

Kubernetes 对这个问题的回答是三层流水线:

  1. 认证(Authentication): 你是谁?
  2. 授权(Authorization): 你能做什么?
  3. 准入控制(Admission): 这次请求在策略上是否允许, 以及是否需要被修改?

可以把 API Server 理解成“集群唯一对外入口 + 安全网关”。

先认识几个资源

ServiceAccount(SA)

ServiceAccount 是给 Pod 内进程使用的“集群身份”。

当 Pod 访问 API Server 时, 通常不是用“人类用户”身份, 而是用 SA 身份。

常见形式:

  • 每个 Namespace 默认有一个 default SA
  • Pod 可以通过 spec.serviceAccountName 显式指定 SA

现代 Kubernetes 中, Pod 常通过 projected token 获取短期令牌(由 kubelet 挂载), 而不是早期那种长期 Secret token 模式。

Role / ClusterRole

这两个资源用来定义“权限集合”, 也就是 Policy Rule。

  • Role: Namespace 级别权限, 只在某个 Namespace 内生效
  • ClusterRole: 集群级权限, 或可被复用于任意 Namespace 的授权

规则通常描述:

  • apiGroups
  • resources
  • verbs(get/list/watch/create/update/patch/delete 等)

RoleBinding / ClusterRoleBinding

Binding 的作用是把“身份”绑定到“权限”。

  • RoleBinding: 在某个 Namespace 内绑定
    • 可以绑定 Role
    • 也可以绑定 ClusterRole(但生效范围仍是这个 Namespace)
  • ClusterRoleBinding: 集群级绑定 ClusterRole

一句话:

  • Role/ClusterRole = 权限定义
  • RoleBinding/ClusterRoleBinding = 权限授予动作

API Server 鉴权流程(请求路径)

这里用一个最常见场景: “某个 Pod 用 SA 调用 API Server, 列出 Pod”

  1. Pod 内程序发起 HTTPS 请求到 API Server
  • 会带上 Bearer Token(来自 SA token)
  1. API Server 做认证(Authentication)
  • 校验 token 是否有效、是否过期、签发者是否可信
  • 认证成功后得到用户标识, 例如:
    • system:serviceaccount:<namespace>:<sa-name>
  1. API Server 做授权(Authorization)
  • 把请求属性提取出来: verb/resource/namespace/name/apiGroup
  • 交给授权器(最常见是 RBAC)
  • RBAC 会查这个身份通过 Binding 关联到了哪些 Role/ClusterRole
  • 判断规则是否允许这次操作
  1. API Server 做准入控制(Admission)
  • 先走 mutating admission(可改请求对象)
  • 再走 validating admission(校验策略, 可拒绝)
  1. 全部通过后才会执行真实读写
  • 读操作返回结果
  • 写操作持久化到 etcd, 再由控制器 watch 后续收敛

所以鉴权链路里, RBAC 不是全部, 它只负责“第 3 步: 是否有权限”。

为什么经常会 403

最常见报错是: forbidden: User "system:serviceaccount:xxx:yyy" cannot list resource "pods" ...

这表示:

  1. 认证通过了(你是谁已经识别出来了)
  2. 授权没通过(RBAC 规则不匹配)

也就是“身份存在, 但没被授予这个动作权限”。

最小 RBAC 示例

下面这组 YAML 表示:

  • 创建一个 SA: reader-sa
  • 创建一个只读 Pod 的 Role
  • 用 RoleBinding 把权限绑定给这个 SA
ServiceAccount + Role + RoleBinding Example
apiVersion: v1
kind: ServiceAccount
metadata:
  name: reader-sa
  namespace: my-namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: my-namespace
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods-binding
  namespace: my-namespace
subjects:
- kind: ServiceAccount
  name: reader-sa
  namespace: my-namespace
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pod-reader

让 Pod 使用这个 SA:

Pod 使用指定 ServiceAccount
apiVersion: v1
kind: Pod
metadata:
  name: my-client
  namespace: my-namespace
spec:
  serviceAccountName: reader-sa
  containers:
  - name: client
    image: bitnami/kubectl:latest
    command: ["sh", "-c", "sleep 3600"]

Workflow

把这套资源 apply 之后, 整个流程可以这样观察:

  1. 创建 SA/Role/RoleBinding
  • API Server 持久化后, RBAC 授权数据生效
  1. 创建使用该 SA 的 Pod
  • kubelet 会为容器注入 SA token(通常是 projected volume)
  1. Pod 内调用 API Server
  • API Server 先认证该 token 对应 SA 身份
  • 再按 RBAC 规则授权
  1. 若允许, 返回资源; 若不允许, 返回 403

可用命令:

kubectl apply -f rbac.yaml
kubectl auth can-i list pods \
  --as=system:serviceaccount:my-namespace:reader-sa \
  -n my-namespace
 
kubectl auth can-i delete pods \
  --as=system:serviceaccount:my-namespace:reader-sa \
  -n my-namespace

预期是:

  • list pods = yes
  • delete pods = no

13. 弹性伸缩(Autoscaling)

Kubernetes 里“弹性伸缩”不是一个单点能力, 而是几类控制器协作:

  1. 工作负载副本数伸缩: HPA(HorizontalPodAutoscaler)
  2. 单 Pod 资源规格伸缩: VPA(VerticalPodAutoscaler)
  3. 集群节点数伸缩: Cluster Autoscaler(CA)

可以记成三层:

  • HPA: 调 Pod 数量
  • VPA: 调 Pod 规格
  • CA: 调 Node 数量

HPA: 横向扩缩容

HPA 的核心是根据指标自动调整 scale 子资源(通常是 Deployment/StatefulSet 的 replicas)。

常见指标来源:

  1. CPU/Memory 资源利用率(依赖 metrics-server)
  2. 自定义指标(例如 QPS)
  3. 外部指标(例如消息队列长度)

metrics-server 是什么

metrics-server 是一个聚合集群资源用量的组件, 它会周期性从各 Node 的 kubelet 拉取 Pod/Node 的 CPU、内存使用数据, 然后通过 Metrics API(metrics.k8s.io)提供给控制面查询。

在 HPA 场景里, 资源型指标(type: Resource, 如 CPU/Memory)通常就是通过它拿到的。

要注意它的边界:

  1. 它主要服务自动伸缩与 kubectl top
  2. 不负责长期存储历史指标
  3. 不是完整监控系统, 也不替代 Prometheus 这类方案

一个常见公式(简化理解):

desiredReplicas=ceil(currentReplicas×currentMetrictargetMetric)desiredReplicas = ceil(currentReplicas \times \frac{currentMetric}{targetMetric})

例如目标 CPU 利用率是 60%, 当前平均 120%, 那么副本数会趋向翻倍。

VPA: 纵向扩缩容

VPA 会根据历史资源使用情况, 建议或自动调整 Pod 的 requests/limits。

注意:

  1. 许多模式下 VPA 调整需要重建 Pod
  2. VPA 和 HPA 如果都同时基于 CPU/Memory 做决策, 可能相互干扰, 需要谨慎组合

Cluster Autoscaler: 节点伸缩

CA 主要盯两类信号:

  1. 有 Pending Pod 且因资源不足无法调度 -> 扩容 Node
  2. 某些 Node 长期低利用率且可安全驱逐 -> 缩容 Node

它本质上是“跟着调度结果走”: 调度器判定放不下, CA 才有扩容依据。

Workflow

以“请求突增”为例, 常见链路是:

  1. 业务流量升高, Pod CPU 利用率升高
  2. metrics-server 暴露资源指标给 HPA
  3. HPA 计算后提高 Deployment replicas
  4. 调度器尝试为新 Pod 找 Node
  5. 若集群容量足够, 直接调度成功
  6. 若容量不足导致 Pending, CA 扩容 Node
  7. 新 Node Ready 后, Pending Pod 被调度并运行

14. 资源限制与调度器视角

这个部分最关键的是: 哪些字段影响调度, 哪些不影响调度

requests / limits 的语义

在容器 spec 里:

  • requests.cpu / requests.memory:
    • 表示“调度保底需求”
    • 调度器主要依据它来做装箱
  • limits.cpu / limits.memory:
    • 表示“运行时上限”
    • 主要由节点上的 cgroup 在运行期约束

一句话:

  • requests 影响“能不能被调度到某个 Node”
  • limits 影响“调度后最多能吃多少资源”

可以再压缩成一句机制结论:

  1. requests 主要影响 Scheduler 的装箱与可调度性判断
  2. limits 主要影响节点上 cgroups 的运行时资源约束
  3. requests + limits 的组合会决定 Pod 的 QoS 类别
  4. QoS 会直接影响内存压力下的 Pod 驱逐优先级(通常 BestEffort 更早被驱逐, Guaranteed 更晚)

这些指标在 API Server 里意味着什么

从 API Server 角度看, resources.requests/limits 是 PodSpec 的声明式字段:

  1. API Server 持久化这些字段到 etcd
  2. 调度器 watch 到未调度 Pod 后读取这些字段做可行性判断
  3. kubelet/container runtime 在节点执行时依据这些字段配置 cgroup

也就是说 API Server 本身不做资源调度计算, 也不直接执行限流, 它是控制面的“事实来源”。

调度器到底看什么

调度器在资源维度最常看的并不是实时使用率, 而是“已分配 requests + 新 Pod requests 是否还能放下”。

简化为:

requestsexisting+requestsnewPodallocatablenode\sum requests_{existing} + requests_{newPod} \le allocatable_{node}

其中 allocatable 是 Node 可分配给 Pod 的资源(扣除了系统预留等)。

因此常见现象:

  1. Node 实际 CPU 看起来不高, 但新 Pod 仍调度失败
  2. 原因是 requests 已经把可分配额度“占满”

运行期影响: CPU 和内存不一样

  1. CPU limit:
  • 更像节流(throttling), 超出会被限速
  1. Memory limit:
  • 是硬上限, 超出可能触发 OOMKill

这也是为什么内存 limit 配置不当比 CPU 更容易造成进程被杀。

QoS 与驱逐

根据 requests/limits 组合, Pod 会落入不同 QoS:

  1. Guaranteed: 每个容器 requests=limits(且 CPU/Memory 都设置)
  2. Burstable: 至少有 request, 但不满足 Guaranteed
  3. BestEffort: 没有 requests/limits

当节点内存压力大时, 通常更容易先驱逐 QoS 低的 Pod, 常见顺序可近似理解为: BestEffort -> Burstable -> Guaranteed。

Spec Example

requests/limits + HPA Example
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: my-namespace
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: nginx
        resources:
          requests:
            cpu: "200m"
            memory: "256Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
  namespace: my-namespace
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60

Workflow

  1. 用户提交 Deployment, API Server 持久化 requests/limits
  2. 调度器按 requests 把 Pod 调度到可放置 Node
  3. kubelet 按 limits 配置 cgroup, 容器运行
  4. HPA 基于指标调整 replicas
  5. 新 Pod 继续走“按 requests 调度”流程

这里可以看到, requests/limits 同时影响了:

  1. 调度可行性(能否上某个 Node)
  2. 运行时行为(限速/OOM)
  3. 弹性伸缩效果(HPA 目标利用率与 requests 相关)

15. PodSecurityPolicy(PSP) 与 Pod 安全准入

先说结论:

  1. PodSecurityPolicy 是 Kubernetes 早期的“Pod 安全策略资源”
  2. 它用于约束 Pod 能否创建, 以及允许什么安全上下文
  3. 但 PSP 已经被废弃并移除(1.25+)
  4. 现在主流是 Pod Security Admission(PSA) + Namespace label

也就是说现在学习它, 重点要分成两层:

  1. 历史机制: PSP 是怎么工作的
  2. 现行机制: PSA 是怎么替代 PSP 的

PSP 当年是怎么做的

PSP 本质上是一组“Pod 安全规则模板”, 例如限制:

  1. 是否允许特权容器(privileged)
  2. 是否允许提权(allowPrivilegeEscalation)
  3. 能用哪些 volume 类型
  4. 是否必须非 root
  5. Linux capabilities 能加哪些

然后通过 RBAC 把“使用某个 PSP 的权限”授予某些用户/SA。

这意味着 PSP 不是只写一个资源就结束, 它的生效依赖两部分:

  1. API Server 开启 PSP admission
  2. 调用方有 RBAC 权限去 use 对应 PSP

历史 Spec Example(仅用于理解机制)

PodSecurityPolicy + RBAC Example(历史机制)
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted-psp
spec:
  privileged: false
  allowPrivilegeEscalation: false
  runAsUser:
    rule: MustRunAsNonRoot
  seLinux:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  volumes:
  - configMap
  - secret
  - persistentVolumeClaim
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: use-restricted-psp
rules:
- apiGroups: ["policy"]
  resources: ["podsecuritypolicies"]
  resourceNames: ["restricted-psp"]
  verbs: ["use"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: use-restricted-psp-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: use-restricted-psp
subjects:
- kind: ServiceAccount
  name: app-sa
  namespace: my-namespace

PSP 的 Workflow(历史机制)

当 Pod 创建请求进入 API Server 时, 典型流程是:

  1. API Server 完成认证和授权
  2. 进入 PSP admission 阶段
  3. admission 会根据调用者身份(用户/SA)去看他“可 use 哪些 PSP”
  • 这个权限来自 RBAC
  1. 在可用 PSP 集合里尝试匹配当前 Pod spec
  • 若能匹配, 则准入通过(必要时会做默认化)
  • 若都不匹配, 请求被拒绝
  1. 准入通过后 Pod 才会持久化到 etcd, 后续才谈调度

你可以看到, PSP 的本质是“准入门禁”, 不是调度策略。

现在推荐: Pod Security Admission(PSA)

PSA 是内置在 API Server 里的 admission 插件, 不再依赖 PSP 这种独立资源对象。

它的核心思想是: 把安全级别绑定到 Namespace, 然后在 Pod 创建/更新时自动校验。

三个安全级别(Pod Security Standards)

  1. privileged
  • 基本不做限制, 兼容性最高
  1. baseline
  • 阻止明显高风险配置(例如特权容器、部分危险能力)
  1. restricted
  • 更严格, 倾向最小权限(例如更强调 non-root、收紧能力与挂载)

三种执行模式

  1. enforce
  • 不满足策略直接拒绝请求
  1. warn
  • 请求放行, 但返回 warning 给调用方
  1. audit
  • 请求放行, 在审计日志打注记

Namespace 标签如何配置

PSA 通过 Namespace label 生效, 常见写法:

kubectl label ns my-namespace \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

也可以显式固定规则版本, 避免集群升级后策略语义变化:

kubectl label ns my-namespace \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30

PSA 的 Workflow(现行机制)

当 Pod 创建请求进入 API Server 时, 典型流程是:

  1. API Server 完成认证和 RBAC 授权
  2. 进入 admission 阶段, 触发 PSA 校验
  3. PSA 读取目标 Namespace 上的 enforce/warn/audit 标签
  4. 按对应级别检查 Pod spec 是否违反规则
  • 例如 privileged: trueallowPrivilegeEscalation: true、host namespace/hostPath 等
  1. 根据模式处理结果:
  • enforce: 拒绝并返回原因
  • warn: 放行但返回 warning
  • audit: 放行并写入审计注记
  1. 通过后对象才会被 API Server 持久化到 etcd, 后续才会进入调度和运行阶段

你可以把 PSA 理解成“Pod 落库前的安全闸门”, 和前文 PSP 一样属于准入层, 但实现更简单统一。

生产补充: PSA 之外的策略插件

PSA 更像 Kubernetes 内置的“通用基线”, 但很多生产规则需要更细粒度表达(例如按镜像仓库白名单、按团队例外、按标签条件组合校验)。

因此在生产上常见做法是: PSA 负责基础防线, 再配合专门策略引擎插件, 例如:

  1. Kyverno
  2. OPA Gatekeeper
  3. Kubewarden

这些插件通常同样工作在 admission 路径上, 负责补充更复杂的校验和治理策略。

Comments

Loading comments...

    Please complete the verification challenge.