{SITE.title} logo 云萱笔记
云原生 · · 4 分钟阅读

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 版本的灰度发布自动化脚本,核心逻辑是:

  1. 逐步增加灰度版本副本数(10% → 30% → 50% → 100%)
  2. 每步等待一段时间,观察健康指标(错误率、P95 延迟)
  3. 如果指标异常,自动回滚
  4. 全部通过后,更新主版本,清理灰度版本

关键代码结构大概是这样:

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 不通的排查顺序

  1. 先看 Pod 本身能不能访问 → kubectl exec 进去 curl localhost
  2. 再看 Pod IP 能不能访问 → 从其他 Pod curl PodIP
  3. 再看 Service 能不能访问 → curl ServiceName
  4. 再看 Endpoints → kubectl get endpoints 看有没有后端
  5. 最后看 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 网络模型的核心要求:

  1. 每个 Pod 有独立 IP
  2. Pod 之间可以直接通信(不需要 NAT)
  3. Pod 和节点之间也可以直接通信

CNI(Container Network Interface)是一套标准接口,K8s 通过 CNI 调用网络插件来实现网络。 常见实现:Calico、Flannel、Cilium、Weave Net。

Q: livenessProbe 和 readinessProbe 的区别?

  • livenessProbe(存活探针):判断容器是不是挂了,挂了就重启
  • readinessProbe(就绪探针):判断容器是不是准备好了,准备好了才接入流量

简单说:liveness 管”活没活”,readiness 管”能不能干活”。 应用启动慢的场景还要加 startupProbe,专门负责启动阶段。

Q: 怎么实现零停机部署?

几个关键点:

  1. Readiness Probe:Pod 准备好了才接入 Service
  2. Rolling Update 策略:maxSurge 和 maxUnavailable 合理配置
  3. preStop 钩子 + 优雅关闭:Pod 停止前先等流量切走
  4. PDB:保证自愿性中断期间有足够的 Pod 可用
  5. 连接排空:应用收到 SIGTERM 后停止接受新请求,处理完现有请求再退出

写在最后

Kubernetes 是个很大的话题,一篇文章不可能覆盖全部。但部署相关的核心知识,就是上面这些——核心资源配置、灰度发布策略、故障排查思路。

掌握了这些,不管是日常工作还是面试,都够用了。更深的(网络原理、调度器、存储、安全等)就是另一个话题了。

如果你觉得有用,欢迎分享~