背景:
當我們使用微服務時,若想在本地聯調就需要啟動多個服務,為了避免本地啟動過多服務,現將註冊中心等基礎服務共用。當我們在服務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()));
}
}