很多团队把 Kubernetes 当成“部署平台”,真正进了生产才会发现,它更像一个分布式系统操作系统。Pod 重启、节点漂移、滚动发布、自动扩缩容、服务发现、资源隔离、故障转移,这些能力如果只是“会配 YAML”,系统最多算是跑起来;只有把控制面原理、应用架构、容量治理、发布策略、观测体系和工程约束串成闭环,Kubernetes 才会从“容器平台”变成“自愈底座”。
本文不讲 Hello World,也不只讲几个 Deployment 配置片段,而是围绕一个真实感很强的生产故障案例,系统回答四个问题:
- Kubernetes 的自愈到底是怎么工作的,边界又在哪里
- 微服务在高并发场景下,怎样才能做到不因单点阻塞演化成全链路雪崩
- 生产级 K8s 应用应该怎么设计探针、扩缩容、发布、隔离和观测
- 一套“能跑”的系统,怎样演进成“扛峰值、可扩展、可回滚、可治理”的工程体系
为了让讨论足够具体,全文统一使用一个电商供应链场景作为主线:
商品服务、库存调度服务、订单履约服务、物流服务和风控服务运行在 Kubernetes 集群中。日常订单处理量在百万级,大促峰值入口流量会放大 8 到 12 倍,库存锁定接口是全链路里最敏感的核心路径。
一、从一次真实的雪崩说起
凌晨两点,值班电话响起。大盘上 CPU 不高、节点负载正常、Pod 也没大面积重启,但商品上下架失败、订单创建超时、履约回调积压,业务已经基本不可用。
复盘这次事故,现场现象很典型:
inventory-scheduler 平均 RT 从 40ms 飙升到 8s 以上
- 数据库连接池耗尽,请求线程大量阻塞
- 上游
order-service 因同步调用超时被拖死,Tomcat 工作线程打满
- Kafka 消费 lag 持续累积,异步补偿链路也逐步失效
- HPA 没有及时扩容,因为 CPU 使用率并不高
- Liveness Probe 误杀了一批本就繁忙的实例,放大了抖动
这类故障最值得警惕的一点在于:
集群没坏,机器没满,Pod 也没全挂,但系统已经不可用。
根因通常不在单一组件,而在“基础设施自愈”和“业务架构韧性”之间断层了。把事故拆开,基本会落到下面几类问题。
1.1 第一层问题:把同步调用链做得太长
很多业务一开始图省事,接口设计是这样的:
- 订单服务同步调用库存锁定
- 库存服务同步访问数据库
- 库存服务同步写缓存
- 库存服务同步发消息
- 全部成功才返回
这条链路在低流量时很顺,在峰值时就会变成串联放大器。只要其中一个环节响应变慢,所有上游线程都会排队,最终形成线程池耗尽、连接池耗尽、队列积压和超时重试风暴。
1.2 第二层问题:把 K8s 自愈误解成系统自愈
Kubernetes 可以做这些事:
- Pod 进程退出后自动拉起
- 节点宕机后把副本调度到别处
- 根据指标自动增减实例
- 在发布时渐进替换旧版本
但 Kubernetes 做不了这些事:
- 理解你的数据库连接池已经打满
- 理解你的线程池已经阻塞
- 理解某个外部依赖出现高延迟,需要限流或熔断
- 理解你的业务必须降级为“只读库存”而不是继续强推写请求
也就是说,K8s 的自愈是基础设施层自愈,不是业务层自愈。如果应用本身没有隔离、熔断、超时、削峰、补偿和降级设计,Pod 再多、节点再健康,也只是把错误复制得更快。
1.3 第三层问题:监控只看资源,不看系统状态
很多团队的观测体系只有:
这远远不够。真正能提前暴露风险的往往是:
- 数据库连接池使用率
- 线程池活跃线程数和队列长度
- 外部依赖超时率
- Kafka lag
- 接口 P95/P99 延迟
- 熔断器打开次数
- 限流丢弃请求数
- 每个下游依赖的错误分布
所以这篇文章的目标不是“把 K8s 配起来”,而是把一个生产系统从“容易崩”升级到“可观测、可隔离、可恢复、可治理”。
二、先把底层原理讲透:Kubernetes 为什么能自愈
理解 Kubernetes,最重要的不是记对象,而是理解它的工作模型:
声明式期望状态 + 控制循环 + 最终一致性收敛
2.1 声明式的本质:你描述终态,不描述步骤
例如你提交一个 Deployment:
spec:
replicas: 4
template:
spec:
containers:
- name: inventory-scheduler
image: registry.example.com/inventory-scheduler:v2.4.0
你并没有告诉 Kubernetes:
- 先创建哪台机器
- 先拉哪个镜像
- 先启动哪一个 Pod
- 挂了以后怎么补
你只是声明:“我希望永远有 4 个符合模板的 Pod 存在。”
接下来由控制器持续观察实际状态和期望状态之间的差异,然后不断收敛。这就是 Kubernetes 整套系统的核心哲学。
2.2 控制面的工作链路
从工程视角看,一次“提交 YAML 到运行成功”的链路大致如下:
kubectl/apply
|
v
API Server
|
v
etcd (存储期望状态)
|
v
Controller Manager --------> 各类 Controller 持续对账
|
v
Scheduler 为待调度 Pod 选择 Node
|
v
Kubelet 在目标节点创建容器
|
v
Container Runtime 拉镜像并启动进程
这里有三个关键角色。
2.3 API Server:控制面的统一入口
API Server 的职责是:
- 接收所有声明式请求
- 做认证、鉴权、准入校验
- 把对象持久化到 etcd
- 对外提供 watch 机制,让控制器感知对象变更
它不是“配置中心”那么简单,而是整个控制面的事务入口。
2.4 Controller:持续对账的自动化执行者
Controller 的逻辑几乎都可以抽象成一句话:
如果实际状态和期望状态不一致,就采取动作让它们重新一致。
例如:
- Deployment Controller 发现副本少了,就补 Pod
- Node Controller 发现节点失联,就驱逐副本
- EndpointSlice Controller 发现 Pod Ready 了,就把它加入服务后端
- HPA Controller 发现指标超过阈值,就调整副本数
这就是 Kubernetes 自愈的本质。它不是“发现异常就报警”,而是“发现偏差就尝试纠偏”。
2.5 Scheduler:不是“找空闲机器”那么简单
调度过程通常分成两步:
- Filter:先筛掉不满足约束的节点
- Score:再对可选节点打分,选最优节点
Filter 关注的是“能不能放”:
- CPU/内存/GPU 是否足够
- taint/toleration 是否匹配
- nodeSelector、nodeAffinity 是否满足
- volume 是否能挂载
- topology spread 是否满足
Score 关注的是“放哪更合适”:
- 资源是否均衡
- 是否和已有副本过于集中
- 是否更接近依赖
- 是否满足亲和性或反亲和性偏好
如果你把调度理解成“随机选个节点”,很多生产事故都解释不通。
2.6 Kubelet 与探针:自愈链路的最后一跳
Kubelet 是节点上的执行代理,负责:
- 感知 Node 和 Pod 状态
- 调用容器运行时启动/停止容器
- 执行 liveness/readiness/startup probe
- 上报状态给 API Server
很多团队的误区是把探针当成“有就行”的模板配置。实际上探针直接决定了:
- 一个实例何时对外接流量
- 一个繁忙实例是否会被误判成故障
- 应用冷启动时是否会被过早拉流量
- 发布和扩容时是否会引入瞬时错误
探针配置错了,自愈就会变成“自杀”。
三、Kubernetes 的边界:为什么它不能替你解决架构问题
这一节很关键,因为很多架构误判都来自能力边界不清。
3.1 K8s 解决的是基础设施编排,不是业务复杂度
Kubernetes 擅长的是:
- 统一调度与生命周期管理
- 资源隔离与多租户承载
- 服务发现与网络抽象
- 发布、回滚和自动扩缩容
- 声明式运维与平台标准化
Kubernetes 不擅长的是:
- 业务事务补偿
- 跨服务一致性
- 接口幂等设计
- 数据模型设计
- 调用链治理
- 慢 SQL 优化
- 线程池/连接池治理
所以正确的工程方法应该是:
让 Kubernetes 负责“把系统稳定地运行起来”,让应用架构负责“即使依赖波动也不至于雪崩”。
3.2 一张图看清“平台韧性”和“业务韧性”
┌───────────────────────────────┐
│ 业务韧性(应用负责) │
│ 超时、重试、幂等、熔断、降级 │
│ 削峰填谷、补偿、异步解耦 │
└───────────────────────────────┘
↑
│
┌───────────────────────────────┐
│ 平台韧性(K8s/平台负责) │
│ 调度、自愈、扩缩容、发布、隔离 │
│ 观测、资源治理、集群高可用 │
└───────────────────────────────┘
只有上下两层一起成立,系统才真的稳。
四、生产级总体架构:从“服务部署”升级为“系统工程”
下面给出一套适合高并发供应链场景的推荐架构。重点不是具体产品必须一模一样,而是每一层的职责要清晰。
┌──────────────────────┐
│ CDN / WAF / SLB │
└──────────┬───────────┘
│
v
┌──────────────────────┐
│ API Gateway / Ingress│
│ APISIX / Nginx │
└──────────┬───────────┘
│
┌─────────────┴─────────────┐
│ │
v v
┌────────────────────┐ ┌────────────────────┐
│ 核心同步服务集群 │ │ 异步消费服务集群 │
│ order/inventory │ │ stock-event-worker │
└─────────┬──────────┘ └─────────┬──────────┘
│ │
v v
┌────────────────────┐ ┌────────────────────┐
│ Service Mesh │ │ Kafka / RocketMQ │
│ timeout/retry/cb │ │ 削峰、解耦、重放 │
└─────────┬──────────┘ └────────────────────┘
│
┌─────────┼───────────────────────────────────┐
│ │ │
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Redis │ │ MySQL │ │ ES/OLAP │
│ cache │ │ Tx data │ │ analytics│
└──────────┘ └──────────┘ └──────────┘
│
v
┌──────────────────────────────────────┐
│ Kubernetes Cluster │
│ HPA / PDB / Affinity / NetPolicy │
│ Argo CD / Prometheus / Loki / Tempo │
└──────────────────────────────────────┘
4.1 这套架构的核心原则
- 同步链路只保留最必要的强一致操作
- 非关键后处理能力尽量异步化
- 核心服务必须有熔断、限流和隔离
- 发布策略、扩缩容策略和容量模型要配套设计
- 业务指标、依赖指标、资源指标三类观测同时建立
4.2 为什么库存锁定要拆成“同步最小闭环 + 异步扩展链路”
库存锁定接口最常见的错误是“顺手做太多事”:
- 锁库存
- 记录操作日志
- 写审计
- 发消息
- 刷缓存
- 通知风控
- 同步第三方仓储
正确思路应该是:
- 同步链路只做最小必要动作
- 后续扩展行为通过可靠事件异步触发
这样才能在峰值时把 RT 和故障半径控制住。
五、技术选型建议:关注的是职责匹配,不是流行度
| 领域 |
推荐方案 |
适用原因 |
| 集群发行版 |
原生 Kubernetes 1.29+ 或云厂商托管版 |
稳定、生态完整、升级路径清晰 |
| Ingress / Gateway |
APISIX、Nginx Ingress、Gateway API |
入口治理成熟,支持限流、鉴权、灰度 |
| 服务治理 |
Istio 或基于 Gateway/Sidecar 的轻量治理 |
适合做超时、重试、熔断和金丝雀 |
| 消息队列 |
Kafka + Operator |
高吞吐、可重放、适合事件驱动解耦 |
| 缓存 |
Redis Cluster |
热点保护、读写削峰 |
| 数据库 |
MySQL 主从/分片或云数据库 |
主交易事实存储仍建议落在 RDBMS |
| 观测 |
Prometheus + Grafana + Loki + Tempo/Jaeger |
指标、日志、链路统一 |
| GitOps |
Argo CD / Flux |
收敛配置漂移,支持回滚 |
| 节点弹性 |
Karpenter / Cluster Autoscaler |
峰值扩容和成本治理 |
| CNI |
Cilium |
eBPF 能力强,NetworkPolicy 和观测更细 |
如果团队尚未具备 Service Mesh 经验,也可以先在应用层实现 Resilience4j,再逐步把一部分治理能力下沉到网格层,不必一开始就“大一统”。
六、生产级 YAML:不是能部署就算完
这一节给出一套接近生产实践的库存调度服务配置。重点不是字段多少,而是每一项配置都服务于某个工程目标。
6.1 Deployment:把可用性、启动行为和调度约束写清楚
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-scheduler
namespace: supply-chain
labels:
app: inventory-scheduler
tier: core
spec:
replicas: 4
revisionHistoryLimit: 10
minReadySeconds: 20
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: inventory-scheduler
template:
metadata:
labels:
app: inventory-scheduler
version: v2-4-0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
serviceAccountName: inventory-scheduler
terminationGracePeriodSeconds: 60
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: inventory-scheduler
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: inventory-scheduler
containers:
- name: inventory-scheduler
image: registry.example.com/supply-chain/inventory-scheduler:v2.4.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:+UseContainerSupport
-XX:InitialRAMPercentage=40.0
-XX:MaxRAMPercentage=70.0
-XX:+ExitOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
- name: DB_URL
valueFrom:
secretKeyRef:
name: inventory-db-secret
key: url
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: inventory-db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: inventory-db-secret
key: password
- name: REDIS_ADDR
value: "redis-cluster.supply-chain.svc.cluster.local:6379"
- name: KAFKA_BOOTSTRAP_SERVERS
value: "kafka-bootstrap.kafka.svc.cluster.local:9092"
startupProbe:
httpGet:
path: /actuator/health/startup
port: 8080
periodSeconds: 10
failureThreshold: 18
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 15"
6.2 Service、PDB 和 HPA:保证流量接入和副本稳定
---
apiVersion: v1
kind: Service
metadata:
name: inventory-scheduler
namespace: supply-chain
spec:
selector:
app: inventory-scheduler
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: inventory-scheduler-pdb
namespace: supply-chain
spec:
minAvailable: 3
selector:
matchLabels:
app: inventory-scheduler
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: inventory-scheduler
namespace: supply-chain
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: inventory-scheduler
minReplicas: 4
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 60
- type: Pods
value: 4
periodSeconds: 60
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120
这里有几个真正影响生产效果的点:
startupProbe 把冷启动阶段和存活检测分离,避免慢启动应用被误杀
minReadySeconds 防止刚就绪就立刻参与滚动替换,降低抖动
preStop + terminationGracePeriodSeconds 给摘流和在途请求留时间
PDB 避免节点维护、升级、弹性收缩时把关键副本一次性赶下线
HPA behavior 显式约束扩缩容节奏,避免抖动
6.3 VPA 的正确姿势:先观察,再自动
很多团队一上来就把 VPA 设为 Auto,结果高峰期反复重启。更稳妥的方式是先用 Off 模式收集建议值。
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: inventory-scheduler
namespace: supply-chain
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: inventory-scheduler
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: inventory-scheduler
minAllowed:
cpu: "500m"
memory: "1Gi"
maxAllowed:
cpu: "4"
memory: "8Gi"
经验上:
- HPA 管副本数
- VPA 先做推荐
- 如果要自动调资源,尽量和 HPA 用不同维度指标,避免控制器打架
6.4 NetworkPolicy:把“默认全通”改成“最小可用”
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: inventory-scheduler-policy
namespace: supply-chain
spec:
podSelector:
matchLabels:
app: inventory-scheduler
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: order-service
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: supply-chain
podSelector:
matchLabels:
app: redis-cluster
ports:
- protocol: TCP
port: 6379
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kafka
ports:
- protocol: TCP
port: 9092
在大多数真实集群里,安全问题不是来自“有没有高深攻击”,而是来自“服务之间默认全通”。
七、应用层必须跟上:生产级代码不是把接口写通
Kubernetes 只解决“进程如何运行”,并不保证你的服务在高并发下稳定。下面给出一个更接近生产的库存锁定设计。
7.1 先看错误示范:同步大而全接口
public LockResult lockStock(LockRequest request) {
inventoryRepository.lock(request);
cacheService.refresh(request.getSkuId());
kafkaTemplate.send("stock-events", request);
auditService.record(request);
return LockResult.success();
}
这段代码的问题不是“风格不好”,而是把数据库、缓存、消息和审计全部绑在一条同步链路里,任何一个依赖抖动都会扩大故障面。
7.2 改造目标:同步做强一致闭环,异步做扩展行为
更稳妥的分层应该是:
- 同步链路:参数校验、幂等校验、库存扣减、事务落库、Outbox 事件落表
- 异步链路:发 MQ、刷新缓存、审计、风控、下游通知
7.3 生产级 Spring Boot 示例
下面是一段简化但具备生产思路的代码。它展示了幂等、防重、事务边界、超时控制和事件解耦。
package com.example.inventory.application;
import com.example.inventory.domain.InventoryLockCommand;
import com.example.inventory.domain.InventoryLockResult;
import com.example.inventory.domain.InventoryService;
import com.example.inventory.domain.OutboxEvent;
import com.example.inventory.infrastructure.IdempotencyRepository;
import com.example.inventory.infrastructure.OutboxRepository;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import jakarta.validation.Valid;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class InventoryApplicationService {
private final InventoryService inventoryService;
private final IdempotencyRepository idempotencyRepository;
private final OutboxRepository outboxRepository;
@Transactional
@CircuitBreaker(name = "inventoryLock")
@Bulkhead(name = "inventoryLock")
public InventoryLockResult lock(@Valid InventoryLockCommand command) {
String idemKey = command.orderNo() + ":" + command.skuId();
InventoryLockResult cached = idempotencyRepository.findResult(idemKey);
if (cached != null) {
return cached;
}
InventoryLockResult result = inventoryService.lock(command);
idempotencyRepository.saveResult(idemKey, result);
OutboxEvent event = new OutboxEvent(
UUID.randomUUID().toString(),
"inventory.locked",
command.orderNo(),
result.toJson(),
Instant.now()
);
outboxRepository.save(event);
return result;
}
@TimeLimiter(name = "inventoryQuery")
public CompletableFuture<Integer> queryAvailableStock(String skuId) {
return CompletableFuture.supplyAsync(() -> inventoryService.availableStock(skuId));
}
}
这段代码背后的工程思想有四个。
第一,幂等不是可选项。订单系统和消息系统都会重试,没有幂等,库存一定出错。
第二,Outbox 模式把“本地事务成功”和“事件可靠发送”拆成两个阶段,避免数据库提交成功但消息没发出去。
第三,CircuitBreaker、Bulkhead、TimeLimiter 不是装饰品,它们决定了下游变慢时是不是会把整个线程池拖死。
第四,同步链路要短,能异步的一律异步。
7.4 Outbox 消费者:可靠发送而不是“发一下试试”
package com.example.inventory.worker;
import com.example.inventory.infrastructure.OutboxEvent;
import com.example.inventory.infrastructure.OutboxRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class OutboxPublisher {
private final OutboxRepository outboxRepository;
private final KafkaTemplate<String, String> kafkaTemplate;
@Scheduled(fixedDelayString = "${inventory.outbox.publish-interval-ms:500}")
public void publish() {
List<OutboxEvent> events = outboxRepository.findTop100Unpublished();
for (OutboxEvent event : events) {
try {
kafkaTemplate.send("inventory-events", event.aggregateId(), event.payload()).get();
outboxRepository.markPublished(event.id());
} catch (Exception ex) {
log.warn("publish outbox failed, eventId={}", event.id(), ex);
outboxRepository.increaseRetry(event.id(), ex.getMessage());
}
}
}
}
在生产环境里,这里通常还会继续补上:
- 分批拉取
- 重试退避
- 死信队列
- 最大重试次数
- 告警阈值
- 发布延迟监控
7.5 线程池和连接池:高并发系统最容易被忽视的水位线
很多高并发问题根本不是 CPU 不够,而是池子打满了。
一个更可靠的配置思路如下:
server:
tomcat:
threads:
max: 400
min-spare: 40
accept-count: 1000
spring:
datasource:
hikari:
maximum-pool-size: 80
minimum-idle: 20
connection-timeout: 800
validation-timeout: 500
leak-detection-threshold: 5000
resilience4j:
bulkhead:
instances:
inventoryLock:
max-concurrent-calls: 120
max-wait-duration: 10ms
thread-pool-bulkhead:
instances:
inventoryQuery:
core-thread-pool-size: 16
max-thread-pool-size: 32
queue-capacity: 200
这里的重点不是数值本身,而是要有一套容量关系模型。例如:
八、服务治理:高并发下不做隔离,扩容只会把问题放大
8.1 为什么“超时、重试、熔断、限流”必须成套出现
只配重试,不配超时,结果是请求越积越多。
只配超时,不配熔断,结果是每次都慢性拖死。
只配熔断,不配限流,结果是入口流量仍会压垮剩余健康实例。
只配限流,不配降级,结果是业务只能直接失败。
正确顺序通常是:
- 先设超时,确保请求不会无限等待
- 再设隔离和并发上限,限制故障传播
- 再设熔断,快速切断不健康依赖
- 最后配重试,而且只对明确可重试场景生效
8.2 Istio DestinationRule 示例
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: inventory-scheduler
namespace: supply-chain
spec:
host: inventory-scheduler.supply-chain.svc.cluster.local
trafficPolicy:
connectionPool:
tcp:
maxConnections: 200
connectTimeout: 300ms
http:
http1MaxPendingRequests: 100
maxRequestsPerConnection: 50
maxRetries: 2
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50
8.3 VirtualService 灰度发布示例
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: inventory-scheduler
namespace: supply-chain
spec:
hosts:
- inventory-scheduler
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: inventory-scheduler
subset: v2
- route:
- destination:
host: inventory-scheduler
subset: v1
weight: 90
- destination:
host: inventory-scheduler
subset: v2
weight: 10
对于核心服务,最稳妥的上线顺序通常是:
- 先按 header 或内部流量做定向验证
- 再从 1% 到 5% 到 10% 小流量放量
- 观察错误率、RT、连接池和 JVM 指标
- 满足阈值后继续放量,否则立即回退
九、扩缩容不能只看 CPU:高并发系统更要关注业务指标
很多实际故障里,CPU 并不高,但系统已经很慢了。原因是瓶颈可能在:
- 数据库连接池
- Redis RT
- 外部依赖
- JVM GC
- 下游线程池
- 网关排队
所以对核心链路,更推荐让 HPA 同时参考业务指标。
9.1 基于自定义指标的 HPA 思路
例如库存调度服务可以参考:
- 单实例 QPS
- P95 RT
- 每秒锁库存请求数
- Kafka lag
示意配置如下:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: inventory-scheduler-by-qps
namespace: supply-chain
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: inventory-scheduler
minReplicas: 4
maxReplicas: 30
metrics:
- type: Pods
pods:
metric:
name: http_server_requests_per_second
target:
type: AverageValue
averageValue: "120"
9.2 容量模型要先于 HPA
不要把 HPA 当成万能救火器。它只是自动执行策略,不替你做容量建模。
一个基础容量模型至少要回答:
- 单实例稳定 QPS 上限是多少
- 单实例在 P95 目标下的最大并发是多少
- 数据库和缓存能支撑多少并发扇出
- 峰值会持续多久
- 扩容需要多少时间完成
- 是否需要预热实例
举例来说,如果一个实例在稳定状态下可承受 100 QPS,峰值预计 1200 QPS,那么副本数不是简单设成 12,而要考虑:
- 发布中的冗余
- 节点故障时的冗余
- HPA 触发滞后
- 冷启动期间不可用副本
工程上通常会再加 20% 到 40% 的安全余量。
十、可观测性升级:从“监控机器”到“监控系统”
一套成熟的云原生观测体系通常分三层。
10.1 第一层:资源指标
- Node CPU、内存、磁盘、网络
- Pod 重启次数
- 容器 OOMKilled
- 节点不可调度数
10.2 第二层:应用指标
- 请求量、成功率、错误率
- P50/P95/P99 延迟
- 线程池活跃数、队列长度
- 连接池占用率
- JVM GC、堆外内存、类加载数
- MQ lag
10.3 第三层:业务指标
- 库存锁定成功率
- 订单创建成功率
- 库存回补延迟
- 幂等冲突率
- 补偿任务积压量
真正能指导故障定位的,是把三层数据串起来。
10.4 Prometheus 告警规则示例
groups:
- name: inventory-alerts
rules:
- alert: InventorySchedulerHighLatency
expr: histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{uri="/api/inventory/lock"}[5m])) by (le)) > 0.3
for: 3m
labels:
severity: critical
annotations:
summary: "inventory-scheduler P95 latency > 300ms"
- alert: InventoryDbPoolNearlyExhausted
expr: hikari_connections_active / hikari_connections_max > 0.85
for: 2m
labels:
severity: warning
annotations:
summary: "database pool usage exceeds 85%"
- alert: InventoryKafkaLagHigh
expr: kafka_consumergroup_lag{consumergroup="inventory-worker"} > 10000
for: 5m
labels:
severity: critical
annotations:
summary: "inventory worker lag is too high"
好的告警应该满足三件事:
-
能说明影响范围
-
能提示最可能的瓶颈
-
能触发明确动作,而不是只会制造焦虑
-
十一、发布策略升级:从滚动更新到可验证灰度
很多团队以为 kubectl apply 就算发布,这在低风险系统里也许够,在核心链路里远远不够。
11.1 四种常见发布方式
| 方式 |
优点 |
风险 |
| Recreate |
简单 |
有中断,不适合核心服务 |
| RollingUpdate |
默认方案,成本低 |
新版本问题可能逐步扩散 |
| Blue/Green |
回滚快,切换清晰 |
资源成本较高 |
| Canary |
风险最可控 |
需要流量治理和观测配合 |
11.2 核心服务推荐策略
对库存、支付、订单这类服务,更推荐:
- 蓝绿作为大版本切换兜底
- 金丝雀作为小流量验证主手段
- 发布前先跑合成流量和只读探测
- 发布期间观测错误率、延迟、连接池和业务指标
- 回滚标准要提前写死,不能靠临场判断
11.3 一个简单但有效的回滚标准
例如可以明确:
- 5 分钟内 P95 延迟升高超过 30%
- 错误率高于基线 2 倍
- 数据库连接池持续超过 90%
- 订单创建成功率低于 99.5%
满足任一条件立即暂停放量并回滚。
十二、配置管理与 GitOps:别让集群越跑越分叉
当环境从 1 个集群变成 5 个、10 个以后,真正难的不是部署,而是防止配置漂移。
12.1 为什么手工改集群一定会失控
常见场景:
- 线上临时
kubectl edit
- 某个集群单独改了资源配额
- 某个命名空间多了一份“临时修复版” ConfigMap
- 测试环境和生产环境的探针、HPA、PDB 逐渐不一致
久而久之,你会发现每个集群都“差不多”,但没有两个集群真的一样。
12.2 推荐做法:Git 是唯一真相源
一个简化目录结构示意如下:
platform-gitops/
├── base/
│ ├── inventory-scheduler/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── hpa.yaml
│ │ └── pdb.yaml
├── overlays/
│ ├── test/
│ ├── staging/
│ └── prod/
└── applications/
└── argocd/
建议原则:
-
基础配置抽到 base
-
环境差异放 overlay
-
不允许绕过 Git 直接改生产
-
所有变更都要能追溯到 PR
-
十三、常见生产坑与治理建议
13.1 OOMKilled:不是“加内存”这么简单
常见原因包括:
- JVM 堆设置和容器 limit 不匹配
- 堆外内存、DirectBuffer、线程栈没预留
- 批处理对象瞬时堆积
- 业务缓存没有上限
治理建议:
- 显式配置
MaxRAMPercentage
- 对大对象和批量任务做限流
- 把 Heap Dump 和 OOM 告警打通
- 用 p95/p99 而不是平均值做资源评估
13.2 Probe 误杀
常见错误:
- 启动很慢却没有
startupProbe
livenessProbe 依赖数据库或外部接口
- 把暂时繁忙当成“进程死亡”
治理建议:
- 存活探针只判断进程是否卡死
- 就绪探针判断是否可以接流量
- 启动探针单独覆盖冷启动窗口
13.3 HPA 抖动
表现通常是副本来回扩缩,业务波动反而更大。治理建议:
- 扩容窗口短一点,缩容窗口长一点
- 不要只看 CPU
- 为核心服务保留基线副本
- 冷启动慢的应用要做预热
13.4 数据库才是真瓶颈,但扩的是应用 Pod
这类问题特别常见。应用 Pod 翻倍了,数据库连接池和 IO 没变,结果只是更快把数据库打满。治理建议:
- 先做链路容量短板识别
- 对数据库操作做缓存、批量、索引优化
- 对热点库存做分片或令牌化削峰
13.5 配置中心和 Secret 管理混乱
建议把:
-
非敏感业务配置放 ConfigMap
-
口令、证书、密钥放 Secret 或外部密钥系统
-
加密材料统一轮转
-
应用不要把明文密码打到日志里
-
十四、AI/批处理工作负载:为什么默认调度器有时不够
虽然本文主线是微服务,但现在很多企业会在同一个 K8s 平台承载在线服务、离线批处理和 AI 作业,所以这部分必须补一笔。
默认调度器擅长长期运行的无状态服务,但在分布式训练、批量作业场景下,会面临两个问题:
- Pod 之间存在强协同,需要成组启动
- GPU、NUMA、拓扑亲和会显著影响性能
这时就需要像 Volcano 这样的批调度能力。
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: llm-training
spec:
minAvailable: 8
schedulerName: volcano
queue: ai-training
tasks:
- name: master
replicas: 1
template:
spec:
containers:
- name: trainer
image: registry.example.com/ai/torch-trainer:2.1
resources:
limits:
nvidia.com/gpu: 4
- name: worker
replicas: 7
template:
spec:
containers:
- name: trainer
image: registry.example.com/ai/torch-trainer:2.1
resources:
limits:
nvidia.com/gpu: 4
minAvailable 的意义就在于:资源不够就别半吊子启动,避免一部分 Pod 启动成功、另一部分永远排队,最终谁都跑不起来。
十五、从单体到云原生的落地路线图
如果你的系统还在从传统架构向 K8s 迁移,不建议一步到位做成“全家桶”。更现实的路径通常是分阶段推进。
阶段 1:容器化标准化
- 收敛 Dockerfile
- 统一日志输出和健康检查
- 建立镜像构建与漏洞扫描流程
阶段 2:无状态服务先上 K8s
- Deployment、Service、Ingress 跑通
- ConfigMap、Secret、Probe、HPA 基础能力到位
- 接入 Prometheus 和日志系统
阶段 3:核心链路治理
- 识别关键同步链路
- 引入超时、熔断、限流、隔离
- 事件驱动解耦
- 补齐幂等和补偿
阶段 4:发布与运维体系化
- 金丝雀发布
- GitOps
- 容量模型
- 混沌演练
- 自动化回滚
阶段 5:多集群与多负载统一治理
-
多环境一致性
-
多 AZ 容灾
-
批处理和 AI 统一接入
-
成本治理和资源分级
-
十六、一次完整的故障处置闭环应该长什么样
为了把前面的能力串起来,最后给出一个更接近实战的处置闭环。
假设大促期间库存锁定接口抖动,推荐动作顺序如下:
- 先确认业务影响面:订单成功率、库存锁定成功率、入口错误率
- 再看瓶颈位置:数据库连接池、线程池、Redis RT、Kafka lag
- 如果下游明显变慢,立即启用入口限流和依赖熔断
- 暂停非关键异步消费者,给核心链路让资源
- 检查 HPA 是否扩容到位,必要时手工扩容
- 如果新版本刚发布,按预设阈值快速回滚
- 故障稳定后回放积压消息并核对补偿结果
- 复盘时更新探针、阈值、容量和演练剧本
真正成熟的系统,不是“不出故障”,而是:
-
故障来时能快速止血
-
止血后能快速恢复
-
恢复后能把经验沉淀成平台能力
-
十七、文章小结:从崩溃到自愈,靠的不是某一个 YAML
Kubernetes 真正的价值,从来不只是“把容器跑起来”。它的核心能力是把分布式系统的运行、调度、发布、恢复和治理抽象成统一的控制平面,让团队可以用声明式方式管理复杂系统。
但要把这种能力转化成生产稳定性,必须补上另外半张图:
- 架构上,要缩短同步链路,强化异步解耦
- 工程上,要配置好探针、PDB、HPA、反亲和和网络隔离
- 应用上,要补齐超时、熔断、幂等、限流、补偿和线程池治理
- 运维上,要建立 GitOps、灰度发布、容量模型和观测体系
- 组织上,要通过故障演练和复盘,把经验固化为平台标准
一句话总结:
Pod 会重启,节点会漂移,流量会波动,版本会回滚,但一个系统能否真正从崩溃走向自愈,最终取决于你是否把“平台韧性”和“业务韧性”一起建设起来。
如果只把 Kubernetes 当成部署工具,它最多帮你把故障恢复得快一点;如果把它和架构治理、发布治理、容量治理、观测治理一起做,它才会成为企业分布式系统真正的工程底座。