目录
-
- [一、资源配置:requests、limits 与 QoS](#一、资源配置:requests、limits 与 QoS)
-
- [1.1 两个参数的真实含义](#1.1 两个参数的真实含义)
- [1.2 CPU limits 的争议](#1.2 CPU limits 的争议)
- [1.3 JVM 在容器里的内存](#1.3 JVM 在容器里的内存)
- 二、探针:三种探针各管一件事
- [三、优雅发布:消除发布期间的 502](#三、优雅发布:消除发布期间的 502)
-
- [3.1 问题根源:两件事并行发生](#3.1 问题根源:两件事并行发生)
- [3.2 解决方案](#3.2 解决方案)
- [3.3 滚动更新参数](#3.3 滚动更新参数)
- 四、高可用:不要把鸡蛋放在一个篮子里
-
- [4.1 拓扑分布](#4.1 拓扑分布)
- [4.2 PodDisruptionBudget](#4.2 PodDisruptionBudget)
- [五、弹性伸缩:HPA 的计算方式与调优](#五、弹性伸缩:HPA 的计算方式与调优)
-
- [5.1 HPA 怎么算副本数](#5.1 HPA 怎么算副本数)
- [5.2 用 behavior 控制扩缩节奏](#5.2 用 behavior 控制扩缩节奏)
- [5.3 HPA 的局限](#5.3 HPA 的局限)
- [六、Service Mesh:解决什么问题,付出什么代价](#六、Service Mesh:解决什么问题,付出什么代价)
-
- [6.1 它解决的问题](#6.1 它解决的问题)
- [6.2 Istio 流量治理示例](#6.2 Istio 流量治理示例)
- [6.3 代价](#6.3 代价)
- [6.4 引入 Mesh 前的自查](#6.4 引入 Mesh 前的自查)
- [七、交付:GitOps 与渐进式发布](#七、交付:GitOps 与渐进式发布)
- 八、生产就绪检查清单
- 九、总结
- 参考资料
适合谁读:正在把 Java / Spring Boot 服务迁移到 Kubernetes,或者已经上了 K8s 但发布时偶发 502、Pod 频繁 OOMKilled 的后端与运维工程师。
解决什么问题 :把应用打成镜像、写个 Deployment 跑起来,只需要半天;但让它在 K8s 上稳定地跑------发布时不报错、节点故障不掉线、流量上涨能扩容、内存不被 OOMKilled------需要处理一长串细节。本文以一个 Spring Boot 订单服务上 K8s 为主线,逐项讲清楚这些细节背后的机制,最后讨论 Service Mesh 到底什么时候值得引入。
本文是"技术架构实战"系列的第 4 篇。应用层的限流、熔断与重试设计见 高并发高可用系统设计:限流、熔断、降级与多级缓存实战指南,本文第六节会讨论它们与 Service Mesh 的重叠问题。
一、资源配置:requests、limits 与 QoS
1.1 两个参数的真实含义
- requests:调度依据。调度器保证节点上所有 Pod 的 requests 之和不超过节点可分配资源;CPU requests 还决定了 CPU 争抢时的权重;
- limits :运行时上限。CPU 超过 limits 会被 CFS 限流(throttling) ,内存超过 limits 会被内核 OOMKill。
由二者关系决定 Pod 的 QoS 等级,节点内存紧张时按 BestEffort → Burstable → Guaranteed 的顺序驱逐:
| QoS | 条件 | 适用 |
|---|---|---|
| Guaranteed | 所有容器都设置了 CPU 和内存,且 requests = limits | 核心在线服务、数据库 |
| Burstable | 至少一个容器设置了 requests 或 limits,但不满足 Guaranteed | 大多数业务服务 |
| BestEffort | 都没设置 | 只适合可随时丢弃的任务,生产环境不建议 |
1.2 CPU limits 的争议
CFS 限流按 100ms 周期计算配额。设置 limits.cpu: 2 意味着每 100ms 内最多使用 200ms 的 CPU 时间。一个有 8 个活跃线程的 Java 服务,可能在周期前 25ms 就用完了配额,剩下 75ms 全部被挂起,表现为 P99 延迟周期性地出现几十毫秒的毛刺,而平均 CPU 使用率看起来还很低。
因此对延迟敏感的服务,常见的两种做法是:
- 设置 CPU requests、不设 CPU limits,依靠 requests 保证份额,允许利用节点空闲 CPU(需要集群层面有合理的超卖控制);
- 或者设置 limits,但通过
container_cpu_cfs_throttled_periods_total监控限流比例,超过 10%~20% 就调高。
内存则必须设置 limits,而且建议 requests = limits:内存不可压缩,超卖内存的后果是 OOMKill 或节点被驱逐。
1.3 JVM 在容器里的内存
Java 服务最常见的问题:堆设置了 -Xmx2g,容器 limits 也是 2Gi,结果频繁 OOMKilled。原因是 JVM 进程内存 ≠ 堆内存:
进程内存 ≈ 堆 + 元空间 + 线程数 × 1 MB + 直接内存 + 代码缓存 + GC 开销 \text{进程内存} \approx \text{堆} + \text{元空间} + \text{线程数} \times 1\text{MB} + \text{直接内存} + \text{代码缓存} + \text{GC 开销} 进程内存≈堆+元空间+线程数×1MB+直接内存+代码缓存+GC 开销
推荐做法:
yaml
resources:
requests: { cpu: "1", memory: 2Gi }
limits: { memory: 2Gi }
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:MaxRAMPercentage=70.0
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
-XX:+ExitOnOutOfMemoryError
- 使用
MaxRAMPercentage让堆跟随容器 limits 变化(JDK 8u191 及以上支持容器感知),堆占 60%~75%,留出非堆空间; ExitOnOutOfMemoryError让 JVM 在堆 OOM 时直接退出,由 K8s 重启,而不是半死不活地继续接流量;- 内存使用以
container_memory_working_set_bytes为准,这是 kubelet 判断驱逐和 OOM 时参考的指标。
二、探针:三种探针各管一件事
| 探针 | 失败后果 | 用来回答的问题 |
|---|---|---|
| startupProbe | 超过阈值后重启容器 | 应用启动完成了吗?(成功前不执行另外两个探针) |
| readinessProbe | 从 Service 的 Endpoints 中摘除,不重启 | 现在可以接收流量吗? |
| livenessProbe | 重启容器 | 进程是否陷入了无法自愈的状态? |
最常见、也是最危险的错误:在 liveness 探针里检查数据库、Redis 等依赖。数据库抖动 10 秒,所有 Pod 的 liveness 同时失败,K8s 把整个服务的所有实例一起重启,一次依赖抖动被放大成全面故障,而重启本身对修复数据库毫无帮助。
正确配置(Spring Boot Actuator 已内置 liveness 与 readiness 两个健康组):
yaml
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8081 }
periodSeconds: 5
failureThreshold: 36 # 最多允许 180 秒启动时间
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8081 }
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8081 }
periodSeconds: 10
failureThreshold: 6 # 连续 60 秒失败才重启,避免 Full GC 时误杀
yaml
# application.yml
management:
server.port: 8081 # 管理端口与业务端口分离,避免业务线程池打满时探针也超时
endpoint.health.probes.enabled: true
endpoint.health.group.readiness.include: readinessState,db
liveness 只反映进程自身状态;readiness 可以包含关键依赖,但要谨慎:如果所有实例都依赖同一个数据库,数据库故障时所有实例同时变为未就绪,效果和全部宕机一样。是否把依赖纳入 readiness,取决于"摘除流量"是否真的有助于用户体验。
三、优雅发布:消除发布期间的 502
3.1 问题根源:两件事并行发生
删除一个 Pod 时,K8s 同时做两件事:
Endpoints/kube-proxy/网关 应用容器 kubelet API Server Endpoints/kube-proxy/网关 应用容器 kubelet API Server #mermaid-svg-iHaGp2T1TDPJmTMW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iHaGp2T1TDPJmTMW .error-icon{fill:#552222;}#mermaid-svg-iHaGp2T1TDPJmTMW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iHaGp2T1TDPJmTMW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iHaGp2T1TDPJmTMW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iHaGp2T1TDPJmTMW .marker.cross{stroke:#333333;}#mermaid-svg-iHaGp2T1TDPJmTMW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iHaGp2T1TDPJmTMW p{margin:0;}#mermaid-svg-iHaGp2T1TDPJmTMW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-iHaGp2T1TDPJmTMW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-iHaGp2T1TDPJmTMW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-iHaGp2T1TDPJmTMW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-iHaGp2T1TDPJmTMW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-iHaGp2T1TDPJmTMW .sequenceNumber{fill:white;}#mermaid-svg-iHaGp2T1TDPJmTMW #sequencenumber{fill:#333;}#mermaid-svg-iHaGp2T1TDPJmTMW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-iHaGp2T1TDPJmTMW .messageText{fill:#333;stroke:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-iHaGp2T1TDPJmTMW .labelText,#mermaid-svg-iHaGp2T1TDPJmTMW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .loopText,#mermaid-svg-iHaGp2T1TDPJmTMW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-iHaGp2T1TDPJmTMW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-iHaGp2T1TDPJmTMW .noteText,#mermaid-svg-iHaGp2T1TDPJmTMW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-iHaGp2T1TDPJmTMW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-iHaGp2T1TDPJmTMW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-iHaGp2T1TDPJmTMW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-iHaGp2T1TDPJmTMW .actorPopupMenu{position:absolute;}#mermaid-svg-iHaGp2T1TDPJmTMW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-iHaGp2T1TDPJmTMW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-iHaGp2T1TDPJmTMW .actor-man circle,#mermaid-svg-iHaGp2T1TDPJmTMW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-iHaGp2T1TDPJmTMW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 各节点 iptables/IPVS、Ingress、 注册中心更新需要若干秒 若应用立即退出, 仍在路上的请求会失败 Pod 标记为 Terminating 从 Endpoints 中移除该 Pod 执行 preStop,然后发送 SIGTERM
这两条路径没有先后保证。如果应用收到 SIGTERM 立刻关闭,而 kube-proxy 和 Ingress 还没完成更新,新请求仍会被转发到这个已经关闭的 Pod,结果就是 502 或连接被拒绝。
3.2 解决方案
yaml
spec:
terminationGracePeriodSeconds: 45 # 必须大于 preStop + 应用优雅停机时间
containers:
- name: order-service
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # 等待各处摘除流量
yaml
# application.yml
server:
shutdown: graceful # 停止接收新请求,等待处理中的请求完成
spring:
lifecycle:
timeout-per-shutdown-phase: 25s
时间线:preStop 等待 10 秒(这期间流量逐步摘除)→ 收到 SIGTERM → Spring 等待处理中的请求完成(最多 25 秒)→ 退出。总计 35 秒,小于 45 秒的宽限期。
1.30 及以上版本可以直接使用
preStop.sleep.seconds,不再依赖镜像里有sleep命令。
如果服务还注册在 Nacos、Eureka 这类注册中心,preStop 中应该先主动注销实例再 sleep,因为客户端缓存刷新通常比 kube-proxy 更慢。
3.3 滚动更新参数
yaml
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0 # 先起新 Pod、就绪后再停旧 Pod,容量不下降
minReadySeconds: 10 # 新 Pod 就绪后再观察 10 秒才算可用,过滤"就绪后立刻崩溃"
四、高可用:不要把鸡蛋放在一个篮子里
4.1 拓扑分布
3 个副本恰好被调度到同一节点,节点一挂服务就全挂了。用拓扑分布约束把副本分散到不同可用区和节点:
yaml
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway # 尽量跨可用区,资源不足时允许不均匀
labelSelector: { matchLabels: { app: order-service } }
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule # 严格要求跨节点
labelSelector: { matchLabels: { app: order-service } }
4.2 PodDisruptionBudget
节点升级、缩容时执行的 kubectl drain 属于自愿中断。没有 PDB 时,drain 可能同时驱逐同一服务的多个副本:
yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-service
spec:
minAvailable: 2 # 驱逐时至少保留 2 个可用副本
selector:
matchLabels: { app: order-service }
注意:副本数为 2 时设置 minAvailable: 2,会导致 drain 永远无法完成,运维升级节点时会被卡住。PDB 要和副本数一起设计。
五、弹性伸缩:HPA 的计算方式与调优
5.1 HPA 怎么算副本数
期望副本数 = ⌈ 当前副本数 × 当前指标值 目标值 ⌉ \text{期望副本数} = \left\lceil \text{当前副本数} \times \frac{\text{当前指标值}}{\text{目标值}} \right\rceil 期望副本数=⌈当前副本数×目标值当前指标值⌉
例如目标 CPU 使用率 60%(相对于 requests ),当前 4 个副本平均 90%:ceil(4 × 90 / 60) = 6。
这个公式揭示了两个容易忽略的点:
- CPU 使用率是相对于 requests 计算的,requests 设得过小,HPA 会频繁扩容;不设 requests,HPA 根本无法工作;
- Java 服务启动时 JIT 编译会使 CPU 飙高,新 Pod 刚启动就可能触发下一轮扩容。HPA 在 Pod 启动初期会忽略部分 CPU 样本,但仍建议配合 startupProbe 和扩容稳定窗口使用。
5.2 用 behavior 控制扩缩节奏
yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: order-service }
minReplicas: 4
maxReplicas: 40
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 60 }
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- { type: Percent, value: 100, periodSeconds: 30 } # 30 秒内最多翻倍
scaleDown:
stabilizationWindowSeconds: 300 # 5 分钟内取最高建议值,防抖动
policies:
- { type: Percent, value: 10, periodSeconds: 60 } # 每分钟最多缩 10%
原则:扩容要快,缩容要慢。
5.3 HPA 的局限
- HPA 是被动的 :从流量上涨到新 Pod 就绪,需要指标采集(约 15~30 秒)+ 调度 + 拉镜像 + 应用启动(Java 服务常常 30~90 秒),总计可能超过 2 分钟。大促、秒杀这类可预期的峰值必须提前扩容,不能依赖 HPA;
- CPU 不一定是最佳指标:I/O 密集型服务的瓶颈在下游,CPU 不高但延迟已经很高。可以通过 Prometheus Adapter 或 KEDA,基于 QPS、队列积压长度扩缩;
- 扩容 Pod 会放大下游压力:订单服务从 4 个扩到 40 个,数据库连接数也会增加 10 倍。maxReplicas 要结合下游容量来设置。
六、Service Mesh:解决什么问题,付出什么代价
6.1 它解决的问题
Service Mesh 把服务间通信的通用能力从应用代码中剥离,下沉到基础设施层:
| 能力 | 没有 Mesh 时 | 有 Mesh 时 |
|---|---|---|
| mTLS 加密与身份认证 | 每个服务自己管理证书,基本没人做 | 自动签发、轮换证书,基于身份授权 |
| 超时、重试、熔断 | 每种语言各自引入 SDK,配置分散 | 统一在代理层配置 |
| 金丝雀发布 | 依赖网关或注册中心的定制能力 | 按权重、按请求头路由 |
| 黄金指标与链路 | 依赖各语言埋点 | 代理层自动产生请求量、错误率、延迟 |
多语言技术栈 和零信任安全要求,是引入 Service Mesh 最有说服力的两个理由。如果团队是纯 Java 技术栈、已经用 Spring Cloud 解决了服务治理,引入 Mesh 的收益会小很多。
6.2 Istio 流量治理示例
yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: order-service
spec:
hosts: [order-service]
http:
- match:
- headers:
x-canary: { exact: "true" }
route:
- destination: { host: order-service, subset: v2 }
- route:
- destination: { host: order-service, subset: v1 }
weight: 90
- destination: { host: order-service, subset: v2 }
weight: 10
timeout: 2s
retries:
attempts: 2
perTryTimeout: 800ms
retryOn: connect-failure,refused-stream,unavailable
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: order-service
spec:
host: order-service
trafficPolicy:
connectionPool:
http:
http2MaxRequests: 1000
http1MaxPendingRequests: 100
outlierDetection: # 被动健康检查:剔除异常实例
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50 # 最多剔除一半实例,避免全部被剔除
subsets:
- name: v1
labels: { version: v1 }
- name: v2
labels: { version: v2 }
需要注意的地方:
- 重试只在一层做:如果应用里已经有 Resilience4j 或 Feign 重试,Mesh 层再配置重试,流量会成倍放大。迁移到 Mesh 时要同步移除应用层的重试配置;
retryOn不要包含5xx,非幂等的写请求(如下单)在服务端已经处理成功但返回 5xx 时,重试会导致重复下单;maxEjectionPercent要设置上限,否则下游整体故障时所有实例都被剔除。
6.3 代价
- 资源开销:Sidecar 模式下每个 Pod 多一个 Envoy 容器。根据 Istio 官方的性能测试数据,每 1000 RPS 大约消耗 0.2 vCPU 和数十 MB 内存,Pod 数量多的集群总开销可观;
- 延迟:每一跳多两次代理转发,官方测试 P90 延迟增加约在毫秒级;
- 启动顺序 :应用比 Sidecar 先启动时,启动阶段的出站请求会失败,需要配置
holdApplicationUntilProxyStarts: true;K8s 1.28+ 的原生 Sidecar 容器可以从根本上解决启动和退出顺序问题; - 排障复杂度 :一个 503 可能来自应用、本地 Envoy、对端 Envoy、控制面下发的配置,排查需要掌握
istioctl proxy-config、Envoy access log 等工具; - 控制面升级:Istio 版本迭代快,升级需要灰度(revision-based 升级),本身就是一项持续的运维工作。
Istio 的 Ambient 模式(1.24 起 GA)用节点级的 ztunnel 处理 L4 和 mTLS,按需部署 waypoint 代理处理 L7,不再为每个 Pod 注入 Sidecar,能显著降低资源开销和运维复杂度,新建集群值得优先评估。
6.4 引入 Mesh 前的自查
- 是否有多语言服务,或者有明确的服务间加密、零信任合规要求?
- 团队是否有人能读懂 Envoy 配置、能在 Mesh 出问题时定位?
- 是否愿意为 Sidecar 支付额外的资源成本?
- 现有的服务治理(Spring Cloud、Dubbo)是否已经足够?
如果前两个问题的答案都是否定的,建议暂缓引入。
七、交付:GitOps 与渐进式发布
- GitOps :所有 K8s 清单放在 Git 仓库,由 Argo CD 或 Flux 持续把集群状态同步到 Git 中声明的状态。好处是变更可审计、可回滚(回滚就是
git revert),且杜绝了手工kubectl edit导致的配置漂移; - 渐进式发布:Argo Rollouts 或 Flagger 可以与 Istio 配合,按"5% → 25% → 50% → 100%"逐步切流,每一步自动查询 Prometheus 的错误率和延迟,指标不达标时自动回滚;
- 镜像不可变 :镜像标签使用 Git commit SHA,不使用
latest,确保每次部署可追溯、可复现。
八、生产就绪检查清单
- 内存 requests = limits,JVM 使用
MaxRAMPercentage并为非堆内存留足空间 - 是否监控 CPU 限流比例,延迟敏感服务是否评估过去掉 CPU limits
- 配置了 startupProbe;liveness 不检查外部依赖
- preStop + 优雅停机 + 足够的
terminationGracePeriodSeconds - 滚动更新
maxUnavailable: 0,设置minReadySeconds - 副本跨节点、跨可用区分布,配置了 PDB 且不会卡住 drain
- HPA 设置了 behavior,maxReplicas 与下游容量匹配,大促前提前扩容
- 重试只在一层配置(应用层或 Mesh 层),且只重试幂等请求
- 部署清单纳入 Git 管理,镜像标签不可变
九、总结
云原生的核心不是"用上了 K8s",而是让应用的行为与平台的机制正确配合:
- 资源配置要理解 CFS 限流和 JVM 内存模型;
- 探针要区分"活着"和"能接流量",不让依赖故障放大为全面重启;
- 优雅发布要承认"摘流量"和"停进程"是并行发生的;
- 弹性伸缩是被动的,可预期峰值要提前准备;
- Service Mesh 是解决多语言和零信任问题的有力工具,但它的成本是实实在在的。
回到开头的四个现象:发布时 502,是因为没有 preStop 等待摘流量;节点故障整体掉线,是因为副本没有跨节点分布;流量上涨扩容不及时,是因为 HPA 本身是被动的;频繁 OOMKilled,是因为把堆大小等同于进程内存。每个现象背后都有一个明确的机制,理解机制比记住配置更重要。
如果你的服务之间通过消息队列协作,消费者的优雅停机与位点提交同样需要配合设计,可参考 事件驱动架构实战:可靠投递、幂等消费、消息顺序与MQ选型。
参考资料
- Kubernetes 文档:Pod Lifecycle(Pod 终止流程、探针)
- Kubernetes 文档:Resource Management for Pods and Containers
- Kubernetes 文档:Horizontal Pod Autoscaling(扩缩算法与 behavior)
- Kubernetes 文档:Disruptions(PodDisruptionBudget)
- Istio 文档:Performance and Scalability(Sidecar 资源与延迟开销)
- Istio 文档:Ambient Mode
- Spring Boot 文档:Kubernetes Probes
- Spring Boot 文档:Graceful Shutdown
- Argo Rollouts 文档(渐进式发布与自动分析)
- Brendan Burns 等,《Kubernetes 设计模式》