Kubernetes 容器编排
Docker 解决「怎么打包」,K8s 解决「怎么在几百台机器上稳定地跑、扩、升、修」。核心思路是:声明期望状态,控制器负责收敛。
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 yaml、describe、logs 三个命令,能解决大半问题。
上下文与命名空间
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 | 生产暴露单个服务 |
| ExternalName | DNS 别名 | 指向外部服务,如 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
| 访问模式 | 缩写 | 含义 |
|---|---|---|
| ReadWriteOnce | RWO | 单节点读写,云盘最常见 |
| ReadOnlyMany | ROX | 多节点只读 |
| ReadWriteMany | RWX | 多节点读写,需 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)等主题。