微服务拆开之后,最容易失去的两样东西是:
前者需要链路追踪解决"看得见",后者需要灰度发布解决"改得稳"。
这两者看起来属于不同主题,但在真实项目里高度相关。因为灰度能不能安全放量,最终取决于你能不能看清灰度流量的真实表现。
单体应用里,一次请求通常只在一个进程内部流转。排查问题时,往往靠:
就能比较快定位。
但在微服务场景下,一次用户请求可能经过:
┌─────────┐
│ 用户 │
└─────────┘
↓
┌─────────┐
│ 网关 │
└─────────┘
↓
┌─────────┐
│ 订单服务 │
└─────────┘
↓
┌─────────┐
│ 库存服务 │
└─────────┘
↓
┌─────────┐
│ 支付服务 │
└─────────┘
↓
┌─────────┐
│ 消息队列 │
└─────────┘
↓
┌─────────┐
│异步消费者│
└─────────┘如果没有统一 Trace 标识,排障时就会非常痛苦。你可能看到多个服务都报错,但很难判断:
这正是链路追踪要解决的问题。
链路追踪的价值不只是"看图",更重要的是快速回答这些问题:
核心价值:
链路追踪 = 问题定位 + 性能分析 + 依赖分析 + 故障还原
价值体现:
├─ 问题定位: 快速找到故障点
├─ 性能分析: 发现性能瓶颈
├─ 依赖分析: 理解服务依赖关系
└─ 故障还原: 还原故障现场traceId 表示一次完整请求或业务链路的统一标识。
作用:
生成规则:
常见生成规则:
1. UUID
- 优点: 简单,唯一性好
- 缺点: 较长(36字符),无序
2. 雪花算法
- 优点: 有序,较短
- 缺点: 需要配置机器ID
3. 自定义规则
- 示例: {时间戳}{机器ID}{序列号}
- 优点: 可读性好,可解析
- 缺点: 需要维护机器ID示例:
// 生成 traceId
public class TraceIdGenerator {
public static String generate() {
// 方式1: UUID
String traceId = UUID.randomUUID().toString().replace("-", "");
// 方式2: 雪花算法
// String traceId = String.valueOf(snowflakeIdWorker.nextId());
return traceId;
}
}
// 在网关生成 traceId
@Component
public class TraceIdFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = exchange.getRequest().getHeaders().getFirst("X-Trace-Id");
if (traceId == null || traceId.isEmpty()) {
traceId = TraceIdGenerator.generate();
}
// 设置到 exchange attributes
// 注意:网关基于 WebFlux,ThreadLocal 的 MDC 在响应式链路中不可靠,应使用 exchange attributes
exchange.getAttributes().put("traceId", traceId);
// 透传给下游服务
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return -1000;
}
}span 可以理解为链路中的一个具体调用片段,例如:
每个 span 通常会记录:
Span 数据结构:
public class Span {
private String traceId; // 所属 trace
private String spanId; // 当前 span ID
private String parentSpanId; // 父 span ID
private String operationName; // 操作名称
private long startTime; // 开始时间
private long endTime; // 结束时间
private long duration; // 耗时(纳秒)
private Map<String, String> tags; // 标签
private List<LogEntry> logs; // 日志
private SpanStatus status; // 状态
public enum SpanStatus {
OK,
ERROR
}
}Span 示例:
TraceId: 1234567890abcdef
Span 1: 网关 → 订单服务
├─ spanId: span-1
├─ parentSpanId: null (根 span)
├─ operationName: POST /api/orders
├─ startTime: 1640000000000000000
├─ endTime: 1640000000200000000
├─ duration: 200ms
├─ tags:
│ ├─ http.method: POST
│ ├─ http.url: /api/orders
│ └─ http.status_code: 200
└─ status: OK
Span 2: 订单服务 → 库存服务
├─ spanId: span-2
├─ parentSpanId: span-1
├─ operationName: POST /internal/inventory/decrement
├─ startTime: 1640000000050000000
├─ endTime: 1640000000150000000
├─ duration: 100ms
├─ tags:
│ ├─ http.method: POST
│ ├─ http.url: /internal/inventory/decrement
│ └─ http.status_code: 200
└─ status: OK
Span 3: 库存服务 → MySQL
├─ spanId: span-3
├─ parentSpanId: span-2
├─ operationName: UPDATE inventory
├─ startTime: 1640000000100000000
├─ endTime: 1640000000120000000
├─ duration: 20ms
├─ tags:
│ ├─ db.type: mysql
│ ├─ db.instance: inventory_db
│ └─ db.statement: UPDATE inventory SET count = count - ? WHERE id = ?
└─ status: OK┌─────────────────────────────────────────────────────────┐
│ 应用服务 │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Trace SDK │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Trace │ │ Span │ │ Context │ │ │
│ │ │ Builder │ │ Manager │ │ Propagator│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 上报数据
┌─────────────────────────────────────────────────────────┐
│ Trace Collector │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 接收 Trace 数据 │ │
│ │ - 数据验证和清洗 │ │
│ │ - 数据格式转换 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 存储数据
┌─────────────────────────────────────────────────────────┐
│ Trace Storage │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - Elasticsearch (主流) │ │
│ │ - Cassandra │ │
│ │ - MySQL (小规模) │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 查询数据
┌─────────────────────────────────────────────────────────┐
│ Trace UI │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 调用链路查询 │ │
│ │ - 性能分析 │ │
│ │ - 拓扑图 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘1. 埋点方式
// 手动埋点
Span span = tracer.spanBuilder("createOrder")
.setParent(parentContext)
.setSpanKind(SpanKind.CLIENT)
.startSpan();
try {
// 业务逻辑
orderService.createOrder(request);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}2. 自动埋点
// 使用 AOP 自动埋点
@Aspect
@Component
public class TraceAspect {
@Autowired
private Tracer tracer;
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
Span span = tracer.spanBuilder(joinPoint.getSignature().getName())
.setSpanKind(SpanKind.SERVER)
.startSpan();
try {
return joinPoint.proceed();
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}3. Agent 方式
# 使用 Java Agent 无侵入埋点
java -javaagent:/path/to/skywalking-agent.jar \
-Dskywalking.agent.service_name=user-service \
-Dskywalking.collector.backend_service=localhost:11800 \
-jar user-service.jar这是很多团队的一个常见问题:只做了 HTTP 请求透传,就以为链路追踪已经完成。
但真实业务链路不只包含同步 HTTP 调用,还可能包含:
如果只做服务间 HTTP 透传,不做 MQ 和异步任务透传,链路图就会出现断层。排障时仍然要靠人工拼日志。
完整链路追踪覆盖范围:
┌─────────────────────────────────────────────────────┐
│ 完整链路追踪覆盖范围 │
├─────────────────────────────────────────────────────┤
│ 1. 同步调用 │
│ ├─ HTTP 调用 │
│ ├─ RPC 调用 │
│ └─ 数据库访问 │
├─────────────────────────────────────────────────────┤
│ 2. 异步调用 │
│ ├─ MQ 消息投递 │
│ ├─ MQ 消息消费 │
│ └─ 线程池任务 │
├─────────────────────────────────────────────────────┤
│ 3. 定时任务 │
│ ├─ 定时触发 │
│ └─ 任务执行 │
├─────────────────────────────────────────────────────┤
│ 4. 缓存访问 │
│ ├─ Redis 调用 │
│ └─ 本地缓存 │
└─────────────────────────────────────────────────────┘// 消息生产者
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void createOrder(Order order) {
// 获取当前 trace context
TraceContext context = TraceContextHolder.getCurrentContext();
// 发送消息时透传 traceId
rabbitTemplate.convertAndSend("order.queue", order, message -> {
message.getMessageProperties().setHeader("X-Trace-Id", context.getTraceId());
message.getMessageProperties().setHeader("X-Span-Id", context.getSpanId());
return message;
});
}
}
// 消息消费者
@Component
public class OrderConsumer {
@RabbitListener(queues = "order.queue")
public void handleOrder(Message message) {
// 从消息头提取 traceId
String traceId = message.getMessageProperties().getHeader("X-Trace-Id");
String parentSpanId = message.getMessageProperties().getHeader("X-Span-Id");
// 设置新的 trace context
TraceContext context = new TraceContext(traceId, parentSpanId);
TraceContextHolder.setCurrentContext(context);
try {
// 处理消息
processOrder(message);
} finally {
TraceContextHolder.clear();
}
}
}// 使用线程池时透传 trace context
@Service
public class OrderService {
@Autowired
private ExecutorService executorService;
public void processOrderAsync(Order order) {
// 获取当前 trace context
TraceContext context = TraceContextHolder.getCurrentContext();
// 提交任务时透传
executorService.submit(() -> {
// 在新线程中设置 trace context
TraceContextHolder.setCurrentContext(context);
try {
processOrder(order);
} finally {
TraceContextHolder.clear();
}
});
}
}
// 使用装饰器模式自动透传
public class TraceContextExecutorService implements ExecutorService {
private final ExecutorService delegate;
public TraceContextExecutorService(ExecutorService delegate) {
this.delegate = delegate;
}
@Override
public void execute(Runnable command) {
TraceContext context = TraceContextHolder.getCurrentContext();
delegate.execute(() -> {
TraceContextHolder.setCurrentContext(context);
try {
command.run();
} finally {
TraceContextHolder.clear();
}
});
}
// ... 其他方法类似
}| 框架 | 开发语言 | 核心特点 | 性能 | 社区活跃度 | 推荐度 |
|---|---|---|---|---|---|
| SkyWalking | Java | 国产、功能全面、无侵入 | 高 | 高 | 首选 |
| Zipkin | Java | Twitter 开源、生态成熟 | 中 | 中 | 适合 Spring Cloud 集成 |
| Jaeger | Go | Uber 开源、云原生 | 高 | 高 | 云原生环境首选 |
| Pinpoint | Java | Naver 开源、界面友好 | 中 | 中 | 备选 |
Apache SkyWalking 是一个开源的 APM 系统,核心特点:
┌─────────────────────────────────────────────────────────┐
│ SkyWalking Agent │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 数据采集 │ │
│ │ - 字节码增强 │ │
│ │ - Trace 数据生成 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ gRPC
┌─────────────────────────────────────────────────────────┐
│ SkyWalking OAP │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 数据聚合 │ │
│ │ - 数据分析 │ │
│ │ - 告警判断 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 存储
┌─────────────────────────────────────────────────────────┐
│ Storage │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - Elasticsearch (推荐) │ │
│ │ - H2 (测试) │ │
│ │ - MySQL (小规模) │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 查询
┌─────────────────────────────────────────────────────────┐
│ SkyWalking UI │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 调用链路查询 │ │
│ │ - 拓扑图 │ │
│ │ - 性能指标 │ │
│ │ - 告警管理 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘1. Agent 配置
# 启动参数
java -javaagent:/path/to/skywalking-agent.jar \
-Dskywalking.agent.service_name=user-service \
-Dskywalking.collector.backend_service=localhost:11800 \
-Dskywalking.agent.instance_properties_json='{"zone":"beijing"}' \
-jar user-service.jar2. agent/config/agent.config
# 服务名称
agent.service_name=${SW_AGENT_NAME:user-service}
# OAP 地址
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:localhost:11800}
# 采样率(-1 表示全采样;正数表示每 3 秒采样条数)
agent.sample_n_per_3_secs=${SW_AGENT_SAMPLE:-1}
# 忽略的路径
agent.ignore_path=${SW_AGENT_IGNORE_PATH:/actuator/**,/health}
# 日志级别
logging.level=${SW_LOGGING_LEVEL:INFO}3. 自定义 Span
import org.apache.skywalking.apm.toolkit.trace.TraceCrossThread;
import org.apache.skywalking.apm.toolkit.trace.Tracer;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
@Service
public class OrderService {
// 手动创建 Span
public void createOrder(Order order) {
// 创建活跃 Span
AbstractSpan span = Tracer.createEntrySpan("createOrder");
try {
// 添加标签
span.tag("orderId", order.getId());
span.tag("userId", order.getUserId());
// 业务逻辑
orderRepository.save(order);
} catch (Exception e) {
// 记录异常
span.errorOccurred().log(e);
throw e;
} finally {
// 结束 Span
Tracer.stopSpan();
}
}
// 异步调用
@Async
@TraceCrossThread
public void asyncProcess(Order order) {
// 自动继承父线程的 trace context
processOrder(order);
}
// 获取 TraceId
public String getTraceId() {
return TraceContext.traceId();
}
}4. 注解方式
import org.apache.skywalking.apm.toolkit.trace.annotation.Trace;
@Service
public class OrderService {
@Trace(operationName = "createOrder")
@Tag(key = "orderId", value = "arg[0].id")
@Tag(key = "userId", value = "arg[0].userId")
public void createOrder(Order order) {
orderRepository.save(order);
}
}Zipkin 是 Twitter 开源的分布式追踪系统,核心特点:
┌─────────────────────────────────────────────────────────┐
│ Application │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Brave SDK │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ HTTP/Kafka
┌─────────────────────────────────────────────────────────┐
│ Zipkin Server │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 数据收集 │ │
│ │ - 数据存储 │ │
│ │ - 数据查询 │ │
│ │ - UI 展示 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 存储
┌─────────────────────────────────────────────────────────┐
│ Storage │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - Elasticsearch │ │
│ │ - Cassandra │ │
│ │ - MySQL │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘1. 依赖配置
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>2. application.yml
spring:
zipkin:
base-url: http://localhost:9411
sender:
type: web
sleuth:
sampler:
probability: 1.0 # 采样率 100%
web:
skip-pattern: /actuator.*,/health3. 自定义 Span
import brave.Span;
import brave.Tracer;
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public void createOrder(Order order) {
Span span = tracer.nextSpan().name("createOrder").start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
span.tag("orderId", order.getId());
span.tag("userId", order.getUserId());
orderRepository.save(order);
} catch (Exception e) {
span.error(e);
throw e;
} finally {
span.finish();
}
}
}Jaeger 是 Uber 开源的分布式追踪系统,核心特点:
┌─────────────────────────────────────────────────────────┐
│ Application │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Jaeger Client │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ UDP/HTTP
┌─────────────────────────────────────────────────────────┐
│ Jaeger Agent │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 数据收集 │ │
│ │ - 数据转发 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ gRPC
┌─────────────────────────────────────────────────────────┐
│ Jaeger Collector │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - 数据验证 │ │
│ │ - 数据转换 │ │
│ │ - 数据存储 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 存储
┌─────────────────────────────────────────────────────────┐
│ Storage │
│ ┌────────────────────────────────────────────────────┐ │
│ │ - Elasticsearch │ │
│ │ - Cassandra │ │
│ │ - Kafka │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘1. 依赖配置
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-client</artifactId>
<version>1.8.1</version>
</dependency>2. 配置
@Configuration
public class JaegerConfig {
@Bean
public io.jaegertracing.Tracer jaegerTracer() {
return new Configuration("user-service")
.withSampler(new Configuration.SamplerConfiguration()
.withType(ConstSampler.TYPE)
.withParam(1))
.withReporter(new Configuration.ReporterConfiguration()
.withLogSpans(true)
.withFlushInterval(1000)
.withMaxQueueSize(10000)
.withSender(new Configuration.SenderConfiguration()
.withEndpoint("http://localhost:14268/api/traces")))
.getTracer();
}
}Java 技术栈:
推荐: SkyWalking
理由:
- 无侵入,Java Agent 方式
- 国产,中文文档完善
- 功能全面(APM + Trace)
- 性能好Spring Cloud 微服务:
推荐: Zipkin + Spring Cloud Sleuth
理由:
- 与 Spring Cloud 深度集成
- 配置简单
- 社区成熟云原生环境:
推荐: Jaeger
理由:
- 云原生设计
- 支持 Istio
- 高性能多语言环境:
推荐: Jaeger 或 SkyWalking
理由:
- 都支持多语言
- OpenTracing 兼容灰度发布的目标,是让新版本先在小流量范围内验证,而不是一上来就全量切换。
它解决的核心问题是: 发布风险控制
与其让所有用户一起验证新版本,不如先让一小部分流量进入新版本,观察是否稳定,再逐步扩大范围。
传统发布 vs 灰度发布:
传统发布(全量发布):
┌─────────────────────────────────────────────────────┐
│ V1 (100%) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 用户1, 用户2, 用户3, ..., 用户N │ │
│ │ 如果出问题,所有用户都受影响 │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
↓ 一键切换
┌─────────────────────────────────────────────────────┐
│ V2 (100%) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 用户1, 用户2, 用户3, ..., 用户N │ │
│ │ 新版本对所有用户生效 │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
风险: 全量切换,如果新版本有问题,影响所有用户
灰度发布(逐步放量):
┌─────────────────────────────────────────────────────┐
│ V1 (95%) V2 (5%) │
│ ┌──────────────────────────┐ ┌──────────┐ │
│ │ 用户1, ..., 用户N-1 │ │ 用户N │ │
│ │ 95% 用户使用旧版本 │ │ 5% 灰度 │ │
│ └──────────────────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
↓ 观察,无异常
┌─────────────────────────────────────────────────────┐
│ V1 (80%) V2 (20%) │
│ ┌────────────────────────┐ ┌────────────────┐ │
│ │ 用户1, ..., 用户N-4 │ │ 用户N-3...用户N │ │
│ │ 80% 用户使用旧版本 │ │ 20% 灰度 │ │
│ └────────────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────┘
↓ 观察,无异常
┌─────────────────────────────────────────────────────┐
│ V1 (0%) V2 (100%) │
│ ┌──────────────────────────┐ ┌──────────────────┐ │
│ │ (空) │ │ 所有用户 │ │
│ │ │ │ 新版本全量 │ │
│ └──────────────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
优势: 逐步放量,如有问题可快速回滚,影响范围可控灰度流量常见的划分方式包括:
# Istio VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- route:
- destination:
host: user-service
subset: v1
weight: 90 # 90% 流量到 v1
- destination:
host: user-service
subset: v2
weight: 10 # 10% 流量到 v2适用场景:
# Istio VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- match:
- headers:
X-User-Id:
exact: "12345" # 指定用户 ID
route:
- destination:
host: user-service
subset: v2
- route:
- destination:
host: user-service
subset: v1适用场景:
// 基于租户 ID 的灰度路由(示意)
@Component
public class TenantGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ServiceInstanceListSupplier supplier;
public TenantGrayLoadBalancer(ServiceInstanceListSupplier supplier, String serviceId) {
this.supplier = supplier;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServerWebExchange exchange = (ServerWebExchange) request.getContext();
String tenantId = exchange.getRequest().getHeaders().getFirst("X-Tenant-Id");
// 根据租户 ID 选择实例
return supplier.get().next()
.map(instances -> new DefaultResponse(selectInstanceByTenant(instances, tenantId)));
}
private ServiceInstance selectInstanceByTenant(List<ServiceInstance> instances, String tenantId) {
// 按 metadata 中的租户/版本标记筛选,未命中则退回普通实例
return instances.stream()
.filter(i -> tenantId != null && tenantId.equals(i.getMetadata().get("tenant")))
.findFirst()
.orElse(instances.get(0));
}
}适用场景:
# Nginx 灰度配置
upstream user-service {
# 北京机房
server user-service-beijing:8080 weight=90;
# 上海机房
server user-service-shanghai:8080 weight=10;
}
# 根据请求来源 IP 路由
map $remote_addr $backend {
default user-service;
~^10\.0\.1\. user-service-beijing;
~^10\.0\.2\. user-service-shanghai;
}适用场景:
// 基于 Header 的灰度路由(示意)
@Component
public class HeaderGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ServiceInstanceListSupplier supplier;
public HeaderGrayLoadBalancer(ServiceInstanceListSupplier supplier, String serviceId) {
this.supplier = supplier;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServerWebExchange exchange = (ServerWebExchange) request.getContext();
String grayVersion = exchange.getRequest().getHeaders().getFirst("X-Gray-Version");
String target = "v2".equals(grayVersion) ? "v2" : "v1";
// 根据灰度版本选择实例
return supplier.get().next()
.map(instances -> new DefaultResponse(selectByVersion(instances, target)));
}
private ServiceInstance selectByVersion(List<ServiceInstance> instances, String version) {
// 按 metadata 中的 version 标记筛选,未命中则退回第一个实例
return instances.stream()
.filter(i -> version.equals(i.getMetadata().get("version")))
.findFirst()
.orElse(instances.get(0));
}
}适用场景:
灰度发布如果没有可观测性支撑,本质上就是:
蒙着眼放量
灰度期间,至少要能够观察:
技术指标:
业务指标:
否则即使 CPU、内存看起来都正常,业务结果也可能已经出问题。
所以灰度能不能安全推进,核心不在于"放量按钮存不存在",而在于: 放量决策是否建立在真实数据上
灰度监控指标体系:
├─ 技术指标
│ ├─ 错误率: 灰度版本 vs 基线版本
│ ├─ RT: P50、P95、P99
│ ├─ QPS: 灰度流量占比
│ └─ 资源使用: CPU、内存、线程池
├─ 业务指标
│ ├─ 核心业务成功率: 下单、支付
│ ├─ 转化率: 访问 → 下单 → 支付
│ └─ 业务错误率: 业务异常占比
└─ 对比指标
├─ 灰度版本 vs 基线版本
├─ 灰度前 vs 灰度后
└─ 历史同期对比// 灰度路由过滤器
@Component
public class GrayRouteFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从请求头获取灰度标识
String grayVersion = exchange.getRequest().getHeaders().getFirst("X-Gray-Version");
String grayUser = exchange.getRequest().getHeaders().getFirst("X-Gray-User");
// 灰度规则判断
if (isGrayUser(grayUser) || isGrayVersion(grayVersion)) {
// 添加灰度标识
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-Service-Version", "v2")
.build();
return chain.filter(exchange.mutate().request(request).build());
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -50;
}
}# application.yml
spring:
cloud:
gateway:
routes:
- id: user-service-v1
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
- Header=X-Service-Version, v1
- id: user-service-v2
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- Header=X-Service-Version, v2# 灰度实例配置
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
metadata:
version: v2 # 版本标记
gray: true # 灰度标记// 灰度负载均衡器(示意)
@Component
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Autowired
private GrayRuleConfig grayRuleConfig;
private final ServiceInstanceListSupplier supplier;
public GrayLoadBalancer(ServiceInstanceListSupplier supplier, String serviceId) {
this.supplier = supplier;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// 获取灰度标识
String grayVersion = getGrayVersion(request);
// 根据灰度规则选择实例
return supplier.get().next()
.map(instances -> new DefaultResponse(selectInstance(instances, grayVersion)));
}
private String getGrayVersion(Request request) {
// 从 Header 获取(Spring Cloud LoadBalancer 网关场景下 context 即 ServerWebExchange)
ServerWebExchange exchange = (ServerWebExchange) request.getContext();
String version = exchange.getRequest()
.getHeaders()
.getFirst("X-Gray-Version");
if (version != null) {
return version;
}
// 从配置获取默认灰度规则
return grayRuleConfig.getDefaultVersion();
}
private ServiceInstance selectInstance(List<ServiceInstance> instances, String grayVersion) {
// 过滤出指定版本的实例
List<ServiceInstance> filtered = instances.stream()
.filter(instance -> grayVersion.equals(instance.getMetadata().get("version")))
.collect(Collectors.toList());
if (filtered.isEmpty()) {
// 没有灰度实例,使用基线版本
filtered = instances.stream()
.filter(instance -> "v1".equals(instance.getMetadata().get("version")))
.collect(Collectors.toList());
}
// 简化处理:未做二次负载均衡,取第一个实例(生产中应在此处再做随机/轮询)
return filtered.get(0);
}
}# DestinationRule - 定义版本子集
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: user-service
spec:
host: user-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
# VirtualService - 灰度路由规则
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
# 基于 Header 的灰度
- match:
- headers:
X-Gray-Version:
exact: "v2"
route:
- destination:
host: user-service
subset: v2
# 基于权重的灰度
- route:
- destination:
host: user-service
subset: v1
weight: 90
- destination:
host: user-service
subset: v2
weight: 10
# 重试配置
retries:
attempts: 3
perTryTimeout: 2s
# 超时配置
timeout: 10s
# 故障注入配置(用于灰度演练验证,非熔断;熔断由 DestinationRule 的 outlierDetection 配置)
fault:
abort:
percentage:
value: 0.1
httpStatus: 503// 特性开关配置
@Configuration
@RefreshScope
public class FeatureToggleConfig {
@Value("${feature.new-order-flow.enabled:false}")
private boolean newOrderFlowEnabled;
@Value("${feature.new-order-flow.percentage:0}")
private int newOrderFlowPercentage;
@Value("${feature.new-order-flow.whitelist:}")
private String newOrderFlowWhitelist;
public boolean isNewOrderFlowEnabled(Long userId) {
if (!newOrderFlowEnabled) {
return false;
}
// 白名单用户
if (newOrderFlowWhitelist.contains(userId.toString())) {
return true;
}
// 按比例灰度
return userId % 100 < newOrderFlowPercentage;
}
}
// 使用特性开关
@Service
public class OrderService {
@Autowired
private FeatureToggleConfig featureToggleConfig;
public Order createOrder(OrderRequest request) {
if (featureToggleConfig.isNewOrderFlowEnabled(request.getUserId())) {
// 新流程
return createOrderNew(request);
} else {
// 旧流程
return createOrderOld(request);
}
}
}# Nacos 配置
feature:
new-order-flow:
enabled: true
percentage: 10 # 10% 用户使用新流程
whitelist: 12345,67890 # 白名单用户订单服务上线新版本,需要灰度验证。
1. 实例标记
# v1 版本实例
spring:
application:
name: order-service
cloud:
nacos:
discovery:
metadata:
version: v1
# v2 版本实例
spring:
application:
name: order-service
cloud:
nacos:
discovery:
metadata:
version: v22. 灰度路由
@Component
public class OrderGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ServiceInstanceListSupplier supplier;
public OrderGrayLoadBalancer(ServiceInstanceListSupplier supplier, String serviceId) {
this.supplier = supplier;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServerWebExchange exchange = (ServerWebExchange) request.getContext();
String grayVersion = exchange.getRequest().getHeaders().getFirst("X-Gray-Version");
// 根据灰度版本选择实例
return supplier.get().next()
.map(instances -> new DefaultResponse(selectByVersion(instances, grayVersion)));
}
private ServiceInstance selectByVersion(List<ServiceInstance> instances, String grayVersion) {
// 按 metadata 中的 version 标记筛选,未命中则退回第一个实例
return instances.stream()
.filter(i -> grayVersion != null && grayVersion.equals(i.getMetadata().get("version")))
.findFirst()
.orElse(instances.get(0));
}
}3. 灰度流程
灰度流程:
1. 部署 v2 版本
↓
2. 5% 流量灰度(内部测试)
- 监控错误率、RT
- 观察业务指标
↓
3. 10% 流量灰度
- 继续监控
- 对比 v1 vs v2
↓
4. 30% 流量灰度
- 扩大灰度范围
- 关注告警
↓
5. 50% 流量灰度
- 半量验证
↓
6. 100% 流量灰度
- 全量切换
↓
7. 下线 v1 版本4. 监控指标
灰度期间重点监控:
├─ 技术指标
│ ├─ 错误率: v2 < v1 * 1.1
│ ├─ RT: v2 < v1 * 1.2
│ └─ 资源使用: CPU < 70%, Memory < 80%
├─ 业务指标
│ ├─ 下单成功率: v2 ≥ v1
│ ├─ 支付成功率: v2 ≥ v1
│ └─ 订单量: v2 ≈ v1
└─ 告警规则
├─ 错误率 > 1% → 告警
├─ RT P95 > 500ms → 告警
└─ 业务成功率下降 > 5% → 告警用户反馈"下单很慢",需要快速定位问题。
1. 查询链路追踪
在 SkyWalking UI 查询:
- TraceId: 从用户请求获取
- 或根据时间范围查询慢 Trace
查看链路详情:
┌─────────────────────────────────────────────────────┐
│ TraceId: 1234567890abcdef │
│ Total Duration: 850ms │
├─────────────────────────────────────────────────────┤
│ 网关 → 订单服务 │
│ Duration: 850ms │
│ ├─ 订单服务 → 库存服务 │
│ │ Duration: 200ms │
│ │ └─ 库存服务 → MySQL │
│ │ Duration: 150ms ← 慢! │
│ ├─ 订单服务 → 支付服务 │
│ │ Duration: 300ms ← 慢! │
│ │ └─ 支付服务 → 第三方支付 │
│ │ Duration: 280ms │
│ └─ 订单服务 → Redis │
│ Duration: 10ms │
└─────────────────────────────────────────────────────┘2. 分析性能瓶颈
性能瓶颈分析:
1. 支付服务调用第三方支付: 280ms (可优化空间小)
2. 库存服务数据库查询: 150ms (可能存在慢 SQL)
3. 订单服务内部处理: 100ms (可接受)
优化建议:
1. 检查库存服务 SQL:
- 是否缺少索引
- 是否存在全表扫描
- 是否锁等待
2. 支付服务优化:
- 异步化非核心流程
- 使用缓存3. 查看慢 SQL
# 从 SkyWalking 查看 SQL 详情
Operation: SELECT * FROM inventory WHERE product_id = ?
Duration: 150ms
TraceId: 1234567890abcdef
# 分析 SQL 执行计划
EXPLAIN SELECT * FROM inventory WHERE product_id = ?;新功能不一定需要直接发新版,也可以配合配置中心和特性开关灰度。
// 特性开关管理
@Service
public class FeatureToggleService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean isFeatureEnabled(String featureName, Long userId) {
// 1. 检查全局开关
if (!isGlobalEnabled(featureName)) {
return false;
}
// 2. 检查白名单
if (isInWhitelist(featureName, userId)) {
return true;
}
// 3. 检查灰度比例
return isInGrayPercentage(featureName, userId);
}
private boolean isGlobalEnabled(String featureName) {
String key = "feature:" + featureName + ":enabled";
return Boolean.TRUE.equals(redisTemplate.opsForValue().get(key));
}
private boolean isInWhitelist(String featureName, Long userId) {
String key = "feature:" + featureName + ":whitelist";
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, userId.toString()));
}
private boolean isInGrayPercentage(String featureName, Long userId) {
String key = "feature:" + featureName + ":percentage";
String percentage = redisTemplate.opsForValue().get(key);
if (percentage == null) {
return false;
}
int grayPercentage = Integer.parseInt(percentage);
return userId % 100 < grayPercentage;
}
}
// 使用特性开关
@RestController
public class OrderController {
@Autowired
private FeatureToggleService featureToggleService;
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest request) {
// 根据特性开关选择不同逻辑
if (featureToggleService.isFeatureEnabled("new-order-flow", request.getUserId())) {
return orderService.createOrderNew(request);
} else {
return orderService.createOrderOld(request);
}
}
}采样率建议:
- 开发环境: 100%
- 测试环境: 100%
- 预发环境: 50%
- 生产环境: 1-10%
注意:
- 关键链路: 100% 采样
- 非关键链路: 采样率可低
- 错误请求: 必须 100% 采样TraceId 透传规范:
├─ HTTP 调用
│ ├─ Request Header: X-Trace-Id
│ └─ Response Header: X-Trace-Id (可选)
├─ RPC 调用
│ └─ RPC Context: TraceId
├─ MQ 消息
│ └─ Message Header: X-Trace-Id
├─ 异步线程
│ └─ ThreadLocal: TraceContext
└─ 日志输出
└─ MDC: traceId性能优化建议:
1. 异步上报数据
2. 批量发送 Span
3. 合理设置采样率
4. 使用高效的序列化方式
5. Agent 使用独立的线程池标准灰度流程:
1. 代码评审 + 测试环境验证
↓
2. 预发环境验证
↓
3. 生产环境灰度
├─ 5% (内部测试)
├─ 10% (小范围验证)
├─ 30% (扩大范围)
├─ 50% (半量验证)
└─ 100% (全量切换)
↓
4. 观察期(24-72小时)
↓
5. 下线旧版本回滚预案:
1. 快速回滚
- 调整流量权重
- 关闭特性开关
- 下线灰度实例
2. 代码回滚
- 保留旧版本代码
- 准备回滚脚本
- 测试回滚流程
3. 数据回滚
- 数据库变更可逆
- 数据迁移脚本
- 数据校验脚本灰度监控体系:
├─ 实时监控
│ ├─ 错误率监控
│ ├─ RT 监控
│ └─ 业务指标监控
├─ 对比分析
│ ├─ v1 vs v2
│ └─ 灰度前后对比
├─ 告警规则
│ ├─ 错误率 > 阈值
│ ├─ RT > 阈值
│ └─ 业务成功率下降
└─ 值班机制
├─ 灰度期间值班
└─ 快速响应现象:
排查步骤:
1. 检查 traceId 是否在网关或入口处生成
- 查看网关日志
- 检查 TraceIdFilter
2. 检查 HTTP、RPC、MQ 调用是否透传上下文
- 查看请求头
- 检查消息头
3. 检查异步线程或线程池切换时是否丢失 trace
- 使用装饰器模式
- 手动透传 TraceContext
4. 检查日志、指标和 trace 是否能互相关联
- 日志中是否包含 traceId
- 指标是否包含 trace 维度现象:
排查步骤:
1. 检查采样率
- 采样率过高
- 建议: 生产环境 1-10%
2. 检查上报方式
- 同步上报 → 改为异步
- 单条上报 → 改为批量
3. 检查序列化方式
- JSON 序列化较慢
- 考虑二进制序列化
4. 检查 Agent 配置
- 缓冲区大小
- 上报间隔现象:
排查步骤:
1. 检查灰度规则配置
- 权重配置是否正确
- 白名单配置是否正确
2. 检查灰度标识透传
- Header 是否正确透传
- Cookie 是否正确解析
3. 检查负载均衡策略
- 是否支持灰度路由
- 是否有缓存导致规则不生效
4. 检查回滚流程
- 回滚脚本是否准备好
- 回滚是否测试过现象:
排查步骤:
1. 检查实例元数据
- 版本标记是否正确
- 是否已注册到注册中心
2. 检查路由规则
- 路由条件是否正确
- 优先级是否合理
3. 检查请求特征
- Header 是否包含灰度标识
- 用户 ID 是否在白名单中
4. 检查日志
- 灰度路由日志
- 负载均衡日志× 错误理解:
日志里有 traceId 字符串就够了,不需要专门的链路追踪系统。
√ 正确理解:
没有统一 trace 标识,排障时只能人工拼接多服务日志,效率非常低。
链路追踪系统的价值:
× 错误理解:
只追踪 HTTP 调用就够了,MQ 和异步任务不需要追踪。
√ 正确理解:
这样会导致链路断层,很多真实问题仍然定位不完整。
完整追踪范围:
× 错误理解:
只要 CPU、内存、错误率正常,灰度就是成功的。
√ 正确理解:
技术指标正常,不代表业务结果一定正常。
必须关注的业务指标:
× 错误理解:
灰度规则配置后就不用管了,出问题再说。
√ 正确理解:
不能快速回退的灰度,不是真正意义上的灰度治理。
灰度必须满足:
× 错误理解:
发布前不需要准备回滚方案,出了问题再说。
√ 正确理解:
发布前不准备回滚路径,本质上是在拿线上环境做不可逆试验。
回滚预案:
答案:
快速定位是哪一跳出了问题,并把一次请求经过的服务、依赖和耗时串起来。
展开:
核心价值:
关键能力:
答案:
TraceId:
Span:
关系:
答案:
因为问题可以先在小范围暴露,并在影响可控时快速回滚。
展开:
风险控制:
对比全量发布:
答案:
因为放量决策必须建立在真实数据上,而不是凭感觉判断"应该没问题"。
展开:
可观测性要求:
决策依据:
答案:
因为业务结果正常,才代表这次发布真正成功。
展开:
技术指标 vs 业务指标:
技术指标:
- CPU、内存使用率正常
- 错误率正常
- RT 正常
业务指标:
- 下单成功率下降
- 支付成功率下降
- 转化率下降
结论:
技术指标正常 ≠ 业务正常
业务指标正常 = 发布成功答案:
回滚方案:
1. 快速回滚
- 调整流量权重
- 关闭特性开关
- 下线灰度实例
2. 代码回滚
- 保留旧版本代码
- 准备回滚脚本
- 测试回滚流程
3. 数据回滚
- 数据库变更可逆
- 数据迁移脚本
- 数据校验脚本
4. 回滚测试
- 测试环境验证
- 预发环境验证
- 回滚演练理解链路追踪与灰度发布,至少要掌握这些点:
核心技术栈:
核心能力:
持续演进:
| 特性 | 旧版(本文编写时) | 当前(Spring Cloud 2025.x / Boot 3.5.x) |
|---|---|---|
| 架构组件 | Eureka/Ribbon/Hystrix/Zuul(已停止维护) | LoadBalancer/Gateway/Resilience4j/Nacos |
| 版本对应 | Hoxton/2020.x + Boot 2.x + JDK 8 | 2025.x + Boot 3.5.x + JDK 17+ |
| 云原生 | 初步支持 | 全面云原生(K8s、虚拟线程、GraalVM 支持) |
| 链路追踪 | Sleuth + Zipkin | Micrometer Tracing + Zipkin/OTel |
本文为通用微服务概念讲解,架构思想长期有效;落地时请采用 Spring Cloud 2025.x + Spring Boot 3.5.x 技术栈(JDK 17+)。