{T}

链路追踪与灰度发布

微服务拆开之后,最容易失去的两样东西是:

  • 出问题时,不知道一次请求到底经过了哪些服务
  • 发布新版本时,不敢直接全量上线

前者需要链路追踪解决"看得见",后者需要灰度发布解决"改得稳"。

这两者看起来属于不同主题,但在真实项目里高度相关。因为灰度能不能安全放量,最终取决于你能不能看清灰度流量的真实表现。

一、链路追踪核心原理

1.1 为什么需要链路追踪

单体时代为什么没那么痛

单体应用里,一次请求通常只在一个进程内部流转。排查问题时,往往靠:

  • 应用日志
  • SQL 日志
  • 调试堆栈

就能比较快定位。

但在微服务场景下,一次用户请求可能经过:

code
┌─────────┐
│  用户    │
└─────────┘
    ↓
┌─────────┐
│  网关    │
└─────────┘
    ↓
┌─────────┐
│ 订单服务 │
└─────────┘
    ↓
┌─────────┐
│ 库存服务 │
└─────────┘
    ↓
┌─────────┐
│ 支付服务 │
└─────────┘
    ↓
┌─────────┐
│ 消息队列 │
└─────────┘
    ↓
┌─────────┐
│异步消费者│
└─────────┘

如果没有统一 Trace 标识,排障时就会非常痛苦。你可能看到多个服务都报错,但很难判断:

  • 是哪一跳先开始变慢
  • 错误从哪个服务开始扩散
  • 某次用户请求到底经过了哪些依赖

这正是链路追踪要解决的问题。

链路追踪的核心价值

链路追踪的价值不只是"看图",更重要的是快速回答这些问题:

  • 哪一跳最慢 - 定位性能瓶颈
  • 哪个服务最先报错 - 定位故障源头
  • 某次异常请求经过了哪些依赖 - 还原调用链路
  • 是接口慢,还是数据库慢,还是消息链路慢 - 分层定位问题

核心价值:

code
链路追踪 = 问题定位 + 性能分析 + 依赖分析 + 故障还原

价值体现:
├─ 问题定位: 快速找到故障点
├─ 性能分析: 发现性能瓶颈
├─ 依赖分析: 理解服务依赖关系
└─ 故障还原: 还原故障现场

1.2 链路追踪的核心概念

TraceId

traceId 表示一次完整请求或业务链路的统一标识。

作用:

  • 把网关、服务、消息、异步任务中的日志和调用记录串起来
  • 同一次请求的所有日志都有相同的 traceId

生成规则:

code
常见生成规则:

1. UUID
   - 优点: 简单,唯一性好
   - 缺点: 较长(36字符),无序

2. 雪花算法
   - 优点: 有序,较短
   - 缺点: 需要配置机器ID

3. 自定义规则
   - 示例: {时间戳}{机器ID}{序列号}
   - 优点: 可读性好,可解析
   - 缺点: 需要维护机器ID

示例:

java
// 生成 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();
        }
        
        // 设置到 MDC
        MDC.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 通常会记录:

  • 开始时间 - 调用开始时间戳
  • 结束时间 - 调用结束时间戳
  • 耗时 - 调用总耗时
  • 调用结果 - 成功/失败
  • 父子关系 - 父 span ID
  • 标签 - HTTP 方法、URL、状态码等
  • 日志 - 事件日志

Span 数据结构:

java
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 示例:

code
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

1.3 链路追踪架构模型

基本架构

code
┌─────────────────────────────────────────────────────────┐
│                      应用服务                            │
│  ┌────────────────────────────────────────────────────┐ │
│  │                   Trace SDK                        │ │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐        │ │
│  │  │ Trace    │  │ Span     │  │ Context  │        │ │
│  │  │ Builder  │  │ Manager  │  │ Propagator│       │ │
│  │  └──────────┘  └──────────┘  └──────────┘        │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 上报数据
┌─────────────────────────────────────────────────────────┐
│                    Trace Collector                       │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 接收 Trace 数据                                  │ │
│  │  - 数据验证和清洗                                    │ │
│  │  - 数据格式转换                                      │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 存储数据
┌─────────────────────────────────────────────────────────┐
│                    Trace Storage                         │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - Elasticsearch (主流)                             │ │
│  │  - Cassandra                                        │ │
│  │  - MySQL (小规模)                                   │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 查询数据
┌─────────────────────────────────────────────────────────┐
│                    Trace UI                              │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 调用链路查询                                      │ │
│  │  - 性能分析                                          │ │
│  │  - 拓扑图                                            │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

