网关全链路灰度标透传: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. 核心设计思想
我们的方案核心是三个关键步骤:
- 入口拦截:在网关或过滤器中提取灰度标记并存储到 ThreadLocal
- Feign 拦截:通过 RequestInterceptor 自动注入灰度标记
- 异步支持:利用 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 | ✅ | ✅ | 中 | 全链路 |
总结
全链路灰度标透传的核心原则:
- 入口拦截:在网关或过滤器中提取并存储灰度标记
- ThreadLocal 存储:使用 TransmittableThreadLocal 支持异步传递
- Feign 拦截:通过 RequestInterceptor 自动注入灰度标记
- 响应透传:将灰度标记传递回客户端
- 资源清理:使用 try-finally 确保上下文被清理
记住:灰度标记就像接力棒,需要在每个环节正确传递。通过 ThreadLocal + RequestInterceptor 的组合,可以让灰度标记在整个调用链路上畅通无阻。
源码获取
文章已同步至小程序博客栏目,需要源码的请关注小程序博客。
公众号:服务端技术精选
小程序码:
标题:网关全链路灰度标透传:Header 在 Feign 调用中丢失?ThreadLocal + RequestInterceptor 完美接力!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/05/25/1779201758357.html
公众号:服务端技术精选
评论
0 评论