Day57-K8s核心概念速通:Pod、Deployment、Service与Ingress

Pod/Deployment/Service/Ingress------后端上生产的四个分水岭

容器化部署容易,但很多人只照着 YAML 复制粘贴、没系统学过 K8s,出故障时连应用跑在哪个 Pod、日志去哪捞、重启容器和发布新版本有什么区别都搞不清。云原生这关,你不一定要成为 K8s 专家,但 Pod、Deployment、Service、Ingress 这四个对象,是从"会写 Java"到"能扛住生产"的分水岭------搞不懂它们,出问题时你连战场在哪都不知道。

本文用四个核心对象,把一个 Spring Boot 应用从"裸进程"一步步变成"生产可用":先讲清声明式 API 与控制循环这一底层思维,再逐个拆解 Pod(最小调度单元 + 三探针)、Deployment(滚动更新与回滚)、Service(稳定访问入口与四种类型)、Ingress(七层路由),最后串成完整落地链路并给出排障三板斧。读完能独立把 Java 应用部署上 K8s 并定位常见故障。


一、先搞懂 K8s 的底层思维:声明式 API

在讲任何对象之前,必须先把"声明式"这三个字嚼烂。这是 K8s 和传统运维最大的思维分野。

传统运维是"命令式":你告诉系统每一步怎么走。

bash 复制代码
# 命令式:一步步下指令
docker run -d -p 8080:8080 my-app:v1   # 先启动一个容器
docker run -d -p 8080:8080 my-app:v1   # 再加一个容器(手动扩容)
# 挂了怎么办?你自己写脚本监控、自己重启

K8s 是"声明式":你只告诉系统"我想要什么状态",剩下交给它。

bash 复制代码
# 声明式:描述期望状态
kubectl apply -f deployment.yaml
# deployment.yaml 里写了:我要 3 个副本,镜像用 my-app:v1
# K8s 会持续"对账":实际跑 2 个?自动拉起 1 个。跑成 v2 了?自动回滚到 v1。

这两者的区别,一句话就能说透:命令式是你盯着每一步,声明式是你定一个目标,系统自己兜底。

K8s 的核心是一个控制循环(Reconciliation Loop),也叫"调谐循环"。它的工作方式极其简单,但威力巨大:

bash 复制代码
循环 {
    观察(Observe):实际状态是什么?
    对比(Diff)   :和期望状态差多少?
    行动(Act)    :做点什么,让实际状态逼近期望状态
}

你提交给 K8s 的任何 YAML,本质都是在声明一个"期望状态"。K8s 的各个控制器(Controller)会不停地把这个期望状态变成现实。理解了这一点,你就能理解 K8s 里几乎所有让你困惑的行为------比如为什么删掉的 Pod 会自动"复活",为什么改了副本数不用手动操作,为什么服务挂了能被拉起来。

划重点:kubectl apply 是声明式的,kubectl create 是命令式的。生产环境一律用 apply,因为它幂等,重复执行不会报错,天然适合写进 CI/CD 流水线。


二、Pod:最小的调度单元,也是容器的"家"

先纠正一个 90% 新手都会犯的认知错误:K8s 里最小的部署单位不是容器,是 Pod。

2.1 Pod 是什么

Pod 是一个或多个容器的集合,这些容器共享网络命名空间和存储卷,永远被调度到同一台机器上。你可以把 Pod 理解成"一组关系紧密、必须同生共死的容器"。

大多数情况下,一个 Pod 只有一个主容器(比如你的 Spring Boot 应用)。什么时候会放多个容器?典型场景是 Sidecar 模式------主容器旁边挂一个辅助容器。比如你的应用容器旁边挂一个"日志采集容器",或者挂一个"AI 推理的模型下载预热容器"。

下面是一个最简 Pod 定义:

XML 复制代码
# pod.yaml ------ 一个最简的 Pod(注意:生产环境不直接裸用 Pod,后面会说为什么)
apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
  labels:                 # 标签:K8s 关联对象的核心手段
    app: my-app
spec:
  containers:
    - name: app
      image: my-registry/my-app:v1.0.0   # 版本号务必写死,禁止用 latest
      ports:
        - containerPort: 8080
      resources:           # 不写资源限制是生产事故的头号元凶
        requests:          # 调度时的"保底"资源
          memory: "256Mi"
          cpu: "250m"
        limits:            # 运行时的"天花板"资源
          memory: "512Mi"
          cpu: "500m"