数据采集方式

1. 埋点方式

java
// 手动埋点
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. 自动埋点

java
// 使用 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 方式

bash
# 使用 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

1.4 为什么链路追踪不能只停留在 HTTP

这是很多团队的一个常见问题:只做了 HTTP 请求透传,就以为链路追踪已经完成。

但真实业务链路不只包含同步 HTTP 调用,还可能包含:

  • RPC 调用
  • MQ 消息
  • 异步线程池任务
  • 定时任务触发的后续处理

如果只做服务间 HTTP 透传,不做 MQ 和异步任务透传,链路图就会出现断层。排障时仍然要靠人工拼日志。

完整链路追踪覆盖范围:

code
┌─────────────────────────────────────────────────────┐
│                完整链路追踪覆盖范围                    │
├─────────────────────────────────────────────────────┤
│  1. 同步调用                                         │
│     ├─ HTTP 调用                                    │
│     ├─ RPC 调用                                     │
│     └─ 数据库访问                                   │
├─────────────────────────────────────────────────────┤
│  2. 异步调用                                         │
│     ├─ MQ 消息投递                                  │
│     ├─ MQ 消息消费                                  │
│     └─ 线程池任务                                   │
├─────────────────────────────────────────────────────┤
│  3. 定时任务                                         │
│     ├─ 定时触发                                     │
│     └─ 任务执行                                     │
├─────────────────────────────────────────────────────┤
│  4. 缓存访问                                         │
│     ├─ Redis 调用                                   │
│     └─ 本地缓存                                     │
└─────────────────────────────────────────────────────┘

MQ 链路透传

java
// 消息生产者
@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();
        }
    }
}

异步线程链路透传

java
// 使用线程池时透传 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();
            }
        });
    }
    
    // ... 其他方法类似
}

二、主流链路追踪框架对比

2.1 框架选型对比

框架开发语言核心特点性能社区活跃度推荐度
SkyWalkingJava国产、功能全面、无侵入
ZipkinJavaTwitter 开源、生态成熟
JaegerGoUber 开源、云原生
PinpointJavaNaver 开源、界面友好

2.2 SkyWalking 详解

SkyWalking 简介

Apache SkyWalking 是一个开源的 APM 系统,核心特点:

  • 无侵入式监控(Java Agent)
  • 多语言支持(Java、.NET、Node.js、Go、Python)
  • 服务网格支持(Istio)
  • 强大的 UI 界面
  • 告警功能

SkyWalking 架构

code
┌─────────────────────────────────────────────────────────┐
│                    SkyWalking Agent                      │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 数据采集                                          │ │
│  │  - 字节码增强                                        │ │
│  │  - Trace 数据生成                                    │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ gRPC
┌─────────────────────────────────────────────────────────┐
│                    SkyWalking OAP                        │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 数据聚合                                          │ │
│  │  - 数据分析                                          │ │
│  │  - 告警判断                                          │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 存储
┌─────────────────────────────────────────────────────────┐
│                    Storage                               │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - Elasticsearch (推荐)                             │ │
│  │  - H2 (测试)                                        │ │
│  │  - MySQL (小规模)                                   │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 查询
┌─────────────────────────────────────────────────────────┐
│                    SkyWalking UI                         │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 调用链路查询                                      │ │
│  │  - 拓扑图                                            │ │
│  │  - 性能指标                                          │ │
│  │  - 告警管理                                          │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

SkyWalking 使用

1. Agent 配置

bash
# 启动参数
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.jar

2. agent/config/agent.config

