背景:
当我们使用微服务时,若想在本地联调就需要启动多个服务,为了避免本地启动过多服务,现将注册中心等基础服务共用。当我们在服务A开发时,都是注册到同一个nacos,这样本地和开发环境的服务A就会同时存在,当调用服务时就会使用负载均衡选择服务,导致我们无法正常调试接口。这时我们可以选择使用灰度版本来进行服务的选择。
具体实现步骤如下:
1、我们在本地配置文件中添加版本头
这样我们服务注册到nacos中点击 服务列表 会发现服务中都会带VERSION
spring: cloud: nacos: discovery: metadata: VERSION: zhangsan
2、添加灰度服务接口
public interface GrayLoadBalancer { /** * 根据serviceId 筛选可用服务 * @param serviceId 服务ID * @param request 当前请求 * @return ServiceInstance */ ServiceInstance choose(String serviceId, ServerHttpRequest request); }
3、灰度过滤器
import lombok.extern.slf4j.Slf4j; import org.apache.http.util.Asserts; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.DefaultResponse; import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools; import org.springframework.cloud.client.loadbalancer.Response; import org.springframework.cloud.gateway.config.LoadBalancerProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; import org.springframework.cloud.gateway.support.DelegatingServiceInstance; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.net.URI; @Slf4j @Component public class GrayReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter { private final static String SCHEME = "lb"; private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150; private final GrayLoadBalancer grayLoadBalancer; private final LoadBalancerProperties loadBalancerProperties; public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties loadBalancerProperties, GrayLoadBalancer grayLoadBalancer) { super(clientFactory, loadBalancerProperties); this.loadBalancerProperties = loadBalancerProperties; this.grayLoadBalancer = grayLoadBalancer; } @Override public int getOrder() { return LOAD_BALANCER_CLIENT_FILTER_ORDER; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); String schemePrefix = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR); // 直接放行 if (url == null || (!SCHEME.equals(url.getScheme()) && !SCHEME.equals(schemePrefix))) { return chain.filter(exchange); } // 保留原始url ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url); if (log.isTraceEnabled()) { log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url); } return choose(exchange).doOnNext(response -> { if (!response.hasServer()) { throw NotFoundException.create(loadBalancerProperties.isUse404(), "Unable to find instance for " + url.getHost()); } URI uri = exchange.getRequest().getURI(); // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default, // if the loadbalancer doesn't provide one. String overrideScheme = null; if (schemePrefix != null) { overrideScheme = url.getScheme(); } DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(response.getServer(), overrideScheme); URI requestUrl = LoadBalancerUriTools.reconstructURI(serviceInstance, uri); if (log.isTraceEnabled()) { log.trace("LoadBalancerClientFilter url chosen: " + requestUrl); } exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl); }).then(chain.filter(exchange)); } /** * 获取实例 * @param exchange ServerWebExchange * @return ServiceInstance */ private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) { URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); Asserts.notNull(uri, "uri"); ServiceInstance serviceInstance = grayLoadBalancer.choose(uri.getHost(), exchange.getRequest()); return Mono.just(new DefaultResponse(serviceInstance)); } }
4、基于客户端版本号灰度路由
当我们调用服务带版本号时会优先匹配带版本号的服务,若找不到则会随机选择一个服务
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import java.util.List; import java.util.stream.Collectors; @Slf4j @RequiredArgsConstructor @Component public class VersionGrayLoadBalancer implements GrayLoadBalancer { private final DiscoveryClient discoveryClient; /** * 根据serviceId 筛选可用服务 * @param serviceId 服务ID * @param request 当前请求 * @return ServiceInstance */ @Override public ServiceInstance choose(String serviceId, ServerHttpRequest request) { List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); // 注册中心无实例 抛出异常 if (CollUtil.isEmpty(instances)) { log.warn("No instance available for {}", serviceId); throw new NotFoundException("No instance available for " + serviceId); } // 获取请求version,无则随机返回可用实例 String reqVersion = request.getHeaders().getFirst(CommonConstant.VERSION); if (StrUtil.isBlank(reqVersion)) { return instances.get(RandomUtil.randomInt(instances.size())); } // 遍历可以实例元数据,若匹配则返回此实例 List<ServiceInstance> availableList = instances.stream() .filter(instance -> reqVersion .equalsIgnoreCase(MapUtil.getStr(instance.getMetadata(), CommonConstant.VERSION))) .collect(Collectors.toList()); if (CollUtil.isEmpty(availableList)) { return instances.get(RandomUtil.randomInt(instances.size())); } return availableList.get(RandomUtil.randomInt(availableList.size())); } }