← 全部文章

前端第一次学 Kubernetes:一条链看懂 Deployment、Pod、Kubelet 和 containerd

不默认懂容器。从 Node.js 代码怎样变成进程开始,逐步看懂 Image、Container、Pod、Node、Deployment 和 Service。

前端第一次学 Kubernetes:一条链看懂 Deployment、Pod、Kubelet 和 containerd
文章目录
  1. 先把 Kubernetes 拿掉:代码怎样变成正在运行的进程
  2. 容器多起来以后,才轮到 Kubernetes 出场
  3. 我们写的是 Deployment,真正被调度的是 Pod
  4. Pod 由谁创建,又会被放到哪台 Node
  5. Service 不保住 Pod,它保住访问入口
  6. 回到 Deployment YAML,每个字段都有了落点
  7. 不要只相信 apply 成功,用这几步验证链路
  8. 最后把整条链合起来
  9. 参考资料

前端开发第一次接触 Kubernetes,往往不是因为要去做运维。

更常见的情况是:项目突然要部署到 K8s,CI/CD 里开始出现 Deployment 和 Service,线上排查问题时又会看到 Pod、Node、Kubelet 和 containerd。

现在还有 AI。你说一句“帮我写一份 Kubernetes Deployment”,几秒钟后 YAML 就出来了。kubectl apply 一跑,服务可能真的能启动。

但到了 Pod 一直 Pending、容器反复 CrashLoopBackOff,或者 Service 始终访问不通时,只会改 YAML 就不够了。你得知道,这份声明最后是怎样变成某台机器上真正运行的进程的。

这篇文章不默认你懂容器,也不从 Kubernetes 的官方定义起步。我们就从前端开发最熟悉的代码和 Node.js 进程开始,只追一条链:一个 Node.js 服务,到底怎样在 Kubernetes 里跑起来?

先把 Kubernetes 拿掉:代码怎样变成正在运行的进程

假设我们有一个 Node.js API。它现在只是一组源码文件,不会自己变成对外提供服务的进程。

按传统部署方式,我们要先准备一台物理机或虚拟机,在上面安装正确版本的 Node.js,复制代码,执行 npm install,配置环境变量,最后用 node server.js 或类似命令启动进程。进程运行时,真正消耗的是这台机器的 CPU 和内存。

只部署一次时,这套做法没什么难理解的。但机器一多,环境差异就会出现:这台是 Node.js 20,那台还在用 Node.js 18;这台少了一个系统库,那台的配置又被人手动改过。

容器解决的,正是“每到一台机器都重新准备运行环境”的问题。

你可以先把 镜像(Image) 理解成一份可分发的运行模板。它不只包含业务代码,还会带上运行代码所需的 Node.js、系统库和安装好的依赖。这样,我们先构建一次镜像,再把同一份镜像发到不同机器上运行。

镜像本身不会执行代码。只有当 Docker 或其他容器工具用这份镜像启动进程时,才产生一个 容器(Container)。

所以,容器不是代码压缩包,也不是一台小型虚拟机。它首先是一个正在运行的进程,只是操作系统给它隔出了自己的文件系统、网络视图和资源边界。在常见的 Linux 容器中,它与同一台机器上的其他容器共享宿主机内核,并不会为每个容器再启动一套完整操作系统。

流程图

用一句话压缩就是:镜像是运行前的静态模板,容器是镜像启动后的隔离进程,机器则提供真正的 CPU 和内存。

到这里还没有 Kubernetes。你的 Node.js、Java 或 Go 服务,最后仍然是某台机器上的容器进程。

镜像不会自己变成进程。机器上必须有一个程序接收“用这份镜像启动容器”的请求,再负责停止容器、删除容器和查询运行状态。

这个程序就是 Container Runtime,容器运行时。初学时,可以先把它理解成“机器上真正管容器的程序”。在 Kubernetes 集群里,containerd 和 CRI-O 都是常见选择。

这里最好别再把 Docker 和 Container Runtime 当成同一个概念。Docker 是一整套容器工具与产品;Kubernetes 需要的,是一个能通过标准接口管理容器的运行时。

容器多起来以后,才轮到 Kubernetes 出场

如果只有一台机器、一个容器,手动执行一次 docker run 也能工作。真正的麻烦出现在规模变大以后:同一个 API 需要 3 个副本,公司有几十台机器,某个容器挂了要补上,发布新版本还不能一次全停。

这时需要的不是另一种容器,而是一个能在多台机器之间统一安排和管理容器的系统。Kubernetes 做的就是这件事。

不过,Kubernetes 不直接把一个裸 Container 当成最小管理单元。它安排和运行的最小计算单元是 Pod。

一个 Pod 可以包含一个或多个容器。大部分业务 Pod 只有一个主容器;只有当多个容器必须紧密协作时,才会把它们放进同一个 Pod。