2.2 Pod 的生命周期

Pod 从创建到销毁,会经历一个明确的状态机,这是你排障时必须能一眼看懂的东西:

四个核心状态,背下来:

状态 含义 你的关注点
Pending 已创建,但还没调度或容器没起来 看是资源不足,还是镜像拉不下来
Running 至少一个容器在运行 不等于"健康",得配合探针判断
Succeeded 所有容器正常退出 通常是 Job/定时任务
Failed 容器异常退出 查容器日志和退出码

这里有个大坑Running ≠ 服务可用。容器进程活着,不代表它已经能对外服务(比如 Spring Boot 可能还在启动、数据库还没连上)。这就是为什么 K8s 设计了"探针(Probe)"。

2.3 三种探针:健康的真正判官

XML 复制代码
# deployment.yaml 的 Pod 模板片段
spec:
  containers:
    - name: app
      image: my-registry/my-app:v1.0.0
      startupProbe:          # 启动探针:给慢启动的 JVM 留足时间
        httpGet:
          path: /actuator/health
          port: 8080
        failureThreshold: 30   # 最多等 30 * 10s = 300秒
        periodSeconds: 10
      livenessProbe:         # 存活探针:进程"死没死"
        httpGet:
          path: /actuator/health
          port: 8080
        initialDelaySeconds: 0
        periodSeconds: 10
      readinessProbe:        # 就绪探针:能不能"接客"
        httpGet:
          path: /actuator/health
          port: 8080
        periodSeconds: 5

三者的分工一定要分清,搞混了会引发生产事故:

  • startupProbe(启动探针):只判断"启动完成没有"。JVM 启动慢,尤其是配了 AI SDK 加载模型的 Spring Boot,可能几十秒才起来,这时 livenessProbe 会误判"死了"而反复重启容器。startupProbe 就是用来给慢启动兜底的。

  • livenessProbe(存活探针) :判断"进程是否已死"。失败就重启容器------注意是重启容器,不是重启 Pod

  • readinessProbe(就绪探针) :判断"能不能接流量"。失败就从 Service 的负载均衡里摘掉,但不重启容器

一句话记忆:startup 管"起没起来",liveness 管"死没死",readiness 管"能不能用"。


三、Deployment:为什么不能裸用 Pod

前面我留了个伏笔------"生产环境不要直接裸用 Pod"。原因很简单:

  1. Pod 一旦挂了,不会自动恢复;

  2. 你想扩容到 3 个副本,得手动创建 3 次;

  3. 你想滚动升级版本,得手动一个个替换。

Deployment 就是来解决这三件事的。它内部管理一个 ReplicaSet(副本集),ReplicaSet 再管理一组 Pod,形成一条"控制链":

XML 复制代码
Deployment(声明期望副本数、版本、更新策略)
    └── ReplicaSet(保证"当前版本"的 Pod 数量达标)
            └── Pod(真正干活的最小单元)

一个生产级 Deployment 长这样:

XML 复制代码
# deployment.yaml ------ Spring Boot 应用的生产级部署(依赖:JDK 17 + Spring Boot 3.x)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-order-service
  labels:
    app: ai-order-service
spec:
  replicas: 3                 # 期望副本数,改这个数字就能扩缩容
  strategy:                   # 滚动更新策略
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0       # 更新期间最多允许 0 个 Pod 不可用 → 零停机
      maxSurge: 1             # 更新期间最多多出 1 个 Pod
  selector:                   # 用来"找到"属于它的 Pod
    matchLabels:
      app: ai-order-service
  template:                   # Pod 模板,定义了 Pod 长什么样
    metadata:
      labels:
        app: ai-order-service
    spec:
      containers:
        - name: app
          image: my-registry/ai-order-service:v2.3.0
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
          resources:
            requests: { memory: "512Mi", cpu: "500m" }
            limits:   { memory: "1Gi",   cpu: "1000m" }
          readinessProbe:
            httpGet: { path: /actuator/health, port: 8080 }
            periodSeconds: 5

Deployment 的三个杀手级能力,配合 kubectl 命令就是:

