模块 04

Kubernetes 容器编排

Docker 解决「怎么打包」,K8s 解决「怎么在几百台机器上稳定地跑、扩、升、修」。核心思路是:声明期望状态,控制器负责收敛。

8 个章节 含完整 YAML 含排障手册

01集群架构与核心对象

记住一句话:K8s 是「声明式 + 控制器循环」的系统。

控制平面(Control Plane)

kube-apiserver:唯一入口,所有读写都经它鉴权。
etcd:集群唯一数据源,务必定期备份。
kube-scheduler:决定 Pod 落到哪个节点。
kube-controller-manager:各类控制器,负责把实际状态拉向期望状态。

工作节点(Node)

kubelet:节点代理,管理 Pod 生命周期与探针。
kube-proxy:实现 Service 的负载均衡转发。
容器运行时:containerd / CRI-O。
CNI 插件:Calico / Cilium / Flannel,负责 Pod 网络。

对象层级关系

Deployment            声明「我要 3 个副本、用哪个镜像」
   └── ReplicaSet     保证副本数,镜像变更时创建新 RS
          └── Pod     最小调度单位,可含多个容器(共享网络与存储)
                 └── Container

Service               给一组 Pod 提供固定虚拟 IP 与负载均衡
Ingress               七层入口,按域名/路径路由到 Service
ConfigMap / Secret    配置与密钥,与镜像解耦
PersistentVolume      持久化存储,生命周期独立于 Pod

Pod 的定位

Pod 是最小调度单位,而不是容器。同一 Pod 内的容器共享网络命名空间(可用 localhost 互访)和存储卷。多容器 Pod 的典型用法是 Sidecar(如日志采集、代理注入)。

💡

Pod 是一次性的:节点故障、驱逐、更新都会导致 Pod 被重建,IP 会变化。因此永远不要直接创建裸 Pod 来跑业务,交给 Deployment / StatefulSet / DaemonSet 管理。

本地实验环境

# 单机学习环境(三选一)
kind create cluster --name learn          # 用 Docker 跑 K8s,最轻量
minikube start --cpus=4 --memory=8192     # 跨平台,功能完整
k3d cluster create learn                  # k3s 版本,启动最快

kubectl cluster-info
kubectl get nodes -o wide
kubectl get componentstatuses             # 老版本可用

02kubectl 与 Pod

熟练使用 -o yamldescribelogs 三个命令,能解决大半问题。

上下文与命名空间

kubectl config get-contexts
kubectl config use-context kind-learn
kubectl config set-context --current --namespace=dev

kubectl create namespace dev
kubectl get ns

# 给常用命令加别名(写入 ~/.bashrc)
# alias k=kubectl
# alias kgp='kubectl get pods -o wide'
# source <(kubectl completion bash)

一个完整的 Pod 定义

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: dev
  labels:
    app: demo
spec:
  containers:
    - name: app
      image: myapp:1.0.0
      ports:
        - containerPort: 8080
      env:
        - name: TZ
          value: Asia/Shanghai
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password
      resources:
        requests:            # 调度依据:保证有多少
          cpu: 100m
          memory: 256Mi
        limits:              # 上限:超了会被限流或 OOMKill
          cpu: "1"
          memory: 1Gi
      volumeMounts:
        - name: config
          mountPath: /app/config
          readOnly: true
  volumes:
    - name: config
      configMap:
        name: app-config
  restartPolicy: Always      # Always / OnFailure / Never

声明式与命令式

# 声明式(推荐,可纳入 Git 管理)
kubectl apply -f pod.yaml
kubectl delete -f pod.yaml

# 命令式(调试、快速生成模板时好用)
kubectl run tmp --image=busybox:1.36 --restart=Never -- sleep 3600
kubectl create deployment web --image=nginx:1.25 --replicas=2 --dry-run=client -o yaml > web.yaml

