网关全链路灰度标透传:Header 在 Feign 调用中丢失?ThreadLocal + RequestInterceptor 完美接力!

做过微服务灰度发布的同学肯定都遇到过这个问题:在网关层设置了灰度标记 Header,但经过 Feign 调用后,这个 Header 就神秘消失了。导致后端服务无法正确识别灰度流量,灰度发布变成了全量发布,引发线上事故。

我之前就遇到过这样一个案例:业务同学做灰度发布,在网关层设置了 X-Gray-Group: v2 的 Header,但经过几次 Feign 调用后,这个标记就丢失了。结果新版本的代码被所有用户访问到,导致部分用户看到了未完成的功能,影响了业务体验。

今天我们就来聊聊全链路灰度标透传的解决方案,让您的灰度标记在整个调用链路上畅通无阻。

灰度标丢失的根本原因

1. HTTP 请求头不会自动传递

这是最核心的问题:

调用链:
网关 → Service A → Service B → Service C

问题场景:
1. 用户请求到达网关,设置了 X-Gray-Group: v2
2. 网关调用 Service A,Header 传递成功
3. Service A 调用 Service B,Header 丢失!
4. Service B 调用 Service C,没有灰度标记

原因:Feign 默认不会传递自定义 Header

2. ThreadLocal 在异步调用中失效

异步调用问题:

主线程设置 ThreadLocal:
ThreadLocal<String> grayGroup = new ThreadLocal<>();
grayGroup.set("v2");

异步线程获取:
CompletableFuture.runAsync(() -> {
    String group = grayGroup.get(); // 返回 null!
});

原因:ThreadLocal 是线程隔离的,异步线程无法访问主线程的值

3. 请求上下文丢失

请求上下文传递问题:

过滤器设置上下文:
RequestContext.set("grayGroup", "v2");

Feign 调用时:
@FeignClient("service-b")
interface ServiceBClient {
    @GetMapping("/api/data")
    String getData();
}

// Feign 内部使用独立的 RequestTemplate,不会自动携带上下文

解决方案:ThreadLocal + RequestInterceptor

1. 核心设计思想

我们的方案核心是三个关键步骤:

  1. 入口拦截:在网关或过滤器中提取灰度标记并存储到 ThreadLocal
  2. Feign 拦截:通过 RequestInterceptor 自动注入灰度标记
  3. 异步支持:利用 TransmittableThreadLocal 支持异步线程传递

架构图如下:

┌─────────────────────────────────────────────────────────────────┐
│                   全链路灰度标透传架构                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌────────────────┐    ┌────────────────┐      │
│  │  用户请求 │───→│  网关过滤器    │───→│  ThreadLocal   │      │
│  │          │    │  (提取灰度标)   │    │  (存储灰度标)   │      │
│  └──────────┘    └────────────────┘    └────────────────┘      │
│                                                      │         │
│                                                      ▼         │
│  ┌──────────┐    ┌────────────────┐    ┌────────────────┐      │
│  │ Service  │───→│  Feign Client  │───→│RequestInterceptor│    │
│  │    A     │    │                │    │ (注入灰度标)    │      │
│  └──────────┘    └────────────────┘    └────────────────┘      │
│                                                      │         │
│                                                      ▼         │
│  ┌──────────┐    ┌────────────────┐    ┌────────────────┐      │
│  │ Service  │───→│  Feign Client  │───→│RequestInterceptor│    │
│  │    B     │    │                │    │ (注入灰度标)    │      │
│  └──────────┘    └────────────────┘    └────────────────┘      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. 灰度标记上下文

// 灰度标记上下文
class GrayContext {
    // 使用 TransmittableThreadLocal 支持异步传递
    private static final TransmittableThreadLocal<String> GRAY_GROUP = 
            new TransmittableThreadLocal<>();
    
    private static final String GRAY_HEADER = "X-Gray-Group";
    
    static void setGrayGroup(String group) {
        GRAY_GROUP.set(group);
    }
    
    static String getGrayGroup() {
        return GRAY_GROUP.get();
    }
    
    static void clear() {
        GRAY_GROUP.remove();
    }
    