properties
# 服务名称
agent.service_name=${SW_AGENT_NAME:user-service}

# OAP 地址
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:localhost:11800}

# 采样率(默认全采样)
agent.sample_n_per_3_secs=${SW_AGENT_SAMPLE:3}

# 忽略的路径
agent.ignore_path=${SW_AGENT_IGNORE_PATH:/actuator/**,/health}

# 日志级别
logging.level=${SW_LOGGING_LEVEL:INFO}

3. 自定义 Span

java
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());
            
            // 添加日志
            span.info("开始创建订单");
            
            // 业务逻辑
            orderRepository.save(order);
            
            span.info("订单创建成功");
            
        } 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. 注解方式

java
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);
    }
}

2.3 Zipkin 详解

Zipkin 简介

Zipkin 是 Twitter 开源的分布式追踪系统,核心特点:

  • 成熟的生态系统
  • 多语言支持
  • 简单易用
  • 与 Spring Cloud 深度集成

Zipkin 架构

code
┌─────────────────────────────────────────────────────────┐
│                  Application                             │
│  ┌────────────────────────────────────────────────────┐ │
│  │                   Brave SDK                        │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ HTTP/Kafka
┌─────────────────────────────────────────────────────────┐
│                    Zipkin Server                         │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 数据收集                                          │ │
│  │  - 数据存储                                          │ │
│  │  - 数据查询                                          │ │
│  │  - UI 展示                                           │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 存储
┌─────────────────────────────────────────────────────────┐
│                    Storage                               │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - Elasticsearch                                    │ │
│  │  - Cassandra                                        │ │
│  │  - MySQL                                            │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

Zipkin 使用

1. 依赖配置

xml
<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

yaml
spring:
  zipkin:
    base-url: http://localhost:9411
    sender:
      type: web
  sleuth:
    sampler:
      probability: 1.0  # 采样率 100%
    web:
      skip-pattern: /actuator.*,/health

3. 自定义 Span

java
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();
        }
    }
}

2.4 Jaeger 详解

Jaeger 简介

Jaeger 是 Uber 开源的分布式追踪系统,核心特点:

  • 云原生设计
  • 高性能
  • OpenTracing 兼容
  • 支持 Istio

Jaeger 架构

code
┌─────────────────────────────────────────────────────────┐
│                  Application                             │
│  ┌────────────────────────────────────────────────────┐ │
│  │                  Jaeger Client                     │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ UDP/HTTP
┌─────────────────────────────────────────────────────────┐
│                    Jaeger Agent                          │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 数据收集                                          │ │
│  │  - 数据转发                                          │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ gRPC
┌─────────────────────────────────────────────────────────┐
│                    Jaeger Collector                      │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - 数据验证                                          │ │
│  │  - 数据转换                                          │ │
│  │  - 数据存储                                          │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
        ↓ 存储
┌─────────────────────────────────────────────────────────┐
│                    Storage                               │
│  ┌────────────────────────────────────────────────────┐ │
│  │  - Elasticsearch                                    │ │
│  │  - Cassandra                                        │ │
│  │  - Kafka                                            │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

Jaeger 使用

1. 依赖配置

xml
<dependency>
    <groupId>io.jaegertracing</groupId>
    <artifactId>jaeger-client</artifactId>
    <version>1.8.1</version>
</dependency>

2. 配置

java
@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();
    }
}

2.5 选型建议

场景化推荐

Java 技术栈:

code
推荐: SkyWalking
理由:
- 无侵入,Java Agent 方式
- 国产,中文文档完善
- 功能全面(APM + Trace)
- 性能好

Spring Cloud 微服务:

code
推荐: Zipkin + Spring Cloud Sleuth
理由:
- 与 Spring Cloud 深度集成
- 配置简单
- 社区成熟

云原生环境:

code
推荐: Jaeger
理由:
- 云原生设计
- 支持 Istio
- 高性能

多语言环境:

code
推荐: Jaeger 或 SkyWalking
理由:
- 都支持多语言
- OpenTracing 兼容

三、灰度发布核心原理

3.1 为什么需要灰度发布

灰度发布解决的问题

灰度发布的目标,是让新版本先在小流量范围内验证,而不是一上来就全量切换。

它解决的核心问题是: 发布风险控制

与其让所有用户一起验证新版本,不如先让一小部分流量进入新版本,观察是否稳定,再逐步扩大范围。

传统发布 vs 灰度发布:

code
传统发布(全量发布):
┌─────────────────────────────────────────────────────┐
│  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%)             │
│  ┌──────────────────────────┐  ┌──────────────────┐ │
│  │ (空)                      │  │ 所有用户         │ │
│  │                           │  │ 新版本全量       │ │
│  └──────────────────────────┘  └──────────────────┘ │
└─────────────────────────────────────────────────────┘

优势: 逐步放量,如有问题可快速回滚,影响范围可控

3.2 灰度发布常见维度

灰度流量常见的划分方式包括:

1. 按实例比例

yaml
# 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

适用场景:

  • 通用放量
  • 不需要区分用户群体
  • 快验证新版本稳定性

2. 按用户白名单

yaml
# 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

适用场景:

  • 内部测试
  • 精准控制
  • VIP 用户优先体验

3. 按租户

java
// 基于租户 ID 的灰度路由
@Component
public class TenantGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        String tenantId = request.getContext()
            .getServerWebExchange()
            .getRequest()
            .getHeaders()
            .getFirst("X-Tenant-Id");
        
        // 根据租户 ID 选择实例
        List<ServiceInstance> instances = serviceInstanceListSupplier.get();
        ServiceInstance instance = selectInstanceByTenant(instances, tenantId);
        
        return Mono.just(new DefaultResponse(instance));
    }
}

适用场景:

  • B 端系统
  • 多租户架构
  • 不同客户独立验证

4. 按地区或机房

yaml
# 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;
}

适用场景:

  • 多地域部署
  • 地域化灰度
  • 逐步地域推广

5. 按请求头、Cookie 或设备标识

java
// 基于 Header 的灰度路由
@Component
public class HeaderGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        String grayVersion = request.getContext()
            .getServerWebExchange()
            .getRequest()
            .getHeaders()
            .getFirst("X-Gray-Version");
        
        List<ServiceInstance> instances = serviceInstanceListSupplier.get();
        
        if ("v2".equals(grayVersion)) {
            // 灰度实例
            ServiceInstance instance = selectByVersion(instances, "v2");
            return Mono.just(new DefaultResponse(instance));
        } else {
            // 正式实例
            ServiceInstance instance = selectByVersion(instances, "v1");
            return Mono.just(new DefaultResponse(instance));
        }
    }
}

适用场景:

  • 精准控制
  • 特定渠道灰度
  • AB 测试

3.3 灰度发布与可观测性的关系

为什么灰度必须配合可观测性

灰度发布如果没有可观测性支撑,本质上就是:

蒙着眼放量

灰度期间,至少要能够观察:

技术指标:

  • 错误率
  • RT (响应时间)
  • 下游依赖成功率
  • 线程池、连接池等资源状态
  • CPU、内存使用率

业务指标:

  • 下单成功率
  • 支付成功率
  • 转化率
  • 业务错误率

否则即使 CPU、内存看起来都正常,业务结果也可能已经出问题。

所以灰度能不能安全推进,核心不在于"放量按钮存不存在",而在于: 放量决策是否建立在真实数据上

灰度监控指标

code
灰度监控指标体系:
├─ 技术指标
│   ├─ 错误率: 灰度版本 vs 基线版本
│   ├─ RT: P50、P95、P99
│   ├─ QPS: 灰度流量占比
│   └─ 资源使用: CPU、内存、线程池
├─ 业务指标
│   ├─ 核心业务成功率: 下单、支付
│   ├─ 转化率: 访问 → 下单 → 支付
│   └─ 业务错误率: 业务异常占比
└─ 对比指标
    ├─ 灰度版本 vs 基线版本
    ├─ 灰度前 vs 灰度后
    └─ 历史同期对比

四、灰度发布实现方案

4.1 网关层灰度

Spring Cloud Gateway 灰度路由

java
// 灰度路由过滤器
@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;
    }
}
yaml
# 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

4.2 服务层灰度

Nacos 元数据标记

yaml
# 灰度实例配置
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        metadata:
          version: v2  # 版本标记
          gray: true   # 灰度标记

灰度负载均衡

java
// 灰度负载均衡器
@Component
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    
    @Autowired
    private GrayRuleConfig grayRuleConfig;
    
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 获取灰度标识
        String grayVersion = getGrayVersion(request);
        
        // 获取所有实例
        List<ServiceInstance> instances = serviceInstanceListSupplier.get();
        
        // 根据灰度规则选择实例
        ServiceInstance instance = selectInstance(instances, grayVersion);
        
        return Mono.just(new DefaultResponse(instance));
    }
    
    private String getGrayVersion(Request request) {
        // 从 Header 获取
        String version = request.getContext()
            .getServerWebExchange()
            .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 loadBalancer.choose(filtered);
    }
}

4.3 Istio 服务网格灰度

Istio 灰度发布配置

yaml
# 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
    
    # 熔断配置
    fault:
      abort:
        percentage:
          value: 0.1
        httpStatus: 503

4.4 特性开关灰度

使用配置中心实现特性开关

java
// 特性开关配置
@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);
        }
    }
}
yaml
# Nacos 配置
feature:
  new-order-flow:
    enabled: true
    percentage: 10  # 10% 用户使用新流程
    whitelist: 12345,67890  # 白名单用户

五、实战场景

5.1 场景一:订单服务新版本灰度

场景描述

订单服务上线新版本,需要灰度验证。

实现方案

1. 实例标记

yaml
# v1 版本实例
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        metadata:
          version: v1

# v2 版本实例
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        metadata:
          version: v2

2. 灰度路由

java
@Component
public class OrderGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        String grayVersion = request.getContext()
            .getServerWebExchange()
            .getRequest()
            .getHeaders()
            .getFirst("X-Gray-Version");
        
        List<ServiceInstance> instances = serviceInstanceListSupplier.get();
        
        // 根据灰度版本选择实例
        ServiceInstance instance = selectByVersion(instances, grayVersion);
        
        return Mono.just(new DefaultResponse(instance));
    }
}

3. 灰度流程

code
灰度流程:
1. 部署 v2 版本
    ↓
2. 5% 流量灰度(内部测试)
    - 监控错误率、RT
    - 观察业务指标
    ↓
3. 10% 流量灰度
    - 继续监控
    - 对比 v1 vs v2
    ↓
4. 30% 流量灰度
    - 扩大灰度范围
    - 关注告警
    ↓
5. 50% 流量灰度
    - 半量验证
    ↓
6. 100% 流量灰度
    - 全量切换
    ↓
7. 下线 v1 版本

4. 监控指标

code
灰度期间重点监控:
├─ 技术指标
│   ├─ 错误率: v2 < v1 * 1.1
│   ├─ RT: v2 < v1 * 1.2
│   └─ 资源使用: CPU < 70%, Memory < 80%
├─ 业务指标
│   ├─ 下单成功率: v2 ≥ v1
│   ├─ 支付成功率: v2 ≥ v1
│   └─ 订单量: v2 ≈ v1
└─ 告警规则
    ├─ 错误率 > 1% → 告警
    ├─ RT P95 > 500ms → 告警
    └─ 业务成功率下降 > 5% → 告警

5.2 场景二:慢调用排查

场景描述

用户反馈"下单很慢",需要快速定位问题。

解决方案

1. 查询链路追踪

code
在 SkyWalking UI 查询:
- TraceId: 从用户请求获取
- 或根据时间范围查询慢 Trace

查看链路详情:
┌─────────────────────────────────────────────────────┐
│ TraceId: 1234567890abcdef                           │
│ Total Duration: 850ms                               │
├─────────────────────────────────────────────────────┤
│ 网关 → 订单服务                                      │
│   Duration: 850ms                                   │
│   ├─ 订单服务 → 库存服务                             │
│   │   Duration: 200ms                               │
│   │   └─ 库存服务 → MySQL                            │
│   │       Duration: 150ms ← 慢!                     │
│   ├─ 订单服务 → 支付服务                             │
│   │   Duration: 300ms ← 慢!                         │
│   │   └─ 支付服务 → 第三方支付                       │
│   │       Duration: 280ms                           │
│   └─ 订单服务 → Redis                                │
│       Duration: 10ms                                │
└─────────────────────────────────────────────────────┘

2. 分析性能瓶颈

code
性能瓶颈分析:
1. 支付服务调用第三方支付: 280ms (可优化空间小)
2. 库存服务数据库查询: 150ms (可能存在慢 SQL)
3. 订单服务内部处理: 100ms (可接受)

优化建议:
1. 检查库存服务 SQL:
   - 是否缺少索引
   - 是否存在全表扫描
   - 是否锁等待

2. 支付服务优化:
   - 异步化非核心流程
   - 使用缓存

3. 查看慢 SQL

bash
# 从 SkyWalking 查看 SQL 详情
Operation: SELECT * FROM inventory WHERE product_id = ?
Duration: 150ms
TraceId: 1234567890abcdef

# 分析 SQL 执行计划
EXPLAIN SELECT * FROM inventory WHERE product_id = ?;

5.3 场景三:特性开关灰度

场景描述

新功能不一定需要直接发新版,也可以配合配置中心和特性开关灰度。

实现方案

java
// 特性开关管理
@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);
        }
    }
}

六、最佳实践

6.1 链路追踪最佳实践

采样率设置

code
采样率建议:
- 开发环境: 100%
- 测试环境: 100%
- 预发环境: 50%
- 生产环境: 1-10%

注意:
- 关键链路: 100% 采样
- 非关键链路: 采样率可低
- 错误请求: 必须 100% 采样

TraceId 透传规范

code
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

性能影响

code
性能优化建议:
1. 异步上报数据
2. 批量发送 Span
3. 合理设置采样率
4. 使用高效的序列化方式
5. Agent 使用独立的线程池

6.2 灰度发布最佳实践

灰度流程规范

code
标准灰度流程:
1. 代码评审 + 测试环境验证
    ↓
2. 预发环境验证
    ↓
3. 生产环境灰度
    ├─ 5% (内部测试)
    ├─ 10% (小范围验证)
    ├─ 30% (扩大范围)
    ├─ 50% (半量验证)
    └─ 100% (全量切换)
    ↓
4. 观察期(24-72小时)
    ↓
5. 下线旧版本

回滚预案

code
回滚预案:
1. 快速回滚
   - 调整流量权重
   - 关闭特性开关
   - 下线灰度实例

2. 代码回滚
   - 保留旧版本代码
   - 准备回滚脚本
   - 测试回滚流程

3. 数据回滚
   - 数据库变更可逆
   - 数据迁移脚本
   - 数据校验脚本

监控告警

code
灰度监控体系:
├─ 实时监控
│   ├─ 错误率监控
│   ├─ RT 监控
│   └─ 业务指标监控
├─ 对比分析
│   ├─ v1 vs v2
│   └─ 灰度前后对比
├─ 告警规则
│   ├─ 错误率 > 阈值
│   ├─ RT > 阈值
│   └─ 业务成功率下降
└─ 值班机制
    ├─ 灰度期间值班
    └─ 快速响应

七、常见问题与排查

7.1 链路追踪问题排查

问题一:链路信息不完整

现象:

  • 链路图出现断层
  • 部分服务没有 Trace 信息

排查步骤:

code
1. 检查 traceId 是否在网关或入口处生成
   - 查看网关日志
   - 检查 TraceIdFilter

2. 检查 HTTP、RPC、MQ 调用是否透传上下文
   - 查看请求头
   - 检查消息头

3. 检查异步线程或线程池切换时是否丢失 trace
   - 使用装饰器模式
   - 手动透传 TraceContext

4. 检查日志、指标和 trace 是否能互相关联
   - 日志中是否包含 traceId
   - 指标是否包含 trace 维度

问题二:性能影响

现象:

  • 接入链路追踪后服务性能下降

排查步骤:

code
1. 检查采样率
   - 采样率过高
   - 建议: 生产环境 1-10%

2. 检查上报方式
   - 同步上报 → 改为异步
   - 单条上报 → 改为批量

3. 检查序列化方式
   - JSON 序列化较慢
   - 考虑二进制序列化

4. 检查 Agent 配置
   - 缓冲区大小
   - 上报间隔

7.2 灰度发布问题排查

问题一:灰度流量不可控

现象:

  • 灰度流量超过预期
  • 无法快速回滚

排查步骤:

code
1. 检查灰度规则配置
   - 权重配置是否正确
   - 白名单配置是否正确

2. 检查灰度标识透传
   - Header 是否正确透传
   - Cookie 是否正确解析

3. 检查负载均衡策略
   - 是否支持灰度路由
   - 是否有缓存导致规则不生效

4. 检查回滚流程
   - 回滚脚本是否准备好
   - 回滚是否测试过

问题二:灰度流量未命中

现象:

  • 灰度规则配置了,但流量没有命中

排查步骤:

code
1. 检查实例元数据
   - 版本标记是否正确
   - 是否已注册到注册中心

2. 检查路由规则
   - 路由条件是否正确
   - 优先级是否合理

3. 检查请求特征
   - Header 是否包含灰度标识
   - 用户 ID 是否在白名单中

4. 检查日志
   - 灰度路由日志
   - 负载均衡日志

八、常见误区

8.1 链路追踪误区

误区一:只有日志,没有 trace 维度

× 错误理解:

日志里有 traceId 字符串就够了,不需要专门的链路追踪系统。

正确理解:

没有统一 trace 标识,排障时只能人工拼接多服务日志,效率非常低。

链路追踪系统的价值:

  • 自动关联调用链路
  • 可视化展示
  • 性能分析
  • 依赖分析

误区二:只做服务间 HTTP trace,不做 MQ 和异步透传

× 错误理解:

只追踪 HTTP 调用就够了,MQ 和异步任务不需要追踪。

正确理解:

这样会导致链路断层,很多真实问题仍然定位不完整。

完整追踪范围:

  • HTTP 调用
  • RPC 调用
  • MQ 消息
  • 异步任务
  • 数据库访问
  • 缓存访问

8.2 灰度发布误区

误区三:灰度只看技术指标,不看业务成功率

× 错误理解:

只要 CPU、内存、错误率正常,灰度就是成功的。

正确理解:

技术指标正常,不代表业务结果一定正常。

必须关注的业务指标:

  • 下单成功率
  • 支付成功率
  • 转化率
  • 业务错误率

误区四:灰度流量不可控,出了问题无法快速缩回

× 错误理解:

灰度规则配置后就不用管了,出问题再说。

正确理解:

不能快速回退的灰度,不是真正意义上的灰度治理。

灰度必须满足:

  • 能明确命中范围
  • 能快速调整比例
  • 能快速回退
  • 能区分灰度流量与正式流量

误区五:全量发布前没有回滚预案

× 错误理解:

发布前不需要准备回滚方案,出了问题再说。

正确理解:

发布前不准备回滚路径,本质上是在拿线上环境做不可逆试验。

回滚预案:

  • 快速回滚方案
  • 代码回滚方案
  • 数据回滚方案
  • 回滚测试

九、面试要点

9.1 链路追踪相关问题

Q1: 链路追踪的核心价值是什么?

答案:

快速定位是哪一跳出了问题,并把一次请求经过的服务、依赖和耗时串起来。

展开:

核心价值:

  • 问题定位 - 快速找到故障点
  • 性能分析 - 发现性能瓶颈
  • 依赖分析 - 理解服务依赖关系
  • 故障还原 - 还原故障现场

关键能力:

  • TraceId 生成与透传
  • Span 数据采集
  • 链路可视化
  • 性能分析

Q2: TraceId 和 Span 的关系是什么?

答案:

TraceId:

  • 一次完整请求的唯一标识
  • 同一次请求的所有 Span 共享一个 TraceId

Span:

  • 链路中的一个具体调用片段
  • 记录调用信息(时间、耗时、结果)

关系:

  • 一个 TraceId 包含多个 Span
  • Span 之间有父子关系
  • Span 通过 TraceId 关联

9.2 灰度发布相关问题

Q3: 为什么灰度发布能降低发布风险?

答案:

因为问题可以先在小范围暴露,并在影响可控时快速回滚。

展开:

风险控制:

  • 小范围验证
  • 逐步放量
  • 快速回滚
  • 影响可控

对比全量发布:

  • 全量发布: 所有用户新版本,问题影响大
  • 灰度发布: 部分用户新版本,问题影响小

Q4: 灰度发布为什么必须配合可观测性?

答案:

因为放量决策必须建立在真实数据上,而不是凭感觉判断"应该没问题"。

展开:

可观测性要求:

  • 技术指标监控
  • 业务指标监控
  • 对比分析能力
  • 告警能力

决策依据:

  • 数据驱动
  • 指标对比
  • 趋势分析

Q5: 业务指标为什么比单纯 CPU、内存更重要?

答案:

因为业务结果正常,才代表这次发布真正成功。

展开:

技术指标 vs 业务指标:

code
技术指标:
- CPU、内存使用率正常
- 错误率正常
- RT 正常

业务指标:
- 下单成功率下降
- 支付成功率下降
- 转化率下降

结论:
技术指标正常 ≠ 业务正常
业务指标正常 = 发布成功

Q6: 如何设计灰度发布的回滚方案?

答案:

回滚方案:

code
1. 快速回滚
   - 调整流量权重
   - 关闭特性开关
   - 下线灰度实例

2. 代码回滚
   - 保留旧版本代码
   - 准备回滚脚本
   - 测试回滚流程

3. 数据回滚
   - 数据库变更可逆
   - 数据迁移脚本
   - 数据校验脚本

4. 回滚测试
   - 测试环境验证
   - 预发环境验证
   - 回滚演练

十、小结

理解链路追踪与灰度发布,至少要掌握这些点:

  • 链路追踪解决的是"请求经过哪里、哪一跳出了问题"
  • 灰度发布解决的是"新版本如何小范围验证、逐步放量"
  • 链路追踪不能只覆盖 HTTP,还要覆盖 MQ、异步和线程切换
  • 灰度能不能安全推进,核心取决于可观测性是否完整
  • 真正成熟的发布治理,必须做到可观测、可放量、可回退

核心技术栈:

  • 链路追踪: SkyWalking、Zipkin、Jaeger
  • 灰度发布: Istio、Spring Cloud Gateway、Nacos
  • 可观测性: Prometheus、Grafana、ELK

核心能力:

  • 链路追踪数据采集
  • TraceId 生成与透传
  • 灰度路由策略
  • 灰度监控告警
  • 回滚预案

持续演进:

  • 服务网格 (Istio)
  • 云原生可观测性
  • 智能灰度发布
  • 自动化回滚

版本差异(Spring Cloud 旧版 → 2025.x)

特性旧版(本文编写时)当前(Spring Cloud 2025.x / Boot 3.5.x)
架构组件Eureka/Ribbon/Hystrix/Zuul(已停止维护)LoadBalancer/Gateway/Resilience4j/Nacos
版本对应Hoxton/2020.x + Boot 2.x + JDK 82025.x + Boot 3.5.x + JDK 17+
云原生初步支持全面云原生(K8s、虚拟线程、GraalVM 支持)
链路追踪Sleuth + ZipkinMicrometer Tracing + Zipkin/OTel

本文为通用微服务概念讲解,架构思想长期有效;落地时请采用 Spring Cloud 2025.x + Spring Boot 3.5.x 技术栈(JDK 17+)。