# 只读查看(排障核心三件套)
kubectl get pods -o wide
kubectl describe pod demo-pod          # 看 Events,答案通常在这里
kubectl logs -f demo-pod
kubectl logs demo-pod --previous       # 看上一个崩溃实例的日志,非常重要

进入容器与临时调试

kubectl exec -it demo-pod -- sh
kubectl exec demo-pod -- env | sort

# 镜像没有 shell 时的调试方法:起一个共享进程命名空间的临时容器
kubectl debug -it demo-pod --image=busybox:1.36 --target=app -- sh

# 在节点上排查(需特权)
kubectl debug node/node01 -it --image=busybox:1.36

# 端口转发到本地
kubectl port-forward pod/demo-pod 8080:8080
⚠️

Pod 处于 Pending 时,kubectl logs 不会有任何输出,因为容器还没启动。此时必须看 describe 末尾的 Events。

03Deployment 与扩缩容

无状态服务的标准载体,也是用得最多的对象。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: dev
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1              # 更新时最多多出 1 个 Pod
      maxUnavailable: 0        # 保证始终有 3 个可用副本
  template:
    metadata:
      labels:
        app: web
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: web
          image: myapp:1.0.0     # 生产建议用 digest 锁定
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: web-config
          resources:
            requests: { cpu: 100m, memory: 256Mi }
            limits:   { cpu: "1",  memory: 1Gi }
          readinessProbe:
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /livez, port: 8080 }
            initialDelaySeconds: 30
            periodSeconds: 10
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 10"]   # 等待流量摘除后再退出

滚动更新与回滚

# 触发镜像更新
kubectl set image deployment/web web=myapp:1.0.1 -n dev

# 观察更新过程
kubectl rollout status deployment/web -n dev

# 暂停 / 恢复(配合 canary 使用)
kubectl rollout pause   deployment/web -n dev
kubectl rollout resume  deployment/web -n dev

# 查看历史与回滚
kubectl rollout history deployment/web -n dev
kubectl rollout undo deployment/web -n dev
kubectl rollout undo deployment/web --to-revision=2 -n dev

# 扩缩容
kubectl scale deployment/web --replicas=5 -n dev

# 基于 CPU 的自动伸缩
kubectl autoscale deployment/web --min=2 --max=10 --cpu-percent=70 -n dev
# HPA 手动定义,指标更丰富
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
  namespace: dev
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # 缩容冷却,避免抖动

其他工作负载类型

类型Pod 身份适用场景
Deployment无状态、可互换Web API、无状态服务
StatefulSet固定名称与稳定存储数据库、消息队列、集群类中间件
DaemonSet每个节点一个日志采集、监控 Agent、CNI
Job / CronJob运行到完成 / 定时数据迁移、批处理、定时报表
⚠️

Deployment 的 spec.selector 创建后不可修改,改了会报 field is immutable。需要换标签选择器时只能删除重建(或用 --cascade=orphan 保留旧 Pod)。

04Service 与 Ingress

Pod 的 IP 会变,Service 提供稳定入口;Ingress 提供集群外的七层入口。

四种 Service 类型

类型可访问范围典型用途
ClusterIP(默认)仅集群内服务间调用
NodePort集群外,通过节点 IP:30000-32767测试环境、临时暴露
LoadBalancer集群外,云厂商分配 ELB生产暴露单个服务
ExternalNameDNS 别名指向外部服务,如 RDS
apiVersion: v1
kind: Service
metadata:
  name: web-svc
  namespace: dev
spec:
  type: ClusterIP
  selector:
    app: web              # 靠标签匹配 Pod,不是靠名字
  ports:
    - name: http
      port: 80            # Service 端口
      targetPort: 8080    # 容器端口
      protocol: TCP

---
# 无头服务:供 StatefulSet 做 DNS 发现,返回所有 Pod IP
apiVersion: v1
kind: Service
metadata:
  name: db-headless
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
# 集群内 DNS 名称格式
# <service>.<namespace>.svc.cluster.local
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- \
  nslookup web-svc.dev.svc.cluster.local