bash 复制代码
# 1. 扩缩容:一条命令,K8s 自己搞定
kubectl scale deployment ai-order-service --replicas=5
​
# 2. 滚动更新:换镜像,K8s 逐个替换旧 Pod(零停机)
kubectl set image deployment/ai-order-service app=my-registry/ai-order-service:v2.4.0
kubectl rollout status deployment/ai-order-service   # 看更新进度
​
# 3. 一键回滚:版本出问题,退回到上一个版本
kubectl rollout undo deployment/ai-order-service

maxUnavailable: 0 配合 maxSurge: 1 这个组合,能保证更新过程中始终有 3 个 Pod 在对外服务------这就是"滚动更新零停机"的原理,值得你记住。


四、Service:Pod 会死,但服务地址不能变

现在你有了 3 个 Pod,但问题来了:Pod 的 IP 是随机分配且会变的。Pod 一旦重启,IP 就换了,那你的前端、你的其他微服务,怎么稳定地找到它?

Service 解决的就是"稳定访问入口"的问题

Service 会给一组 Pod 分配一个稳定的虚拟 IP(ClusterIP)和 DNS 名称 ,然后通过 selector 找到它要代理的 Pod,把流量转发过去。Pod 死了重启,IP 变了,但只要 labels 没变,Service 就永远能找到它。

Service 有四种类型,这是面试和实战的双料高频考点:

类型 访问范围 典型场景 一句话
ClusterIP 集群内部 微服务之间互调 默认类型,内部专用
NodePort 集群外部(节点 IP:端口) 临时暴露、调试 在 ClusterIP 基础上,在每个节点开个端口
LoadBalancer 集群外部(云厂商 LB) 生产对外服务 在 NodePort 基础上,自动创建云负载均衡器
ExternalName 集群外部(DNS 别名) 访问集群外的服务 就是个 CNAME,不代理 Pod

四种类型是层层递进的关系,看这个图就明白了:

bash 复制代码
ClusterIP     ──→  仅集群内部可达
   │ 叠加"在每个节点开一个端口"
   ▼
NodePort      ──→  外部可通过 <节点IP>:<端口> 访问
   │ 叠加"云厂商自动创建负载均衡器"
   ▼
LoadBalancer  ──→  外部通过云 LB 的固定 IP 访问(生产首选)
   
ExternalName  ──→  独立的,仅做 DNS 别名映射

下面是一个 ClusterIP 型 Service,配合上面的 Deployment 使用:

XML 复制代码
# service.yaml ------ 为 ai-order-service 提供一个稳定的集群内部地址
apiVersion: v1
kind: Service
metadata:
  name: ai-order-service
spec:
  type: ClusterIP        # 默认类型,集群内部访问
  selector:              # 通过 label 匹配到对应的 Pod
    app: ai-order-service
  ports:
    - name: http
      port: 8080          # Service 对外暴露的端口
      targetPort: 8080    # Pod 上实际监听的端口
      protocol: TCP

部署完成后,集群内其他服务可以直接用 http://ai-order-service:8080 访问它,不用关心背后是 3 个还是 30 个 Pod,也不用关心它们的 IP 是什么。这就是 Service 的价值:把"访问入口"和"具体实例"彻底解耦。

实战提醒:selector 是 Service 找到 Pod 的唯一桥梁,selector 里的 label 必须和 Pod 模板里的 label 完全一致,错一个字母流量就断,还不会报错。


五、Ingress:七层路由,一个入口管所有服务

Service 解决了"稳定访问",但还差最后一公里。

假设你有三个服务:订单服务、用户服务、还有前几周我们做的 AI 智能客服服务 。如果每个服务都用 NodePort 暴露,那你要记三个端口号,前端还要处理一堆 IP。更重要的是,NodePort 和 LoadBalancer 都是四层(TCP)的,干不了"按域名/路径路由"这种七层的事

Ingress 就是来解决七层路由的 。它让你用一个统一入口,根据域名和路径,把请求分发到不同的 Service:

