一、背景
在灰度部署、A/B测试、单元化部署等场景下,微服务服务之间的调用,要求我们对上游服务给过来的数据进行透传至下游服务。
如果是灰度部署,需要对http请求进行染色,http header头部增加灰度标识,然后传递给下游服务。这个传递就跟击鼓传花一样,谁都不能丢弃掉这个灰度标识。
可现实是,我们的服务在执行的过程中,极容易把这个灰度标识丢掉了。(当然不是故意的)
如果程序的执行顺序都是串行的,那当然不会丢了上下文中的数据。现实中的程序执行,为了高性能和解耦,我们就会使用线程池、本地异步事件、mq异步、redis数据订阅与发布等技术,使得上下文的数据丢掉了,无法继续透传下去了。
为了做到数据透传,我们一般有两种做法,一是java agent技术,另外就是在服务中,在拦截器中增加数据的赋值。
验证数据透传的结果,一般的做法是输入日志,通过唯一标识去跟踪数据透传到哪一步。比如http feign调用框架,你就可以使用wireshark抓包来验证。(本文就使用后者来验证feign框架的数据透传)
二、目标
- 支持feign调用的数据透传
- 支持父子线程和线程池等的数据透传
- 能够验证数据透传的结果
三、边界
服务之间的数据透传,不包括rabbitmq、redis等第三方中间件的透传。
四、关键思路
-
父子线程的上下文传递,包括线程池等并发工具类,可以使用阿里云的开源框架transmittable-thread-local
-
抓http协议的包,你可以使用charles,也可以使用强大的抓包工具:wireshark(说后者强大,是因为它还可以抓websocket等协议的包)
-
feign调用的参数传递,使用RequestInterceptor拦截器,在头部增加你的参数进行调用
五、feign拦截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class CustomFeignInterceptor implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(CustomFeignInterceptor.class);
/**
* FeignClient调用时传递的用户id
*/
public static String XX_USER_ID = "XX_USER_ID";
/**
* FeignClient调用时传递的学校id
*/
public static String XX_SCHOOL_ID = "XX_SCHOOL_ID";
@Autowired
XxTransmittableThreadLocal<TransmittedUserInfo> transmittableThreadLocal;
@Override
public void apply(RequestTemplate template) {
TransmittedUserInfo userInfo = transmittableThreadLocal.get();
if (userInfo != null) {
if (StringUtils.isNotEmpty(userInfo.getUserId())){
template.header(CustomFeignInterceptor.XX_USER_ID, userInfo.getUserId());
}
if (StringUtils.isNotEmpty(userInfo.getSchoolId())){
template.header(CustomFeignInterceptor.XX_SCHOOL_ID, userInfo.getSchoolId());
}
}
}
}
- 拦截器的实例化(当然你可以额外增加开关)
@Bean
public RequestInterceptor customFeignInterceptor() {
return new CustomFeignInterceptor();
}
六、上下文类-阿里ttl框架
import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class XxTransmittableThreadLocal<T> {
private static final Logger log = LoggerFactory.getLogger(XxTransmittableThreadLocal.class);
private TransmittableThreadLocal<T> threadLocal = new TransmittableThreadLocal();
public void set(T value) {
threadLocal.set(value);
}
public T get() {
return threadLocal.get();
}
}
七、把要透传的数据传递至上下文
在自定义feign拦截器里,我们从上下文中取出了需透传的值。现在,我们将详细说明,在controller拦截器中,注入透传数据至上下文。
- spring servlet 过滤器之OncePerRequestFilter
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class InfoTransmitFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(InfoTransmitFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authUserId = request.getHeader(JwtAuthHeaders.AUTH_USER_ID);
String authSchoolId = request.getHeader(JwtAuthHeaders.AUTH_SCHOOL_ID);
if (StringUtils.isNotBlank(authUserId)) {
TransmittedUserInfo userInfo = new TransmittedUserInfo(authUserId, authSchoolId);
XxTransmittableThreadLocal<TransmittedUserInfo> xxTransmittableThreadLocal = (XxTransmittableThreadLocal<TransmittedUserInfo>) ApplicationContextProvider
.getApplicationContext()
.getBean("xxTransmittableThreadLocal");
// 把需透传的用户信息,存储到上下文
xhTransmittableThreadLocal.set(userInfo);
}
filterChain.doFilter(request, response);
}
}
- 过滤器的实例化(可以额外增加开关)
@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public FilterRegistrationBean infoTransmitFilterRegBean() {
InfoTransmitFilter infoTransmitFilter = new InfoTransmitFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(infoTransmitFilter);
filterRegistrationBean.setOrder(Ordered.LOWEST_PRECEDENCE);
return filterRegistrationBean;
}
- REACTIVE框架(如果你使用的非servlet框架)
@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public InfoTransmitWebFilter infoTransmitWebFilter() {
return new InfoTransmitWebFilter();
}
- InfoTransmitWebFilter的实现和InfoTransmitFilter类似,详情见下:
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
public class InfoTransmitWebFilter implements WebFilter {
public static final int ORDER = Ordered.LOWEST_PRECEDENCE;
private static final Logger logger = LoggerFactory.getLogger(InfoTransmitWebFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
final String userId = exchange.getRequest().getHeaders().getFirst(JwtAuthHeaders.AUTH_USER_ID);
final String authSchoolId = exchange.getRequest().getHeaders().getFirst(JwtAuthHeaders.AUTH_SCHOOL_ID);
if (StringUtils.isNotBlank(userId)) {
TransmittedUserInfo userInfo = new TransmittedUserInfo(userId, authSchoolId);
XxTransmittableThreadLocal<TransmittedUserInfo> xxTransmittableThreadLocal = (XxTransmittableThreadLocal<TransmittedUserInfo>) ApplicationContextProvider
.getApplicationContext()
.getBean("xxTransmittableThreadLocal");
// 把需透传的用户信息,存储到上下文
xhTransmittableThreadLocal.set(userInfo);
}
return chain.filter(exchange);
}
}
八、wireshark抓包
开启抓包
# 这里输入过滤条件
ip.addr == 192.168.5.70 and tcp.port == 7584
使用Postman调用接口,开始测试
找到http协议的报文,见"Hypertext Transfer Protocol"部分:
如果你想要去看那些tcp协议的报文,可以进一步帅选:
ip.addr == 192.168.5.70 and tcp.port == 7584 and http