# 临时端口转发验证
kubectl port-forward svc/web-svc 8080:80 -n dev

Ingress:七层入口

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  namespace: dev
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: 20m
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: web-svc
                port: { number: 80 }
          - path: /
            pathType: Prefix
            backend:
              service:
                name: front-svc
                port: { number: 80 }
💡

Service 与 Ingress 的分工:Service 是四层(TCP/UDP)负载均衡,工作在集群内部;Ingress 是七层(HTTP/HTTPS),按域名和路径把外部流量分发到不同 Service。

本机测试可用 curl -H "Host: app.example.com" http://<ingress-ip>/api,避免改 hosts。

05配置、密钥与存储

把配置从镜像里拿出来,是十二要素应用的基本要求。

ConfigMap

# 从字面量创建
kubectl create configmap web-config -n dev \
  --from-literal=LOG_LEVEL=info \
  --from-literal=FEATURE_FLAG=true

# 从文件创建(文件名成为 key)
kubectl create configmap nginx-conf -n dev --from-file=nginx.conf

kubectl get configmap web-config -n dev -o yaml
kubectl edit configmap web-config -n dev
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: dev
data:
  application.yml: |
    server:
      port: 8080
    logging:
      level:
        root: INFO
  LOG_LEVEL: "info"      # 注意:值必须是字符串,1.0 要加引号

Secret

# 从命令行创建(值会被 base64 编码,仅为编码不是加密)
kubectl create secret generic db-secret -n dev \
  --from-literal=username=appuser \
  --from-literal=password='S3cure!Pass'

# 从文件创建 TLS 证书
kubectl create secret tls app-tls -n dev --cert=tls.crt --key=tls.key

# 查看(-o yaml 会显示 base64;decode 才能看到明文)
kubectl get secret db-secret -n dev -o jsonpath='{.data.password}' | base64 -d
🚫

Secret 默认只做 base64 编码,不是加密。任何能读 etcd 或该命名空间 Secret 的人都能拿到明文。生产必须:启用 etcd 静态加密、用 RBAC 收敛权限、接入 Vault / External Secrets 等外部密钥管理。

存储:PV / PVC / StorageClass

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
  namespace: dev
spec:
  accessModes: [ReadWriteOnce]      # RWO / ROX / RWX
  storageClassName: standard
  resources:
    requests:
      storage: 10Gi
# 在 Pod/Deployment 中挂载
spec:
  containers:
    - name: app
      volumeMounts:
        - name: data
          mountPath: /app/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data-pvc
访问模式缩写含义
ReadWriteOnceRWO单节点读写,云盘最常见
ReadOnlyManyROX多节点只读
ReadWriteManyRWX多节点读写,需 NFS/CephFS 等支持
⚠️

PVC 的回收策略默认是 Delete:删除 PVC 会连带删除底层存储卷。数据库等关键数据请显式使用 Retain 的 StorageClass,并做独立备份。

06探针、资源与调度

决定「K8s 如何判断你的服务健康」和「Pod 落在哪台机器」。

三种探针的职责

探针检查什么失败后果
readinessProbe是否准备好接收流量从 Service Endpoints 摘除,不重启容器
livenessProbe进程是否还活着重启容器(可能反复重启)
startupProbe慢启动应用是否启动完成未通过前不执行其他探针,避免误杀
# 慢启动的 Java 应用:用 startupProbe 兜住
startupProbe:
  httpGet: { path: /actuator/health/readiness, port: 8080 }
  failureThreshold: 30          # 30 × 5s = 最多容忍 150s 启动
  periodSeconds: 5
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 8080 }
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 8080 }
  periodSeconds: 10
  failureThreshold: 3
🚫

最常见的配置事故:liveness 探针依赖数据库或下游服务。下游一抖动,所有 Pod 同时被判定不健康并重启,形成雪崩。

