Что такое публикация в оттенках серого?
Публикация в оттенках серого (также называемая канареечной публикацией) относится к методу публикации с плавным переходом между черным и белым. На нем можно провести A/B-тестирование, то есть пусть некоторые пользователи продолжают использовать фичу продукта А, а некоторые пользователи начинают использовать фичу продукта Б. Если у пользователей нет возражений против Б, то постепенно расширять область применения и мигрировать все пользователей к Б. Приходите. Выпуск оттенков серого может обеспечить стабильность всей системы, а проблемы могут быть обнаружены и устранены на начальных градациях серого, чтобы гарантировать их влияние.
В этой статье используется springcloud gateway + nacos, чтобы продемонстрировать, как реализовать публикацию в градациях серого.Если вы не знакомы с springcloud gateway и nacos, вы можете сначала прочитать следующие статьи, а затем прочитать эту статью.
Официальное введение шлюза springcloud
Общая идея реализации:
- Написать взвешенную маршрутизацию в оттенках серого
- Написать пользовательские фильтры
- Конфигурация службы nacos требует информации метаданных и веса службы, опубликованной в оттенках серого.
- Маршрутизация в градациях серого извлекает метаданные и весовые коэффициенты из службы nacos, а затем возвращает экземпляры службы, соответствующие требованиям, к пользовательскому фильтру в соответствии с алгоритмом взвешивания.
- Конфигурация файла конфигурации шлюза требует служб, требующих маршрутизации в оттенках серого (поскольку в этом коде нет шлюза для реализации динамической маршрутизации, в противном случае маршрутизацию в оттенках серого можно настроить в центре конфигурации и извлечь из центра конфигурации)
- Фильтр прозрачно передает экземпляр службы другим фильтрам, таким как NettyRoutingFilter, через режим цепочки ответственности.
Введите реальный бой ниже
текст
1. Используемая версия разработки
<jdk.version>1.8</jdk.version>
<!-- spring cloud -->
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
2. Введение pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
ps: банку с накосом следует остерегаться, чтобы исключить зависимость от ленты, иначе loadbalancer не вступит в силу
3. Напишите маршрутизацию веса
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(GrayLoadBalancer.class);
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private String serviceId;
public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = (HttpHeaders) request.getContext();
if (this.serviceInstanceListSupplierProvider != null) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return ((Flux)supplier.get()).next().map(list->getInstanceResponse((List<ServiceInstance>)list,headers));
}
return null;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) {
if (instances.isEmpty()) {
return getServiceInstanceEmptyResponse();
} else {
return getServiceInstanceResponseWithWeight(instances);
}
}
/**
* 根据版本进行分发
* @param instances
* @param headers
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
String versionNo = headers.getFirst("version");
System.out.println(versionNo);
Map<String,String> versionMap = new HashMap<>();
versionMap.put("version",versionNo);
final Set<Map.Entry<String,String>> attributes =
Collections.unmodifiableSet(versionMap.entrySet());
ServiceInstance serviceInstance = null;
for (ServiceInstance instance : instances) {
Map<String,String> metadata = instance.getMetadata();
if(metadata.entrySet().containsAll(attributes)){
serviceInstance = instance;
break;
}
}
if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
return new DefaultResponse(serviceInstance);
}
/**
*
* 根据在nacos中配置的权重值,进行分发
* @param instances
*
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) {
Map<ServiceInstance,Integer> weightMap = new HashMap<>();
for (ServiceInstance instance : instances) {
Map<String,String> metadata = instance.getMetadata();
System.out.println(metadata.get("version")+"-->weight:"+metadata.get("weight"));
if(metadata.containsKey("weight")){
weightMap.put(instance,Integer.valueOf(metadata.get("weight")));
}
}
WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap);
if(ObjectUtils.isEmpty(weightMeta)){
return getServiceInstanceEmptyResponse();
}
ServiceInstance serviceInstance = weightMeta.random();
if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
System.out.println(serviceInstance.getMetadata().get("version"));
return new DefaultResponse(serviceInstance);
}
private Response<ServiceInstance> getServiceInstanceEmptyResponse() {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
4, пользовательский фильтр
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(ReactiveLoadBalancerClientFilter.class);
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClientFactory clientFactory;
private LoadBalancerProperties properties;
public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
this.clientFactory = clientFactory;
this.properties = properties;
}
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) {
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
}
return this.choose(exchange).doOnNext((response) -> {
if (!response.hasServer()) {
throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance)response.getServer(), overrideScheme);
URI requestUrl = this.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));
} else {
return chain.filter(exchange);
}
}
protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
if (loadBalancer == null) {
throw new NotFoundException("No loadbalancer available for " + uri.getHost());
} else {
return loadBalancer.choose(this.createRequest(exchange));
}
}
private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
Request<HttpHeaders> request = new DefaultRequest<>(headers);
return request;
}
}
5. Настройте пользовательские фильтры для управления весной
@Configuration
public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration {
public GrayGatewayReactiveLoadBalancerClientAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class})
public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties);
}
}
6. Напишите конфигурацию шлюза application.yml
server:
port: 9082
# 配置输出日志
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG
#开启端点
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: gateway-reactor-gray
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: hello-consumer
uri: grayLb://hello-consumer
predicates:
- Path=/hello/**
Конфигурация grayLb в uri указывает, что сервис должен быть опубликован в оттенках серого.
7. Настройте версию службы и значение веса, выпущенное Grayscale, в центре регистрации nacos.
вес представляет вес, версия представляет версию
Суммировать
Выше описан процесс реализации публикации в градациях серого.Существует много способов реализовать публикацию в градациях серого.Статья дает только идею. Хотя springcloud официально рекомендует использовать балансировщик нагрузки вместо ленты. Поскольку лента заблокирована, но из официального алгоритма балансировки нагрузки балансировщик нагрузки, текущий балансировщик нагрузки по умолчанию поддерживает только алгоритм опроса, а другие алгоритмы должны быть расширены и реализованы сами по себе.Лента поддерживает 7 алгоритмов по умолчанию, а алгоритм по умолчанию в основном это может удовлетворить наши потребности. Во-вторых, лента поддерживает такие конфигурации, как отложенная обработка загрузки, тайм-аут и интеграция повторных попыток с автоматическим выключателем hystrix, а балансировщик нагрузки в настоящее время поддерживает повторные попытки. Поэтому, если вы хотите реализовать публикацию в оттенках серого в формальной среде, вы можете рассмотреть возможность расширения ленты. Реализация этой статьи только как расширение и дополнение, ведь Springcloud рекомендует loadbalancer, и просто пишет демо для его реализации.
Наконец, реализация версии в оттенках серого, в отрасли также есть реализация с открытым исходным кодом - Discover, заинтересованные друзья могут просмотреть ее по следующей ссылке.
демонстрационная ссылка
Эта статья публикуется блогерами и другими операционными платформами.OpenWriteвыпуск