同一 Pod 里的容器共享网络命名空间,使用同一个 Pod IP,可以通过 localhost 互相访问。它们也可以通过配置挂载同一个 Volume。所以 Pod 不是“容器文件夹”,而是一组需要共同运行的容器所在的边界。

无论 Pod 里有几个容器,它都必须落到某台真实机器上。Kubernetes 把加入集群、用来承载这些 Pod 的机器叫作 Node。一个 Node 上可以运行很多 Pod。

为了让 Pod 真正在 Node 上跑起来,每个 Node 上还有两个关键角色:

  • Kubelet:运行在 Node 上的 Agent。它获取分配给这台 Node 的 PodSpec,并确保其中描述的容器正在运行且健康。
  • Container Runtime:真正管理容器执行与生命周期的组件,例如 containerd。

两者通过 CRI(Container Runtime Interface,容器运行时接口) 通信。CRI 是一套 gRPC 接口规范,不是一个会自己启动容器的程序。

流程图

这张图里最容易看错的关系是:Kubelet 和 containerd 属于 Node,不在 Pod 里。

现在,一台 Node 已经具备了启动 Pod 的能力。但它还不知道应该启动几个 Pod,也不知道哪一个 Pod 应该由自己负责。要回答这两个问题,得回到我们平时写的 Deployment。

我们写的是 Deployment,真正被调度的是 Pod

假设 Node.js API 需要 3 个副本。你可以直接创建 3 个 Pod,但这样就得自己记住它们的数量,在 Pod 被删除后手动补上,发布新版本时再一个个替换。

因此,日常部署无状态服务时,我们通常不直接维护裸 Pod,而是创建 Deployment。Deployment 负责表达副本数和更新策略,它再通过 ReplicaSet 管理具体的 Pod。

  • Deployment 管理声明式更新,包括滚动更新和回滚等能力。
  • Deployment 通常创建并管理 ReplicaSet。
  • ReplicaSet 通过 Pod 模板创建或删除 Pod,使运行中的副本数接近你声明的数量。

假设目标是 3 个 Pod,某个 Pod 被删除后,ReplicaSet Controller 会看到当前副本数只有 2,再创建一个新 Pod。它不是把原来那个 Pod “修活”,而是用新对象恢复期望数量。

流程图

所以我们写 Deployment,日常排查却常常要落到 Pod 和 Container:Deployment 表达“我想要什么”,Pod 和 Container 则更接近“现在真的发生了什么”。

Pod 由谁创建,又会被放到哪台 Node

Deployment 已经声明了“我要 3 个 Pod”,Node 也已经具备启动 Pod 的能力。现在还差一个协调者:它要创建这些 Pod,再决定每个 Pod 去哪台 Node。

这个协调者就是 Control Plane,也就是集群的控制中枢。其中,API Server 接收 Kubernetes API 请求;一组 Controller 持续对比期望状态和当前状态;Scheduler 则为还没有分配 Node 的 Pod 选择合适机器。

现在再把 replicas: 3 的 Deployment 从头到尾走一遍:

流程图

这条链里同时有两种关系。资源管理关系是:

Deployment → ReplicaSet → Pod → Container

真正把 Pod 落到机器并启动容器的执行链路是:

Control Plane 创建与调度 Pod → Kubelet 执行本机期望状态 → containerd 启动容器

Deployment 本身不会钻进 Node 启动进程,Scheduler 也不会创建容器。每个角色只处理自己那一段,才把一份声明逐步变成了正在运行的进程。

Service 不保住 Pod,它保住访问入口

到这一步,3 个 Pod 已经跑起来了。但是每个 Pod 都有自己的 IP,而且 Pod 是短暂的:它可能被删除、重建,新 Pod 的名字和 IP 也会变。调用方不应该去追踪每一个 Pod IP。

假设这 3 个 Pod 现在的 IP 分别是 10.0.1.11、10.0.1.12 和 10.0.1.13。如果调用方把这些地址写死,只要某个 Pod 重建,原来的地址就可能失效。

Service 就是放在这些 Pod 前面的固定入口。比如集群里的其他服务只需要访问 my-api,不用记住后面每个 Pod 的 IP。

那么,Service 怎么知道哪些 Pod 属于 my-api?Deployment 创建 Pod 时,会给它们加上 app: my-api 标签;Service 里的 Label Selector 也写着 app: my-api。两边匹配上之后,这些 Pod 就成了 Service 的后端。

Kubernetes 还会用 EndpointSlice 保存一份“当前可用 Pod 名单”,里面记录这些 Pod 现在的 IP。Pod 被替换后,这份名单也会跟着更新。

当请求到达 Service 时,kube-proxy 或集群使用的网络插件,会根据这份名单把请求转发给其中一个 Pod。

流程图