原则:liveness 只检查进程自身(如 /livez),readiness 才检查依赖可用性。

requests 与 limits

字段作用配置建议
requests.cpu调度依据,保证可用份额压测得到的稳态 CPU,如 100m ~ 500m
limits.cpu上限,超过被节流(不会杀)requests 的 2~4 倍,或直接不设
requests.memory调度依据接近实际常驻内存
limits.memory上限,超过 OOMKill必须设置,通常 = requests × 1.2~2
# QoS 等级
# Guaranteed:requests == limits,最优先保留,最不易被驱逐
# Burstable :requests < limits,常见选择
# BestEffort:完全不设,最先被驱逐 —— 生产禁止

# 命名空间级配额
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"

---
apiVersion: v1
kind: LimitRange
metadata:
  name: dev-defaults
  namespace: dev
spec:
  limits:
    - default:            # 未设置 limits 时的默认值
        cpu: "500m"
        memory: 512Mi
      defaultRequest:     # 未设置 requests 时的默认值
        cpu: 100m
        memory: 128Mi
      type: Container

调度控制

spec:
  # 硬性要求:只在满足条件的节点运行
  nodeSelector:
    disktype: ssd

  # 亲和性:更灵活的表达
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: kubernetes.io/arch
                operator: In
                values: ["amd64"]
    podAntiAffinity:               # 同一服务的副本尽量不挤在一台机器
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels: { app: web }
            topologyKey: kubernetes.io/hostname

  # 容忍污点:专有节点(如 GPU、只给某业务用)
  tolerations:
    - key: dedicated
      operator: Equal
      value: web
      effect: NoSchedule

  topologySpreadConstraints:       # 均匀打散到多个可用区
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: ScheduleAnyway
      labelSelector:
        matchLabels: { app: web }
# 给节点打标签与污点
kubectl label node node01 disktype=ssd
kubectl taint nodes node01 dedicated=web:NoSchedule

# 驱逐节点上所有 Pod(维护前操作)
kubectl drain node01 --ignore-daemonsets --delete-emptydir-data
kubectl uncordon node01            # 恢复可调度

07发布策略与故障排查

发布要能控制风险,排障要有固定顺序。

三种发布策略

策略原理优缺点
滚动更新逐步替换 Pod,Deployment 默认资源占用小;回滚快;无法精细控制流量比例
蓝绿发布新旧两套环境,切 Service 标签切换瞬时、回滚简单;需双倍资源
灰度 / 金丝雀按比例或流量特征导入新版本风险最小;需 Ingress 或服务网格支持
# 蓝绿:切换 Service 的 selector 即可完成发布
apiVersion: v1
kind: Service
metadata:
  name: web-svc
spec:
  selector:
    app: web
    version: blue      # 改成 green 就完成切换,回滚同理

---
# 金丝雀:两个 Deployment 共享同一 Service 标签
# 稳定版 9 副本 + 金丝雀 1 副本 ≈ 10% 流量
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-canary
spec:
  replicas: 1
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web, track: canary }
    spec:
      containers:
        - name: web
          image: myapp:1.1.0-rc1

Pod 状态对照表

状态含义排查方向
Pending未被调度或镜像未拉完资源不足、节点选择器不匹配、污点未容忍
ContainerCreating正在创建容器镜像拉取慢、卷挂载失败
ImagePullBackOff镜像拉不下来镜像名/标签错误、私有仓库缺 imagePullSecret
CrashLoopBackOff反复崩溃重启启动报错、配置缺失、探针过严
Running 但未 Ready容器在跑但不健康readiness 探针失败,看日志与探针路径
Terminating 卡住长时间无法删除finalizer、preStop 阻塞、节点失联
OOMKilled内存超限被杀调大 limits 或排查内存泄漏
Evicted被节点驱逐节点磁盘/内存压力,见节点 Conditions

标准排查流程

