Kubernetes 部署最佳实践:从 YAML 到灰度发布
大家好,我是云萱。
Kubernetes 现在已经是 DevOps 的标配了,但很多人对 K8s 的理解停留在”会写 Deployment YAML”这个层面。一到面试被问”Pod 一直 Pending 怎么排查”、“HPA 怎么基于自定义指标扩缩容”、“灰度发布怎么实现”就卡住了。
这篇文章把 K8s 部署相关的知识系统梳理一遍——从核心资源配置,到灰度发布自动化,再到常见故障排查。尽量做到”拿来就能用”。
一、核心资源清单详解
一个完整的 K8s 应用部署,通常包含这些资源:ConfigMap、Secret、Deployment、Service、Ingress、HPA、PDB、ServiceMonitor。
1. Deployment:部署的核心
Deployment 是最常用的资源,但很多人只配置了 replicas 和 image,忽略了很多重要配置。
一个生产可用的 Deployment 应该包含:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
labels:
app: my-app
spec:
replicas: 3
revisionHistoryLimit: 10 # 保留多少个历史版本(回滚用)
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # 滚动更新时最多多出多少 Pod
maxUnavailable: 25% # 滚动更新时最多不可用多少 Pod
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
# 亲和性:Pod 尽量打散到不同节点
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["my-app"]
topologyKey: kubernetes.io/hostname
# 资源限制:一定要设!防止资源争抢
containers:
- name: my-app
image: registry.example.com/my-app:v1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
# 环境变量(从 ConfigMap 和 Secret 读取)
envFrom:
- configMapRef:
name: my-app-config
- secretRef:
name: my-app-secrets
# 健康检查:三个探针缺一不可
startupProbe: # 启动探针:应用启动慢的时候用
httpGet:
path: /health/startup
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe: # 存活探针:应用挂了自动重启
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe: # 就绪探针:准备好了才接入流量
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# 生命周期钩子
lifecycle:
preStop: # 停止前等待(优雅关闭)
exec:
command: ["/bin/sh", "-c", "sleep 30"]
# 安全上下文:以非 root 用户运行
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
# 安全上下文(Pod 级别)
securityContext:
fsGroup: 1000
# 重启策略
restartPolicy: Always
几个容易忽略但很重要的点:
startupProbe:应用启动慢的话,只用 livenessProbe 会导致启动期间被反复杀掉。startupProbe 专门用来解决这个问题——启动成功后再切到 livenessProbe。podAntiAffinity:让 Pod 尽量分散在不同节点上,避免一个节点挂了全挂。securityContext:生产环境必须设,最小权限原则。preStop:配合 terminationGracePeriodSeconds 做优雅关闭,避免流量还没切完 Pod 就被杀了。
2. Service + Ingress
# Service
apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: production
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/limit-rps: "100" # 限流
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
tls:
- hosts:
- api.example.com
secretName: my-app-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
3. HPA(水平自动扩缩容)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 3
maxReplicas: 10
metrics:
# CPU 指标(最常用)
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# 内存指标
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# 自定义指标(比如 QPS,需要 Prometheus Adapter)
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: 500
# 扩缩容行为(更精细的控制)
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容前观察多久
policies:
- type: Percent
value: 50 # 每次最多扩 50%
periodSeconds: 60
- type: Pods
value: 2 # 每次至少加 2 个
periodSeconds: 60
selectPolicy: Max # 两个策略选扩容最多的
scaleDown:
stabilizationWindowSeconds: 300 # 缩容前观察 5 分钟(防止抖动)
policies:
- type: Percent
value: 25 # 每次最多缩 25%
periodSeconds: 60
面试常问:HPA 怎么基于自定义指标扩容?
需要部署 Prometheus Adapter,把 Prometheus 指标转换成 K8s 自定义指标 API(custom.metrics.k8s.io),然后 HPA 就可以引用了。常用的指标有 QPS、队列长度、业务指标等。
4. PDB(Pod 中断预算)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app
namespace: production
spec:
minAvailable: 2 # 至少保证 2 个 Pod 可用
# 或者用 maxUnavailable: 1 # 最多允许 1 个 Pod 不可用
selector:
matchLabels:
app: my-app
PDB 的作用是保证在节点维护、集群升级等自愿性中断时,应用不会全挂。生产环境很重要。
二、灰度发布怎么实现
K8s 原生的滚动更新其实是最基础的灰度,但粒度很粗(只有 Pod 数量比例)。实际生产中通常需要更精细的控制。
常见灰度方案对比
| 方案 | 流量控制粒度 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 滚动更新 | 粗(Pod 数量比例) | 低 | 简单场景 |
| 双 Deployment + Service | 中(Pod 数量比例) | 低 | 基础灰度 |
| Ingress Nginx | 细(权重/Header/Cookie) | 中 | 大多数场景 |
| Istio / Service Mesh | 极细(百分比、用户、地域) | 高 | 大规模微服务 |
| Argo Rollouts | 细 + 自动化 | 中高 | 渐进式交付 |
方案一:双 Deployment + Service(最朴素的方式)
原理很简单:同一个 Service 下挂两个 Deployment(主版本 + 灰度版本),通过调整副本数控制流量比例。
Service my-app (selector: app=my-app)
├── Deployment my-app-main (10 replicas, v1.0) → 90% 流量
└── Deployment my-app-canary (1 replica, v2.0) → 10% 流量
优点:简单,不需要额外组件。 缺点:流量比例不精确(靠 Pod 数量近似),没有自动回滚。
方案二:Ingress Nginx 权重
通过 nginx.ingress.kubernetes.io/canary 注解实现:
# 主版本 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-main
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
backend:
service:
name: my-app-main
port:
number: 80
# 灰度版本 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20" # 20% 流量到灰度
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
backend:
service:
name: my-app-canary
port:
number: 80
还支持基于 Header 和 Cookie 的灰度(让特定用户走灰度版本):
# 特定 Header 走灰度
nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
nginx.ingress.kubernetes.io/canary-by-header-value: "true"
方案三:Python 自动化灰度发布
我自己写过一个 Python 版本的灰度发布自动化脚本,核心逻辑是:
- 逐步增加灰度版本副本数(10% → 30% → 50% → 100%)
- 每步等待一段时间,观察健康指标(错误率、P95 延迟)
- 如果指标异常,自动回滚
- 全部通过后,更新主版本,清理灰度版本
关键代码结构大概是这样:
class DeploymentAutomator:
def canary_deploy(self, config):
"""灰度发布主流程"""
for step_pct in config.canary_steps:
# 调整 canary 副本数
self.scale_deployment(canary_name, canary_replicas)
# 等待就绪
self.wait_for_rollout(namespace, canary_name)
# 观察期
time.sleep(config.step_interval)
# 健康检查
health = self.check_health_from_prometheus(service)
# 判断回滚
if health['error_rate'] > config.max_error_rate:
self.rollback_canary(config)
return False
# 全量发布
self.update_deployment_image(main_name, new_image)
self.scale_deployment(canary_name, 0)
return True
完整脚本放在了我的 DevOps 面试脚本合集中,感兴趣的可以去看。
三、常见故障排查思路
K8s 问题千千万,但排查思路是有套路的。
Pod 状态异常排查
| 状态 | 常见原因 | 排查命令 |
|---|---|---|
| Pending | 资源不足、调度失败、镜像拉不到 | kubectl describe pod <pod> 看 Events |
| ContainerCreating | 镜像拉取失败、Secret 不存在、PV 挂载失败 | kubectl describe pod 看 Events |
| CrashLoopBackOff | 应用启动失败、配置错误、健康检查失败 | kubectl logs <pod> --previous 看上一次日志 |
| ImagePullBackOff | 镜像名错、镜像不存在、认证失败 | kubectl describe pod 看 Events |
| Running 但不正常 | 应用逻辑问题、资源不足、网络问题 | kubectl logs + kubectl exec 进去看 |
排查套路
1. 先看 Pod 状态
kubectl get pods -n <namespace>
kubectl describe pod <pod-name> -n <namespace>
重点看 Events 部分,90% 的问题在这就能找到答案。
2. 再看日志
kubectl logs <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous # 看上一次崩溃的日志
kubectl logs <pod-name> -c <container-name> # 多容器指定容器
3. 进容器排查
kubectl exec -it <pod-name> -n <namespace> -- bash
# 检查网络
curl localhost:8080/health
# 检查进程
ps aux
# 检查资源
top / free / df
4. 排查网络问题
# 从其他 Pod 测试 Service 连通性
kubectl exec -it test-pod -- curl my-app.namespace.svc.cluster.local
# 看 Endpoints(Service 有没有匹配到 Pod)
kubectl get endpoints my-app -n <namespace>
# 看 DNS 解析
kubectl exec -it test-pod -- nslookup my-app
Service 不通的排查顺序
- 先看 Pod 本身能不能访问 →
kubectl exec进去 curl localhost - 再看 Pod IP 能不能访问 → 从其他 Pod curl PodIP
- 再看 Service 能不能访问 → curl ServiceName
- 再看 Endpoints →
kubectl get endpoints看有没有后端 - 最后看 kube-proxy / iptables → 高级问题
四、面试高频问题
Q: Pod 的生命周期?
Pending → ContainerCreating → Running → Succeeded/Failed
Running 阶段容器的状态:Waiting → Running → Terminated
还有几个重要阶段:init containers → startup probe → liveness/readiness probes → preStop hook
Q: Deployment、StatefulSet、DaemonSet 的区别?
- Deployment:无状态应用,Pod 可互换,名字随机
- StatefulSet:有状态应用,Pod 有稳定的名字和网络标识,有序部署和扩缩容
- DaemonSet:每个节点跑一个(或指定节点),用于日志收集、监控 Agent 等
Q: Kubernetes 的网络模型?CNI 是什么?
K8s 网络模型的核心要求:
- 每个 Pod 有独立 IP
- Pod 之间可以直接通信(不需要 NAT)
- Pod 和节点之间也可以直接通信
CNI(Container Network Interface)是一套标准接口,K8s 通过 CNI 调用网络插件来实现网络。 常见实现:Calico、Flannel、Cilium、Weave Net。
Q: livenessProbe 和 readinessProbe 的区别?
- livenessProbe(存活探针):判断容器是不是挂了,挂了就重启
- readinessProbe(就绪探针):判断容器是不是准备好了,准备好了才接入流量
简单说:liveness 管”活没活”,readiness 管”能不能干活”。 应用启动慢的场景还要加 startupProbe,专门负责启动阶段。
Q: 怎么实现零停机部署?
几个关键点:
- Readiness Probe:Pod 准备好了才接入 Service
- Rolling Update 策略:maxSurge 和 maxUnavailable 合理配置
- preStop 钩子 + 优雅关闭:Pod 停止前先等流量切走
- PDB:保证自愿性中断期间有足够的 Pod 可用
- 连接排空:应用收到 SIGTERM 后停止接受新请求,处理完现有请求再退出
写在最后
Kubernetes 是个很大的话题,一篇文章不可能覆盖全部。但部署相关的核心知识,就是上面这些——核心资源配置、灰度发布策略、故障排查思路。
掌握了这些,不管是日常工作还是面试,都够用了。更深的(网络原理、调度器、存储、安全等)就是另一个话题了。
如果你觉得有用,欢迎分享~