2522 字
约 8 分钟
16
S15-X15 · 容器编排:发布、回滚与配置注入

发布出事很少是"新版本代码有 bug"这一类,多数是编排层面的三件事:滚动参数把可用副本压到 0,探针让流量在进程就绪前打进来,配置改了但没触发重启。这几个问题都不在业务代码里,却都表现为 5xx。

发布方案要给三个答案:切换方式、回滚速度、配置怎么进容器。


1. 四种发布策略

策略 停机 回滚速度 资源开销 适用
滚动(RollingUpdate) 无 中(再滚一次) 低(原地替换) 默认首选
蓝绿(Blue/Green) 无 快(切 Service) 高(双份资源) 关键服务、要秒级回退
金丝雀(Canary) 无 快(调权重) 中 有流量比例可控的入口
重建(Recreate) 有 中 低 单实例、有状态、不可并存

没有入口流量控制能力(没有网关/Service Mesh)时,金丝雀只能靠副本数近似——比如 10 个副本里先起 1 个新版本,本质是粗粒度滚动。


2. 滚动更新的四个参数

参数 默认 作用 调错的后果
maxSurge 25% 允许超出期望副本的数量 太大吃满资源,新 Pod 卡 Pending
maxUnavailable 25% 允许同时不可用的数量 太大容量瞬时不足,5xx
minReadySeconds 0 就绪后再观察多久才算可用 为 0 时"刚起来就报错"的版本直接进池
progressDeadlineSeconds 600 无进展多久判定失败 太小误判慢启动为失败,反复重试
spec:
  replicas: 6
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2            # 峰值 8 个 Pod
      maxUnavailable: 1      # 最低 5 个可用
  minReadySeconds: 15
  progressDeadlineSeconds: 600
  revisionHistoryLimit: 10

容量算式:滚动期间最低可用副本 = replicas - maxUnavailable。6 - 1 = 5,即发布过程中至少损失 1/6 容量,若线上水位已到 80%,这一下就会打满。

反过来说,maxSurge: 0 + maxUnavailable: 1 是"省资源但降容量"的组合;maxSurge: 1 + maxUnavailable: 0 是"不降容量但要多一份资源"。二者不能同时为 0,否则滚动永远无法推进。


3. 探针与流量次序

探针 答什么问题 失败后果 别用它做什么
startupProbe 进程起来了没 重启容器 判业务可用性
readinessProbe 能接流量了没 摘出 Endpoints 判进程死活
livenessProbe 还活着没 杀容器重启 探数据库/下游

次序是硬约束:readinessProbe 通过 → Pod 进 Endpoints → 才有流量。所以任何"启动后要预热 30 秒"的服务,都必须让 readiness 在这 30 秒里返回失败,而不是让 livenessProbe 去做这件事。

startupProbe:
  httpGet: { path: /actuator/health/liveness, port: 8080 }
  periodSeconds: 5
  failureThreshold: 36            # 最多等 180s,覆盖 JVM + 预热
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 8080 }
  periodSeconds: 5
  failureThreshold: 2
livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 8080 }
  periodSeconds: 10
  failureThreshold: 3

Spring Boot 3.3.x 里 management.endpoint.health.probes.enabled=true 会提供 /actuator/health/liveness 与 /actuator/health/readiness 两组端点,且 readiness 组默认可包含 db/redis 等 indicator——这就是"探下游"唯一该放的地方。把 db 放进 liveness,数据库抖一下就会把所有 Pod 连环杀掉,是最典型的事故。


4. 优雅下线:先摘流量,再停进程

Pod 删除是并行的两件事:从 Endpoints 摘除、向容器发 SIGTERM。两者没有先后保证,所以必须自己加缓冲。

lifecycle:
  preStop:
    exec:
      command: ["sh","-c","sleep 5"]
terminationGracePeriodSeconds: 40
// Spring Boot:等优雅停机把在途请求处理完
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

preStop sleep 5 给 kube-proxy/ingress 摘流量的时间,terminationGracePeriodSeconds 要大于 preStop + 关闭耗时,否则到点 SIGKILL,日志里只有一条 exit code 137。


5. 回滚:三条"回不去"的路

kubectl rollout status deploy/api --timeout=300s
kubectl rollout history deploy/api
kubectl rollout undo deploy/api                 # 回上一版
kubectl rollout undo deploy/api --to-revision=3 # 回指定版
回不去的原因 表现 事前对策
镜像被覆盖或用了 :latest 回滚后拉到的还是新代码 用不可变 tag(commit sha),禁 latest
配置已改且不在版本控制里 回滚镜像,配置仍是新的 配置进 Git,随版本一起回
数据库已迁移 老代码读新表结构报错 迁移向后兼容(先加列后用,两阶段删)

第三条最难,因为它意味着回滚不是部署动作,而是数据契约问题。只要迁移遵循"扩展-收缩"(先加字段、双写、再切读、最后删旧字段),镜像回滚就能成立。

revisionHistoryLimit 也别设成 0 或 2:太小就没有历史版本可回滚。


6. 配置注入的三种方式