# 1. 先看整体状态
kubectl get pods -n dev -o wide
kubectl get events -n dev --sort-by=.lastTimestamp | tail -30

# 2. 定位到具体 Pod,看 Events
kubectl describe pod <pod> -n dev

# 3. 看日志(当前实例 + 上一个崩溃实例)
kubectl logs <pod> -n dev --tail=200
kubectl logs <pod> -n dev --previous

# 4. 看容器退出码与原因
kubectl get pod <pod> -n dev -o jsonpath='{.status.containerStatuses[*].lastState}'

# 5. 深入容器排查(镜像无 shell 时)
kubectl debug -it <pod> --image=busybox:1.36 --target=app -n dev -- sh

# 6. 怀疑资源问题,看节点与配额
kubectl describe node <node>
kubectl top pods -n dev
kubectl describe resourcequota -n dev

# 7. 怀疑服务不通,跟着链路走
kubectl get endpoints web-svc -n dev          # 是否为空?
kubectl get ingress -n dev
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- \
  wget -qO- http://web-svc.dev.svc.cluster.local/healthz

高频问题速查

Service 通不了

先看 Endpoints 是否为空。为空说明 selector 匹配不到 Ready 的 Pod,通常是标签写错或 readiness 未通过。

Pod 反复重启

--previous 日志与退出码:1 是应用异常,137 是 OOM 或强杀,143 是收到 SIGTERM。

服务偶发 5xx

多半是滚动更新时连接未摘除。加上 preStop sleep 与合理的 readiness 探针可消除。

配置改了不生效

ConfigMap 以 volume 挂载时会自动更新(有延迟),以 env 注入则不会。用 kubectl rollout restart 触发重建。

Pod 一直 Pending

describe 里会有 0/3 nodes are available: insufficient cpu/memory 之类的明确原因。

磁盘被打满

节点 DiskPressure 会驱逐 Pod。检查镜像存储与容器日志,配置日志轮转与 imageGCHighThresholdPercent

# 让配置变更生效(滚动重启)
kubectl rollout restart deployment/web -n dev

# 查看对象最终 YAML(排查实际生效了什么)
kubectl get deploy web -n dev -o yaml

# 强制删除卡在 Terminating 的 Pod(最后手段)
kubectl delete pod <pod> -n dev --grace-period=0 --force
⚠️

生产集群务必开启 RBAC 最小权限、审计日志与 etcd 定期备份。一次 kubectl delete ns prod 的事故,恢复成本远高于所有运维投入。

08自测清单

勾选表示已在集群里实际操作过。

  • 说出控制平面四个组件的职责,以及 etcd 为什么最需要备份
  • 解释 Pod 与容器的区别,并说明何时需要多容器 Pod
  • 用 kind 或 minikube 建集群并部署一个 Service 与 Deployment
  • kubectl describe 的 Events 定位一次 Pending
  • 完成一次滚动更新并回滚到上一版本
  • 配置 HPA,用压测触发自动扩容
  • 解释 ClusterIP / NodePort / LoadBalancer 的差别
  • 用 Ingress 按路径把流量分发到两个 Service
  • 用 ConfigMap 与 Secret 注入配置,并说明二者安全性差异
  • 为 StatefulSet 配置 PVC 并验证数据在 Pod 重建后仍存在
  • 说出三种探针的区别,并说明 liveness 不该依赖下游的原因
  • 为一 Pod 设置 requests/limits,并用 kubectl top 验证
  • 读懂 CrashLoopBackOff 并借助 --previous 日志定位根因
  • 说出 Service 的 Endpoints 为空有哪些可能原因
🎉

到这里四个模块已经打通:Linux 提供底座,Java 提供应用,Docker 提供交付格式,K8s 提供运行环境。接下来可以按需扩展监控(Prometheus + Grafana)、日志(EFK/Loki)与 CI/CD(GitLab CI / Argo CD)等主题。