gray-transmit
介绍
灰度发布组件:在网关层按用户维度进行流量分配,并将灰度标记续传到后续每一个节点
工作流程
- 前端发起Http请求,header中携带用户信息
- 网关gateway全局拦截器GlobalFilter获取用户信息,根据用户查询灰度用户配置
- 如果当前用户是灰度用户,则在负载均衡时,选择灰度的实例,如果没有后续节点没有灰度节点,那就选择正式服务;如果当前用户是正式用户,走正式服务
- 业务组件Mvc请求拦截器,如果header中的gray标记为true,则将灰度续传到下一个节点,LoadBalancer选择灰度实例。
具体实现
-
gray-common灰度发布组件实现:
-
实现HandlerInterceptor接口,对Http请求进行灰度过滤,如果header中gray=true,将GrayRequestContextHolder中的灰度标记设置为ture,用ThreadLocal 保存gray状态,作为在后续负载均衡选择灰度服务的依据。
public class GrayHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String gray = request.getHeader("gray"); // 如果HttpHeader中灰度标记为true,则将灰度标记放到holder中,如果需要就传递下去 if (gray != null && gray.equals("true")) { GrayRequestContextHolder.setGrayTag(true); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { GrayRequestContextHolder.remove(); } }public class GrayRequestContextHolder { private static final ThreadLocal<Boolean> GARY_TAG = new ThreadLocal<>(); public static void setGrayTag(final Boolean tag) { GARY_TAG.set(tag); } public static Boolean getGrayTag() { return GARY_TAG.get(); } public static void remove() { GARY_TAG.remove(); } }将上述GrayHandlerInterceptor灰度拦截器,配置到MVC拦截器中,拦截请求
@Configuration @ConditionalOnClass(value = WebMvcConfigurer.class) public class GrayMvcConfig { /** * Spring MVC 请求拦截器 * @return WebMvcConfigurer */ @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new GrayHandlerInterceptor()); } }; } } -
自定义灰度负载均衡,实现springcloud的Loadbalancer
@Slf4j public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer { private AtomicInteger position; private final String serviceId; private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; public GrayLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) { this.position = new AtomicInteger(0); this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getIfAvailable(NoopServiceInstanceListSupplier::new); return supplier.get(request).next() .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, request)); } private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) { Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, request); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) { if (instances.isEmpty()) { if (log.isWarnEnabled()) { log.warn("No servers available for service: " + serviceId); } return new EmptyResponse(); } // 获取ServiceInstance列表 instances = getInstances(instances, request); if (instances.size() == 1) { return new DefaultResponse(instances.get(0)); } // 轮训策略 int pos = this.position.incrementAndGet(); ServiceInstance instance = instances.get(pos % instances.size()); return new DefaultResponse(instance); } private List<ServiceInstance> getInstances(List<ServiceInstance> instances, Request request) { // 获取灰度标记 String gray = GrayRequestContextHolder.getGrayTag().toString(); // 灰度标记不为空并且标记为true, 筛选ServiceInstance if (StringUtils.isNotEmpty(gray) && StringUtils.equals("true", gray)) { List<ServiceInstance> list = new ArrayList<>(); for (ServiceInstance instance : instances) { if (StringUtils.isNotEmpty(instance.getMetadata().get("gray")) && "true".equalsIgnoreCase(instance.getMetadata().get("gray"))) { list.add(instance); } } // 如果没有灰度实例,则转正式服务 return !list.isEmpty() ? list : instances; } else { return instances; } } }getInstances方法中,取出本地线程变量中gray标记,根据标记过滤灰度服务实例;如果没有灰度实例,则走正式服务,保证业务正常使用,这里可根据具体业务场景进行改写
-
将自定义负载均衡器实例化
@Configuration public class LoadBalancerGrayAutoConfiguration { @Bean @ConditionalOnBean(LoadBalancerClientFactory.class) public ReactorLoadBalancer<ServiceInstance> grayReactorLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new GrayLoadBalancer(name, loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class)); } }
在开启灰度负载均衡,只需要在业务组件启动类上添加:
@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})
-
-
改造客户端http请求拦截器,从ThreadLocal中取出灰度标记,并续传
@Slf4j public class GrayRuleInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpHeaders headers = request.getHeaders(); log.info("灰度标记:{}", GrayRequestContextHolder.getGrayTag()); headers.add("gray", GrayRequestContextHolder.getGrayTag().toString()); return execution.execute(request, body); } }将客户端http请求拦截器放入RestTemplate,使RestTemplate实现灰度在服务间续传
@Configuration public class RestConfig { @LoadBalanced @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); List<ClientHttpRequestInterceptor> list = new ArrayList<>(); list.add(new GrayRuleInterceptor()); restTemplate.setInterceptors(list); return restTemplate; } }
-
-
Gateway灰度改造
在gateway中实现GlobalFilter接口、Ordered接口。在filter方法中,根据请求头中的USER_ID,查询灰度配置表tb_gray_user,如果配置是Y,则将这条链路设置为灰度gray=true,保存在本地线程变量ThreadLocal中,为网关负载均衡获取灰度实例备用
public class GrayPublishFilter implements GlobalFilter, Ordered {
private IGrayService grayService;
public GrayPublishFilter(IGrayService grayService) {
this.grayService = grayService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GrayRequestContextHolder.setGrayTag(false);
// 从header中获取用户信息
HttpHeaders headers = exchange.getRequest().getHeaders();
String userId = Objects.requireNonNull(headers.get("USER_ID")).get(0);
// 查询用户的灰度信息
GrayUser user = GrayUser.builder().userId(userId).build();
GrayUser grayUser = grayService.queryUserById(user);
// Y:需要走灰度服务
if ("Y".equalsIgnoreCase(grayUser.getGray())) {
GrayRequestContextHolder.setGrayTag(true);
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.header("gray", GrayRequestContextHolder.getGrayTag().toString())
.build();
ServerWebExchange newExchange = exchange.mutate()
.request(newRequest)
.build();
return chain.filter(newExchange);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
将GrayPublishFilter灰度发布过滤器生效
@Configuration
public class GrayConfig {
@Bean
public GlobalFilter globalFilter(IGrayService grayService) {
return new GrayPublishFilter(grayService);
}
}
- 灰度用户配置的增删改查接口封装在gray-common组件,在网关层对外暴露
数据库配置:
create table tb_gray_user
(
id int auto_increment comment '唯一id'
primary key,
user_id varchar(11) not null comment '用户id',
gray varchar(1) default 'N' not null comment '是否灰度用户'
)
comment '灰度用户配置表';
使用说明
-
在gateway组件引入gray-common组件包
<dependency> <groupId>com.sky</groupId> <artifactId>gray-common</artifactId> <version>1.0.1</version> </dependency>-
启动类上@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})
@SpringBootApplication @Configurable @EnableEurekaClient @LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class}) public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } } -
gateway组件需要配置好数据库信息,以更新灰度用户
-
-
业务组件引入gray-common包
<dependency> <groupId>com.sky</groupId> <artifactId>gray-common</artifactId> <version>1.0.1</version> </dependency>-
启动类上@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})
-
如果是灰度实例,需要在yml中配置:
instance: metadata-map: { gray: true }
eureka: client: service-url: defaultZone: http://127.0.0.1:8081/eureka/ instance: metadata-map: { gray: true }
-
验证
order-service(8086):正式服务
order-service(8087):灰度服务
user-service(8088):正式服务
user-service(8089):灰度服务
网关服务:gateway(8030)
当前用户:10001 配置为灰度用户
调用关系:前端 -> gateway -> order-service -> user-service
在灰度服务中,打印日志
log.info("【灰度服务】订单服务查询订单:{}", orderId);
log.info("【灰度服务】查询User,by id:{}", id);
-
验证灰度发布及续传结果:
访问 GET: localhost:8030/api/v1/order/query_order/202502190001
-
验证gateway网关查询灰度用户:
GET: localhost:8030/api/gray/query_users
完整代码: gitee.com/dmgyz/gray-…