方式 生效时机 变更是否重启 适用
环境变量 容器创建时 必须重启 简单标量、连接串
挂载文件(ConfigMap/Secret) kubelet 同步后(分钟级) 取决于应用是否热加载 配置文件、证书
配置中心 应用主动拉取/推送 可热更新 频繁变更、多环境

三条铁律:环境变量改了就一定得重启(进程创建后无法改);挂载文件更新有延迟且用 subPath 挂的单文件永不更新;配置中心热更新要求应用真的实现了刷新逻辑,否则只是内存里换了个值。

volumes:
  - name: cfg
    configMap: { name: api-config }
containers:
  - volumeMounts:
      - name: cfg
        mountPath: /etc/api/application.yml
        subPath: application.yml     # 陷阱:不会随 ConfigMap 更新

用 subPath 时唯一可靠的做法是改配置就改 Pod 模板,靠滚动更新生效。常见做法是给 Pod 加一个 checksum 注解:

metadata:
  annotations:
    checksum/config: "{{ sha256sum of application.yml }}"

配置一变注解就变,Pod 模板变化触发滚动——这样"配置变更"和"发布"走同一条可回滚的路径。


7. Secret 不是加密

认知 事实
Secret 比 ConfigMap 安全 只是 base64 编码,任何能 get secret 的人都能读
etcd 里是密文 默认不加密;需配 EncryptionConfiguration
有了 RBAC 就安全 RBAC 只管"能不能读",不管"读到了能干嘛"
打进镜像最方便 镜像可被任何有拉取权限的人解出来
# 生产:外部密钥管理 + CSI 驱动,Secret 不落地成 K8s 对象
- name: db-password
  secretStoreRef: { name: vault-backend, kind: ClusterSecretStore }

至少做到:etcd 静态加密、限制 get/list secrets 的 RBAC、密钥用外部 KMS/Vault 注入、镜像里绝不含密钥。


8. requests 与 limits:一个管调度,一个管杀

字段 谁用 设错的后果
requests.cpu 调度器分配节点 设太小 → 节点超卖 → 互相抢 CPU
requests.memory 调度器 + 驱逐判断 设太小 → 节点内存超卖 → OOMKill
limits.cpu cgroup 限流(CFS) 设太小 → 周期性卡顿,延迟尖刺
limits.memory OOMKill 阈值 设太小 → 直接 137

JVM 服务的两个常见错法:limits.memory 设 1Gi 但没设堆,JVM 按宿主机内存算默认堆,直接被 OOMKill;或者堆设满整个 limit,容器开销一挤就死。

resources:
  requests: { cpu: "500m", memory: "1Gi" }
  limits:   { cpu: "2",    memory: "2Gi" }
env:
  - name: JAVA_TOOL_OPTIONS
    value: "-XX:MaxRAMPercentage=70 -XX:+UseG1GC"

MaxRAMPercentage=70 给堆外(元空间、线程栈、直接内存、Netty)留 30%。看到 exit code 137 且日志里没有 OutOfMemoryError,基本就是 cgroup 内存超限,不是 JVM 堆溢出——两者的排查路径完全不同。


9. 常见坑

坑 现象 改法
maxUnavailable 过大 发布期间 5xx 上升 降到 1,或加 maxSurge
minReadySeconds: 0 坏版本立刻全量 设 10~30s
liveness 探 DB 数据库抖动引发连环重启 只探进程,db 放 readiness
无 preStop 摘流量前进程已退,少量 5xx preStop sleep 5
用 :latest 回滚无效 不可变 tag
subPath 挂配置 改了 ConfigMap 不生效 checksum 注解触发滚动
revisionHistoryLimit: 0 无版本可回 保留 3~10
只设 limits 不设 requests 调度按最小值分配,节点超卖 requests 必填

10. 排错

现象 排查方向
Pod 一直 Pending 资源不足或 maxSurge 超出配额;看 kubectl describe 事件
滚动卡住不动 progressDeadlineSeconds + readiness 一直不通过
新 Pod Ready 但无流量 Service selector / targetPort 不匹配;查 Endpoints
日志 exit code 137 cgroup 内存超限或 terminationGracePeriodSeconds 到点
代码回滚了行为没变 镜像 tag 被覆盖,或配置/DB 已变
延迟周期性尖刺 limits.cpu 触发 CFS 限流;看 container_cpu_cfs_throttled

11. 边界

场景 更适合的做法
单实例内部工具 Recreate 即可,不必滚动
有状态服务 StatefulSet + 有序更新,滚动语义不同
数据库迁移 独立 Job,与发布解耦,走扩展-收缩
极短生命周期任务 Job/CronJob,谈发布策略没意义
无入口流量控制 金丝雀退化为按副本比例近似

12. 自测

  1. maxSurge: 0 与 maxUnavailable: 1 组合下,发布期间可用副本是多少?
  2. 为什么"探数据库"这件事只能放在 readiness 而不是 liveness?
  3. preStop sleep 5 解决的是什么时序问题?
  4. 三种"回滚不生效"的原因里,哪一种不是部署问题?
  5. 用 subPath 挂载的配置文件为什么改 ConfigMap 不生效?
  6. 看到 exit code 137 却找不到 OOM 日志,先查什么?
S15-X15 · 容器编排:发布、回滚与配置注入
https://www.clxhxhhr.top/posts/5944/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。