    static String getHeaderName() {
        return GRAY_HEADER;
    }
}

3. 入口过滤器

// 入口过滤器
class GrayHeaderFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 提取灰度标记
        String grayGroup = httpRequest.getHeader(GrayContext.getHeaderName());
        
        if (grayGroup != null && !grayGroup.isEmpty()) {
            GrayContext.setGrayGroup(grayGroup);
            log.info("Set gray group: {}", grayGroup);
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            // 清理上下文,防止内存泄漏
            GrayContext.clear();
        }
    }
}

4. Feign 请求拦截器

// Feign 请求拦截器
class GrayRequestInterceptor implements RequestInterceptor {
    
    @Override
    public void apply(RequestTemplate template) {
        String grayGroup = GrayContext.getGrayGroup();
        
        if (grayGroup != null && !grayGroup.isEmpty()) {
            // 将灰度标记注入到请求头
            template.header(GrayContext.getHeaderName(), grayGroup);
            log.debug("Inject gray header: {}={}", 
                    GrayContext.getHeaderName(), grayGroup);
        }
    }
}

5. 异步场景支持

// 异步任务包装器
class GrayAsyncWrapper {
    
    static <T> CompletableFuture<T> wrapAsync(Supplier<T> supplier) {
        // 捕获当前上下文
        String grayGroup = GrayContext.getGrayGroup();
        
        return CompletableFuture.supplyAsync(() -> {
            // 在异步线程中恢复上下文
            GrayContext.setGrayGroup(grayGroup);
            
            try {
                return supplier.get();
            } finally {
                GrayContext.clear();
            }
        });
    }
}

最佳实践与注意事项

1. 自定义 Header 白名单

白名单配置:

gray:
  headers:
    - X-Gray-Group
    - X-Trace-Id
    - X-Request-Id
    - X-User-Id

2. 全局配置 Feign Interceptor

// 全局配置
@Configuration
class FeignConfig {
    
    @Bean
    public RequestInterceptor grayInterceptor() {
        return new GrayRequestInterceptor();
    }
    
    @Bean
    public RequestInterceptor traceInterceptor() {
        return new TraceRequestInterceptor();
    }
}

3. 网关层透传配置

# Spring Cloud Gateway 配置
spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=X-Gray-Group, {gray-group}
      routes:
        - id: service-a
          uri: lb://service-a
          predicates:
            - Path=/api/service-a/**

4. 响应头透传

// 响应头透传过滤器
class GrayResponseFilter implements GlobalFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange)
                .then(Mono.fromRunnable(() -> {
                    String grayGroup = GrayContext.getGrayGroup();
                    if (grayGroup != null) {
                        exchange.getResponse()
                                .getHeaders()
                                .set(GrayContext.getHeaderName(), grayGroup);
                    }
                }));
    }
}

5. 监控与日志

日志埋点:

function logGrayInfo(request):
    grayGroup = GrayContext.getGrayGroup()
    if grayGroup:
        log.info("[Gray] group={}, uri={}", grayGroup, request.uri)

效果对比

方案Header 传递异步支持复杂度适用场景
手动传递简单场景
ThreadLocal同步调用
TransmittableThreadLocal全链路

总结

全链路灰度标透传的核心原则:

  1. 入口拦截:在网关或过滤器中提取并存储灰度标记
  2. ThreadLocal 存储:使用 TransmittableThreadLocal 支持异步传递
  3. Feign 拦截:通过 RequestInterceptor 自动注入灰度标记
  4. 响应透传:将灰度标记传递回客户端
  5. 资源清理:使用 try-finally 确保上下文被清理

记住:灰度标记就像接力棒,需要在每个环节正确传递。通过 ThreadLocal + RequestInterceptor 的组合,可以让灰度标记在整个调用链路上畅通无阻。


源码获取

文章已同步至小程序博客栏目,需要源码的请关注小程序博客。

公众号:服务端技术精选

小程序码:


标题:网关全链路灰度标透传:Header 在 Feign 调用中丢失?ThreadLocal + RequestInterceptor 完美接力!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/05/25/1779201758357.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消