XML 复制代码
用户请求 https://api.yourdomain.com
        │
        ▼
   Ingress Controller(如 nginx-ingress)
        │  按规则匹配
        ├── /order/**  ──→  ai-order-service (Service)
        ├── /user/**   ──→  user-service (Service)
        └── /ai/**     ──→  ai-chat-service (Service)

一个 Ingress 规则长这样:

XML 复制代码
# ingress.yaml ------ 按路径路由到不同服务
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /   # 重写规则,常见坑
spec:
  ingressClassName: nginx     # 必须指定 Ingress Controller
  rules:
    - host: api.yourdomain.com
      http:
        paths:
          - path: /order
            pathType: Prefix
            backend:
              service:
                name: ai-order-service
                port: { number: 8080 }
          - path: /ai
            pathType: Prefix
            backend:
              service:
                name: ai-chat-service
                port: { number: 8080 }

一个必须记住的坑 :Ingress 只是一纸"路由规则",它本身不干活。真正转发流量的是 Ingress Controller(比如 nginx-ingress、Traefik),这个是需要你额外部署的组件。很多新手写完 Ingress 发现不生效,就是忘了装 Controller。


六、串起来:一个 Spring Boot 应用的完整落地

把上面四件套串起来,一个 Java 应用从裸进程到生产可用的完整路径就是:

复制代码

部署顺序也很清晰,照着敲就行:

到这里,你的 Java 应用就有了:自动恢复(Deployment)+ 稳定访问(Service)+ 统一入口(Ingress)+ 健康自检(Probe)。这就是云原生时代一个后端工程师的"及格线"配置。


实战建议:

1. 先懂"声明式",再背命令。 K8s 的命令有几百条,背不完也没必要。但只要你把"声明式 + 控制循环"这个底层思维吃透,所有的对象、控制器、行为都能自己推导出来。看任何 K8s 文档,先问一句"这是在声明什么期望状态"。

2. 生产环境的三条铁律:写死版本号、配好资源限制、配好就绪探针。 我见过太多线上事故,根源就是这三条没做好:镜像用了 latest 导致不可复现;没写 limits 导致一个内存泄漏的 Pod 拖垮整台节点;没配 readinessProbe 导致流量打进还没就绪的 Pod 全部报错。这三条不做到,别谈"上生产"。

3. 排障三板斧:describe 看事件,logs 看日志,get events 看历史。 Pod 起不来,别瞎猜。按顺序来:

bash 复制代码
kubectl describe pod <pod名>    # 看 Events,会告诉你"为什么":镜像拉不下?资源不足?探针失败?
kubectl logs <pod名> -f         # 看容器日志,找应用层面的报错
kubectl get events --sort-by=.metadata.creationTimestamp | tail -20  # 看集群最近发生了什么

90% 的问题,这三条命令走一遍就能定位。


结尾

K8s 这玩意,概念多、术语绕,但它骨子里就一句话:你声明一个目标,它负责把目标变成现实。 你作为后端工程师要做的,不是记住所有命令,而是把 Pod、Deployment、Service、Ingress 这四个对象的职责边界刻进脑子------Pod 是干活的,Deployment 是管人的,Service 是开门的,Ingress 是引路的。

下一篇,我们把上一周搭的 Spring Boot 微服务真正搬进 K8s,讲ConfigMap + Secret 配置管理,以及基于 QPS 的弹性伸缩(HPA)------让你的服务不仅能跑,还能"自己扩容"。咱们不见不散。

相关推荐
lxw20230271162 小时前
k8s安装部署(利用ansible)
linux·kubernetes·ansible
.柒宇.3 小时前
使用 cri-docker 搭建 Kubernetes 集群完整教程(v1.36.3)
docker·容器·kubernetes·k8s集群安装
starzy19903 小时前
K8s 集群容器管理工具选型与实战指南
云原生·容器
养海绵宝宝的小蜗3 小时前
K8S总结
云原生·容器·kubernetes
Yiiz.4 小时前
Kubernetes部署
云原生·容器·kubernetes
qizhideyu4 小时前
kubernetes
云原生·容器·kubernetes
Csxyzj4 小时前
kubernetes集群部署方法
java·linux·kubernetes
wish3664 小时前
K8S 免镜像部署:通过 API 上传文件动态创建服务(以 Ollama 为例)
人工智能·云原生·容器·kubernetes·local llm
小小克5 小时前
k8s集群部署的方法
云原生·容器·kubernetes