主题
微服务治理实战:灰度、混沌与 K8s 落地
本文是微服务架构系统学习系列的 L3 实战篇。前置:28. 注册发现与配置中心、32. 可观测性三支柱。 学完可以配合面试题食用:微服务发布与灰度、服务网格与 Istio、K8s 基础
为什么需要微服务治理
微服务拆完之后,服务数从个位涨到几十上百,单靠人工运维已经管不过来。一个改配置、一个发布顺序错、一个依赖超时,都可能引发雪崩。治理就是把这套"管不过来"的事变成自动化的规则和流程——灰度发布让你敢上线,混沌工程让你知道上线会出什么问题,K8s 把这些规则变成声明式配置。
三个治理维度各自解决不同痛点:发布防故障扩散,混沌提前暴露隐患,平台把前两者落地到基础设施。
发布策略谱系:从滚动到灰度
生产环境的上线不能停服,也不能一口气全换。常见发布策略按风险递增排列:
mermaid
graph LR
A[滚动更新] --> B[蓝绿部署]
B --> C[灰度/金丝雀]
C --> D[全链路灰度]滚动更新(Rolling Update):分批替换实例,每批就绪再下一批。K8s Deployment 默认策略,简单但缺乏精准控制——新版本有 bug 可能已经污染了一批流量。
蓝绿部署(Blue-Green):同时维护两套完整环境,切流量时一次性从蓝切到绿。回滚快(切回 DNS 即可),但资源成本翻倍,且数据库兼容性问题容易被忽略。
灰度/金丝雀(Canary):只让一小部分流量(比如 5% 的用户/请求)走新版本,观察一段时间无异常后再全量。流量切分有两种实现方式:
- 网关权重:Nginx / Spring Cloud Gateway 按 weight 分配流量到不同后端
- 标签路由:根据请求头 / Cookie / 用户 ID 哈希路由到指定版本,精确到用户级别
灰度发布的核心是可观测性+快速回滚——灰度期间如果错误率上升或 P50 延迟翻倍,必须能在 1 分钟内切回旧版本。
全链路灰度:流量标记的透传
单服务灰度容易,但一次发布可能涉及 A->B->C 三条链路。如果 A 灰度了,B 和 C 还是旧版,A 的新请求打到 B 的旧接口可能协议不兼容。
全链路灰度的解决思路:给请求打标,标记沿调用链透传,每个服务根据标记路由到对应版本。
mermaid
sequenceDiagram
participant GW as 网关
participant A as 服务A(v2)
participant B as 服务B(v2)
participant C as 服务C(v1)
Note over GW: 请求头 x-version=gray
GW->>A: x-version=gray
A->>B: 传递 x-version=gray
B->>C: 传递 x-version=gray
C-->>B: 返回(v1响应)
B-->>A: 返回
A-->>GW: 返回实现步骤:
- Nacos 元数据标记:在 Nacos 注册时给实例打标签,如
version=gray、version=base - 网关层选择:Spring Cloud Gateway 根据请求头(
x-version: gray)路由到对应标签的服务实例 - Feign 拦截器传递:用
RequestInterceptor把灰度标记从上游请求复制到下游 RPC 调用 - 下游按标签路由:每个服务拿到标记后,从 Nacos 拉取对应标签的实例列表
这样一条请求从网关到最底层,全程走灰度版本;没有标记的请求默认走 base 版本。
混沌工程:主动找故障
混沌工程不是随便搞破坏,而是有假设地验证系统韧性。三步走:
- 稳态假设:定义系统正常状态(如 P99 延迟 < 200ms、错误率 < 0.1%)
- 注入故障:在可控范围内模拟真实故障(CPU 飙高、网络延迟、杀 Pod)
- 验证假设:故障期间的稳态指标是否达标?不达标说明系统有薄弱点
ChaosBlade 常用场景:
bash
# CPU 满载(模拟计算密集型服务被攻击)
blade create cpu load --cpu-percent 90 --timeout 60
# 网络延迟 3000ms(模拟跨机房网络抖动)
blade create network delay --time 3000 --interface eth0
# 杀指定 Pod(模拟节点宕机)
blade create k8s pod-kill --namespace prod --labels app=order-service混沌工程的关键不是"能扛住所有故障",而是建立故障恢复的预期——比如"单个 Pod 被杀后 30 秒内应恢复",如果实际恢复用了 2 分钟,说明就绪探针或 HPA 配置有问题。
K8s 落地:探针与优雅停机
微服务跑在 K8s 上,最基本的治理是让 K8s 知道服务"活没活"。
三种探针:
livenessProbe:检查容器是否还活着(死锁了返回失败,K8s 重启容器)readinessProbe:检查服务是否准备好接收流量(刚启动或正在重载配置时返回失败,K8s 从 Service 摘掉该 Pod)startupProbe:慢启动服务专用,给初始化留足时间
优雅停机流程:
K8s 发 SIGTERM → 服务拦截信号 → 关闭新请求入口 → 处理完进行中请求(wait 30s)→ 退出yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: order-service
image: order-service:v2
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 15
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"]配置要点:
readinessProbe周期短(5s),检测快;livenessProbe周期长(15s),避免误杀preStop sleep 30:给 K8s 从 Service Endpoints 中移除 Pod 的时间,防止在 SIGTERM 瞬间还有流量打进来- Spring Boot 的
spring.lifecycle.timeout-per-shutdown-phase=30s配合server.shutdown=graceful
治理度量:用数据驱动优先级
治理不是一次性做完,是个持续投入的过程。用四类指标定优先级:
| 指标 | 含义 | 什么算好 |
|---|---|---|
| 变更失败率 | 发布导致故障的比例 | < 15% |
| MTTR(平均修复时间) | 从故障到恢复 | < 30 min |
| 回滚时长 | 确认回滚到完成 | < 5 min |
| 错误预算消耗 | SLO 允许的故障时间 | 每月消耗 < 20% |
实践建议:优先治"回滚慢"和"MTTR 长"的问题,因为这俩直接影响线上故障的止损速度。灰度发布和混沌工程先做最核心的 3-5 个服务,不要一口气铺全量。
动手实操:全链路灰度配置
以下是一个最小可运行的全链路灰度配置,包含网关、Nacos 元数据、Feign 拦截器。
1. 网关路由(Spring Cloud Gateway)
yaml
# application.yml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- AddRequestHeader=x-version, ${request.headers.x-version}2. Nacos 注册时打版本标签
yaml
# bootstrap.yml
spring:
cloud:
nacos:
discovery:
metadata:
version: gray # 灰度实例打 gray,基线打 base3. Feign 拦截器传递灰度标
java
@Component
public class GrayTagInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
String version = attrs.getRequest().getHeader("x-version");
if (version != null) {
template.header("x-version", version);
}
}
}
}4. 下游服务调用时按标签选实例
java
// 获取所有实例,再按 metadata key=version 过滤出灰度版本
List<Instance> allInstances = namingService.selectInstances(
"order-service", "DEFAULT_GROUP", true);
List<Instance> instances = allInstances.stream()
.filter(inst -> "gray".equals(inst.getMetadata().get("version")))
.collect(Collectors.toList());这个配置完整跑通后,灰度流量就能从网关一路透传到最底层服务。
常见误区与小结
- 灰度比例不等于风险比例:5% 的灰度流量如果触发的是数据库死锁,影响范围会扩散到全量。灰度期间必须监控数据库连接池和慢查询。
- 混沌工程不是测试:混沌验证的是"故障发生时系统表现是否符合预期",而不是测 bug。应在已有监控告警的稳定系统上做。
- 探针路径不要用复杂逻辑:
/actuator/health不应检查数据库连接,否则数据库抖动会导致 K8s 反复重启整个集群。用livenessProbe的轻量检查和readinessProbe的独立路径。 - preStop sleep 太长也可能有问题:30s 够用,设到 120s 会让滚动更新变慢。配合 K8s 的
terminationGracePeriodSeconds一起调,保持两者一致。 - 治理不是一次做完:先做最痛的(回滚流程、发布失败率),再逐步铺灰度、混沌、SLO 驱动。
小结:微服务治理的本质是把"人肉运维"变成"代码规则"。灰度发布降低了上线风险,混沌工程验证了系统韧性,K8s 探针和优雅停机保证了发布过程的稳定性。从最痛的指标开始,逐步构建治理闭环。
下一篇:系统设计篇,从分布式系统设计面试的角度重新组织这些知识。
参考
参考:ChaosBlade 官方文档(https://chaosblade.io)、K8s 官方 Pod Lifecycle 文档、Spring Cloud Gateway 标签路由文档