2023 年我们公司有一套核心交易系统 跑在 Kubernetes 上 大概 60 个 service 4 个 namespace。一开始 K8s 集群是云厂商管的我接手时配置很默认 resource request limit 凭感觉拍 HPA 全开 default 没设 PodDisruptionBudget 调度策略默认 namespace 没 quota。测试环境跑得也挺顺 但上线半年我们陆续踩了一堆坑。第一种最让我傻眼 某个 service 没设 memory limit 内存泄漏后吃到 32GB 把整个 node 拉爆 同 node 上 8 个 pod 全被 evict 业务雪崩 30 分钟。第二种最难缠 集群升级 cordon drain 一个 node 几个关键 service 没设 PDB 被一次性全 evict 跑了一段没实例的真空期。第三种最离谱 我们的 HPA 配 CPU 80% 触发扩容 实际 CPU 没到 80% 但 memory 已经 95% pod 因为 OOM 一直 kill restart loop HPA 觉得没扩容必要 业务一直挂。第四种最致命 promotion 流量 5 倍 HPA 触发但 max replicas=10 撑不住 我们手动把 max 改成 50 才扛过去 之后再没人调回去 平时空跑大量 pod 浪费资源 每月多花 1.2 万美元。第五种最莫名其妙 同一个 deployment 不同 zone 的 pod 行为不一致 某些 zone 上的 pod CPU 高 排查发现 node 是异构机型 同一个 limit 在不同 CPU 架构上表现不同。我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 K8s 就是 docker run 加自动化 把镜像扔进去给个 yaml 它自己跑 可这个认知是错的真正能扛业务的 K8s 是一个 resource 规划 加 PDB 与亲和性 加 HPA 与 VPA 联动 加 namespace quota 加 调度策略与节点池 加 升级与回滚 加 监控与容量 的整套工程方法论 任何一环没做都可能在某次发布或者峰值或者升级里造成业务中断或资源浪费本文从头梳理 K8s 生产工程的核心要点 resource request limit 怎么定 HPA 与 VPA 怎么配合 PDB 怎么用 调度怎么细化 quota 怎么管 升级回滚怎么做 以及一些把 K8s 做扎实要避开的工程坑
问题背景:为什么 K8s 不是写 yaml 就完事
很多人对 K8s 的认知是 kubectl apply 写写 deployment 就完事 但生产里你会发现 单个 pod 拖垮 node 升级时业务真空 HPA 扩不出来 namespace 之间互相挤 升级时配置丢。问题的根源在于:
- resource limit 不是可选项:不设 limit 一个内存泄漏就能拖垮整个 node 同 node 上其他业务全遭殃。
- HPA 默认只看 CPU:很多业务的瓶颈是内存或者 QPS 不是 CPU 必须配多指标或者自定义指标。
- PDB 是升级安全的必备:不配 PDB 集群升级 cordon drain 时关键 service 可能瞬间归零。
- namespace quota 防互相挤:多团队共享集群 没 quota 一个团队失控的 deployment 把整个集群挤爆。
- 调度策略影响可用性:节点亲和反亲和 跨 zone 分散 异构机型隔离 这些没做好 单 node 故障就 50% 实例挂。
- 滚动更新策略要细化:maxSurge maxUnavailable 默认值在某些场景太激进或太保守 必须按业务 SLA 调。
一 Resource Request 与 Limit:资源预算的根本
K8s 调度和 QoS 都依赖 resource request limit。request 是调度时给的保证 limit 是运行时的上限。生产环境每个 container 都必须显式配置 不配是给自己埋雷。
# deployment.yaml 生产级 resource 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: orders
spec:
replicas: 6
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
containers:
- name: app
image: registry.mycompany.com/orders-api:v2.4.1
ports:
- containerPort: 8080
resources:
requests:
cpu: 500m
memory: 512Mi
ephemeral-storage: 1Gi
limits:
cpu: 2000m
memory: 2Gi
ephemeral-storage: 4Gi
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 4
lifecycle:
preStop:
exec:
command: ['sh', '-c', 'sleep 10 && kill -SIGTERM 1']
request 和 limit 设置的核心原则 request 按 P50 实际用量设 limit 按 P99 加一些 buffer 设 ratio 一般 1:3 到 1:5 之间 太接近调度灵活性差 差太多容易超卖触发 OOM 连锁反应。preStop hook 加 sleep 10 是优雅退出 让 Service 把流量摘掉再发 SIGTERM 不然请求中途断连。livenessProbe 阈值要宽容 否则启动期就被反复 kill restart loop 永远起不来。
二 HPA:多指标自动扩缩容
默认 HPA 只看 CPU 80% 触发 但很多业务是 IO bound 或者 memory bound CPU 没到 80% 但已经响应慢。必须用 v2 版本 HPA 配多指标 加自定义指标比如 QPS RT 排队长度。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: orders-api-hpa
namespace: orders
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: orders-api
minReplicas: 4
maxReplicas: 40
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "200"
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
- type: Pods
value: 4
periodSeconds: 60
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
HPA 的 behavior 是 1.18+ 的关键能力 scaleUp 快 stabilizationWindow 60 秒 scaleDown 慢 300 秒 避免抖动 上线时缩容太快直接挤掉刚启动的 pod。多指标 selectPolicy=Max 任意一个指标触发就扩 这样 CPU 没到 但 QPS 已经到 200 也能扩。自定义 QPS 指标需要装 prometheus-adapter 把 Prometheus 数据暴露给 HPA。
三 PDB 与亲和性:升级与故障的双保险
PodDisruptionBudget 是声明 这个应用至少要有 N 个实例可用 K8s 调度器在 drain node 时会遵守 PDB 不会把可用实例降到阈值以下 这是滚动升级 节点 cordon 的安全网。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: orders-api-pdb
namespace: orders
spec:
minAvailable: 50% # 也可以 maxUnavailable: 25% 二选一
selector:
matchLabels:
app: orders-api
PDB 只解决了升级时的最小可用 但要让单 node 故障不影响业务 还要做 pod 的反亲和让它们跨 node 跨 zone 分散。下面是 deployment 里的 topologySpreadConstraints 与 nodeAffinity 配置 推荐用 topologySpread 替代老的 podAntiAffinity 更灵活也更精细。
# 反亲和让 pod 跨 node 跨 zone 分散 单 node 故障不会全挂
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: orders-api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: orders-api
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-pool
operator: In
values: ['app-pool']
- key: arch
operator: In
values: ['amd64']
PDB 的关键是 minAvailable 必须低于 replicas 否则 drain 时一个 pod 也不能 evict 升级会卡死 一般设 replicas 的 50-75% 让升级有空间又保住可用。topologySpreadConstraints 比老的 podAntiAffinity 更灵活 maxSkew=1 让 pod 在 zone 之间均匀分布 一个 zone 挂了还有 2/3 实例。nodeAffinity 显式指定 node-pool 加 arch 防止异构机型上跑出不同行为。
四 Namespace Quota 与 LimitRange:多团队共享集群
多团队共享一个集群时 不设 quota 一个团队的 deployment 失控会把整个集群挤爆 其他团队的 pod 调度不进来。ResourceQuota 限制 namespace 级别的总资源 LimitRange 给 pod container 设默认 request limit 防止漏配。
apiVersion: v1
kind: ResourceQuota
metadata:
name: orders-quota
namespace: orders
spec:
hard:
requests.cpu: "100"
requests.memory: 200Gi
limits.cpu: "200"
limits.memory: 400Gi
requests.storage: 1Ti
persistentvolumeclaims: "50"
pods: "200"
services: "50"
services.loadbalancers: "5"
ResourceQuota 把 namespace 的总额度卡死 接下来 LimitRange 给每个 pod container 设默认与上下限 防止某个 deployment 漏配 request limit 或者一个 pod 申请过多资源把 quota 一次性吃光。
apiVersion: v1
kind: LimitRange
metadata:
name: orders-defaults
namespace: orders
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: 4000m
memory: 8Gi
- type: Pod
max:
cpu: 8000m
memory: 16Gi
quota 配置的关键原则是 留 20% buffer 给运维突发用 比如 hotfix 紧急扩容 否则 quota 卡得太死反而限制了应急能力。LimitRange 让漏配 resource 的 pod 自动套上默认值 防止误操作 我们要求所有 namespace 必须有 LimitRange 这是上线 review 的必查项。
[mermaid]flowchart TD
A[Deployment apply] --> B[K8s API Server]
B --> C{Quota 检查}
C -->|超限| D[reject]
C -->|通过| E[LimitRange 套默认]
E --> F[Scheduler 选 node]
F --> G[亲和反亲和约束]
G --> H[资源 request 检查]
H --> I[Pod 调度到 node]
I --> J[readiness 通过加入 Service]
J --> K[HPA 监控指标]
K -->|触发| L[扩缩容]
L --> F
M[节点 drain] --> N[PDB 检查]
N -->|不满足| O[等待]
N -->|满足| P[evict pod]
五 滚动更新与回滚:发布安全
K8s 默认 RollingUpdate 策略 maxSurge=25% maxUnavailable=25% 对一般业务够用 但金融或者核心交易需要更保守 必须按业务 SLA 调。同时回滚要简单可靠 kubectl rollout undo 是一键回滚 但前提是 deployment 历史保留足够。
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
replicas: 10
revisionHistoryLimit: 20 # 默认 10 关键服务建议 20+
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # 同时多起 2 个新 pod
maxUnavailable: 0 # 不允许实例下降 严格保住容量
minReadySeconds: 15 # 新 pod ready 后再等 15 秒确认稳定
progressDeadlineSeconds: 600 # 10 分钟没完成视为失败
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: registry.mycompany.com/orders-api:v2.4.2
常用滚动操作命令:
# 触发滚动更新 修改 image 后会自动启动
kubectl set image deploy/orders-api app=registry.mycompany.com/orders-api:v2.4.2 -n orders
# 实时看进度
kubectl rollout status deploy/orders-api -n orders --timeout=10m
# 暂停滚动 用于灰度验证
kubectl rollout pause deploy/orders-api -n orders
# 恢复滚动
kubectl rollout resume deploy/orders-api -n orders
# 一键回滚到上一版本
kubectl rollout undo deploy/orders-api -n orders
# 回滚到指定历史 revision
kubectl rollout history deploy/orders-api -n orders
kubectl rollout undo deploy/orders-api -n orders --to-revision=14
# 看具体 revision 配置
kubectl rollout history deploy/orders-api -n orders --revision=14
maxUnavailable=0 是金融业务的严格选择 滚动期间实例数永远不低于 replicas 是绝对的容量保证 但代价是滚动更慢 maxSurge 决定了同时启新 pod 的数量。minReadySeconds 让新 pod 通过 readinessProbe 后还要 stable 一段时间才算成功 这是防止 readiness 检测过早的安全网。revisionHistoryLimit 决定能回滚多少代 关键服务设大点。
六 K8s 工程坑:那些文档里学不到的
讲完原理来说几个真实生产里踩过的坑。第一个坑是 init container 阻塞 init container 失败会让整个 pod 一直处于 Init 状态 readiness 也通不过 Service 不会摘掉这个 pod 但流量也进不去 必须监控 pod phase 不只看 Ready 数。第二个坑是 Service ClusterIP DNS 解析 大量 pod 频繁创建销毁 CoreDNS 的负载暴涨 必须 deploy 多个 CoreDNS 副本 加 NodeLocal DNSCache 减少 DNS 查询成本。第三个坑是 ConfigMap 更新不重启 pod ConfigMap 修改后挂载的文件会自动更新 但环境变量形式注入的不会 pod 不重启读到的还是旧值 必须用 Reloader 这种工具或者强制 rollout restart。第四个坑是 Secret base64 不是加密 base64 只是编码 任何能看 etcd 的人都能看到内容 必须用 KMS 加密 etcd at rest 或者用 Vault Sealed Secrets 这种外部 secret 系统。第五个坑是 HPA 与 Cluster Autoscaler 配合 HPA 扩 pod 集群可能没足够 node Cluster Autoscaler 来加 node 但 CA 加 node 要几分钟 这段时间 pod 处于 Pending 必须预留 buffer node 或者用 Karpenter 这种秒级扩容方案。
关键概念速查
| 概念 | 含义 | 工程价值 |
|---|---|---|
| request limit | 资源预算 | QoS 与调度基础 |
| HPA v2 | 多指标自动扩容 | CPU 不是唯一信号 |
| VPA | 纵向自动调整 | request limit 自适应 |
| PDB | 最小可用实例 | 升级安全网 |
| topologySpread | 跨域分散 | 单 node 故障保命 |
| ResourceQuota | namespace 总额 | 多团队共享必备 |
| LimitRange | 容器默认与上限 | 防漏配 |
| maxSurge maxUnavailable | 滚动策略 | SLA 决定参数 |
| rollout undo | 一键回滚 | 故障兜底 |
| CoreDNS NodeLocal | DNS 本地缓存 | 大集群必备 |
避坑清单
- 每个 container 必须配 request 和 limit 不配等于给自己埋雷 一个内存泄漏拖垮整个 node。
- HPA 必须用 v2 多指标 CPU memory QPS 任意一个触发就扩 单指标容易失灵。
- HPA behavior scaleDown stabilization 300 秒以上 防止流量短暂下降就缩容掉刚扩的 pod。
- 每个关键 deployment 必须有 PDB 否则集群升级时业务可能瞬间归零。
- topologySpreadConstraints 跨 zone 跨 node 分散 单点故障最多影响 1/N 实例。
- 多团队共享集群必须配 ResourceQuota 加 LimitRange 防止一个失控的 deployment 挤爆所有人。
- maxUnavailable=0 是核心交易业务的严格选择 滚动期间容量不下降。
- revisionHistoryLimit 关键服务 20+ 让回滚有足够空间。
- Secret 必须开 etcd at rest 加密 或者用 Vault Sealed Secrets base64 不是加密。
- 大集群上 NodeLocal DNSCache 减 CoreDNS 压力 大量 pod 时 DNS 是隐藏瓶颈。
总结
K8s 这事 很多人的直觉是 写写 yaml kubectl apply 就完事 这其实是把 我会写 deployment 和 我能在生产用 K8s 扛住高并发高可用大集群多团队 混为一谈。前者是会用 API 后者是懂 K8s 工程。中间隔着的是 resource 规划 HPA 多指标 PDB 与亲和性 namespace quota 滚动策略 监控容量 整整一套工程方法论。
从原型到生产 你需要做的事远不止 kubectl apply。你要懂 resource request 与 limit 怎么定 要会配多指标 HPA 要设 PDB 与 topologySpread 要规划 namespace quota 与 LimitRange 要调滚动策略 要管 ConfigMap 与 Secret 要解决 DNS 与 CA 配合。每一项单独看都不复杂 但它们组合在一起 才是一个能扛业务的 K8s 体系。少任何一项 都可能在某次发布或者升级或者峰值里让你的业务挂上一段时间。
我经常用一个比喻来理解 K8s 它有点像一个智能写字楼。Pod 是租户 Node 是楼层 request limit 是合同里租用的工位数 HPA 是租户业务火了自动扩工位 PDB 是写字楼装修时必须保证租户运营不中断 Quota 是楼层总容量 RollingUpdate 是搬新址时分批搬不一次性全断。你不能因为有了写字楼就觉得租户都能正常运营 还要管租约工位数 业务高峰扩容 装修时不中断 楼层容量 搬迁策略 这才是一整套写字楼运营。
这套架构最难的地方在于 它的复杂度在小集群跑两个 demo deployment 时几乎完全暴露不了。你的小集群跑 10 个 pod 一切都很顺 觉得 K8s 真好用。但真正上规模 60 个 service 1000 个 pod 多团队多 namespace 业务峰值 5 倍 升级要求零宕机 你才发现 99% 的复杂度都在 那 1% 的极端 case 里 resource 不当一个 pod 拖垮一片 HPA 失灵扩不出来 PDB 没配升级断业务 Quota 没设互相挤死 滚动策略激进容量下降。建议任何想用 K8s 上规模业务的团队 上线前一定要做 故障演练 故意杀 node 故意拉满流量 故意做错配置升级 看业务能不能抗住 千万别等真实故障来教你 那时候业务损失已经造成了。
—— 别看了 · 2026