可以把这段压缩成一句话:Service 提供固定入口,EndpointSlice 记录入口后面现在有哪些 Pod,网络组件负责把请求真正送过去。

这和 ReplicaSet 解决的不是同一个问题:ReplicaSet 负责维持 Pod 副本数,Service 则让调用方不用关心后面的 Pod 怎样替换。

另外,Service 不等于公网入口。默认的 ClusterIP 只在集群内可达。要把 HTTP/HTTPS 服务暴露到集群外,通常还会使用 LoadBalancer 类型的 Service、Ingress 或 Gateway API,具体取决于集群环境。

回到 Deployment YAML,每个字段都有了落点

我们已经从代码一路追到了机器上的容器,也知道 Service 怎样找到会变化的 Pod。现在回头看最开始的 YAML,它声明的其实就是这些关系:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-api
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
        - name: api
          image: my-api:1.0
          ports:
            - containerPort: 3000

它表达的是:用 my-api:1.0 镜像运行容器;每个 Pod 按这份模板创建;让 3 个符合 app: my-api 标签的 Pod 持续存在。

如果还要让集群内的其他服务访问它,可以增加一份 Service:

apiVersion: v1
kind: Service
metadata:
  name: my-api
spec:
  selector:
    app: my-api
  ports:
    - port: 80
      targetPort: 3000

这里最关键的连接不是文件顺序,而是 Service.spec.selector 与 Pod 上的 app: my-api 标签相互匹配。port: 80 是调用方访问 Service 的端口,targetPort: 3000 是请求最终到达 Pod 容器的端口。

不要只相信 apply 成功,用这几步验证链路

kubectl apply 返回成功,只能说明 API Server 接受了这份声明,不能证明容器已经健康,也不能证明 Service 已经有可用后端。

先看 Deployment 是否达到期望副本数:

kubectl get deployment my-api
kubectl rollout status deployment/my-api

再沿着管理关系往下看 ReplicaSet 和 Pod:

kubectl get rs
kubectl get pods -l app=my-api -o wide

如果 Pod 一直 Pending,先看事件,确认调度为什么没有完成:

kubectl describe pod <pod-name>

如果 Pod 已经被调度,但容器不断重启,再看容器日志与上一次退出信息:

kubectl logs <pod-name> -c api
kubectl logs <pod-name> -c api --previous

如果 Pod 正常,Service 却访问不通,先确认 Selector 是否真的选中了后端:

kubectl get service my-api
kubectl get endpointslice -l kubernetes.io/service-name=my-api

这组命令对应的并不是一份万能排障清单,而是前面的心智模型:先看期望状态,再看 Pod 被放到了哪里、容器有没有跑起来,最后看稳定入口是否指向了正确后端。

最后把整条链合起来

如果前面的名词有点多,可以分三层记。

第一层是程序怎样跑起来:

代码 + 运行时 + 依赖 → 镜像 → 容器进程 → 机器的 CPU 和内存

第二层是 Kubernetes 的资源管理关系:

Deployment → ReplicaSet → Pod → Container

第三层是真正把 Pod 变成容器进程的执行链路:

Control Plane → 为 Pod 选择 Node → Kubelet → CRI → Container Runtime → Container

Service 则站在另一侧,为会变化的 Pod 提供稳定的网络入口。

这套模型有意省略了 etcd、CNI、CSI、DNS、探针、资源限制和安全策略等细节,也没有覆盖有状态工作负载。它不是完整的 Kubernetes 架构图,但足以帮前端开发回答一个最常见的问题:一份 Deployment YAML 提交以后,究竟是谁创建了 Pod,谁选择了机器,谁启动了容器,流量又怎样找到它。

下一次 AI 再生成一份 YAML 时,不妨别急着直接 apply。先顺着这三层问一遍:镜像启动后会运行什么进程?Deployment 声明了什么期望状态?Pod 会怎样被创建和调度?Service 的 Selector 又会选中谁?能回答这些问题,才算真的开始会用 Kubernetes。


参考资料

  1. Kubernetes Documentation, Cluster Architecture
    https://kubernetes.io/docs/concepts/architecture/

  2. Kubernetes Documentation, Container Runtime Interface (CRI)
    https://kubernetes.io/docs/concepts/containers/cri/

  3. Kubernetes Documentation, Workloads
    https://kubernetes.io/docs/concepts/workloads/

  4. Kubernetes Documentation, Pods
    https://kubernetes.io/docs/concepts/workloads/pods/

  5. Kubernetes Documentation, Deployments
    https://kubernetes.io/docs/concepts/workloads/controllers/deployment/

  6. Kubernetes Documentation, ReplicaSet
    https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/

  7. Kubernetes Documentation, Service
    https://kubernetes.io/docs/concepts/services-networking/service/

  8. Kubernetes Documentation, Services, Load Balancing, and Networking
    https://kubernetes.io/docs/concepts/services-networking/