gateway简介和灰度发布实现方案

gateway介绍

官方文档:https://docs.spring.io/spring-cloud-gateway/docs/2.2.8.RELEASE/reference/html/#gateway-starter

网关请求处理过程

springcloud 灰度发布 spring gateway 灰度发布_nacos


springcloud 灰度发布 spring gateway 灰度发布_springcloud 灰度发布_02

客户端向Spring Cloud Gateway发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关Web处理程序。此处理程序运行通过特定于请求的过滤器链发送请求。滤波器被虚线划分的原因是滤波器可以在发送代理请求之前或之后执行逻辑。执行所有“pre”过滤器逻辑,然后进行代理请求。在发出代理请求之后,执行“post”过滤器逻辑。

在没有端口的路由中定义的URI将分别为HTTP和HTTPS URI获取默认端口设置为80和443。

  • DispatcherHandler:所有请求的调度器,负载请求分发
  • RoutePredicateHandlerMapping:路由谓语匹配器,用于路由的查找,以及找到路由后返回对应的WebHandler,DispatcherHandler会依次遍历HandlerMapping集合进行处理
  • FilteringWebHandler : 使用Filter链表处理请求的WebHandler,RoutePredicateHandlerMapping找到路由后返回对应的FilteringWebHandler对请求进行处理,FilteringWebHandler负责组装Filter链表并调用链表处理请求。


filter的作用和生命周期

作用

当我们有很多个服务时,比如下图中的user-service、goods-service、sales-service等服务,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。

对于这重复的工作,有没有办法做的更好,答案是肯定的。在微服务的上一层加一个全局的权限控制、限流、日志输出的Api Gatewat服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。

springcloud 灰度发布 spring gateway 灰度发布_nacos_03

生命周期

Spring Cloud Gateway同zuul类似,有“pre”和“post”两种方式的filter。客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,比如上图中的user-service,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。

springcloud 灰度发布 spring gateway 灰度发布_gateway_04

与zuul不同的是,filter除了分为“pre”和“post”两种方式的filter外,在Spring Cloud Gateway中,filter从作用范围可分为另外两种,一种是针对于单个路由的gateway filter,它在配置文件中的写法同predict类似;另外一种是针对于所有路由的global gateway filer。现在从作用范围划分的维度来讲解这两种filter。

gateway filter

过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。过滤器可以限定作用在某些特定请求路径上。 Spring Cloud Gateway包含许多内置的GatewayFilter工厂。

GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。

Spring Cloud Gateway 内置很多过滤器工厂,在包``org.springframework.cloud.gateway.filter.factory包下

下面以AddRequestHeader GatewayFilter Factory作为示例。

AddRequestHeader GatewayFilter Factory

若在yml文件中,加入以下的配置:

server:
  port: 8081
spring:
  profiles:
    active: add_request_header_route

---
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: http://httpbin.org:80/get
        filters:
        - AddRequestHeader=X-Request-Foo, Bar
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  profiles: add_request_header_route

在上述的配置中,工程的启动端口为8081,配置文件为add_request_header_route,在add_request_header_route配置中,配置了roter的id为add_request_header_route,路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为X-Request-Foo,值为Bar。为了验证AddRequestHeaderGatewayFilterFactory是怎么样工作的,查看它的源码,AddRequestHeaderGatewayFilterFactory的源码如下:

public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

	@Override
	public GatewayFilter apply(NameValueConfig config) {
		return (exchange, chain) -> {
			ServerHttpRequest request = exchange.getRequest().mutate()
					.header(config.getName(), config.getValue())
					.build();

			return chain.filter(exchange.mutate().request(request).build());
		};
    }

}

由上面的代码可知,根据旧的ServerHttpRequest创建新的 ServerHttpRequest ,在新的ServerHttpRequest加了一个请求头,然后创建新的 ServerWebExchange ,提交过滤器链继续过滤。

来自客户端的请求经过filter后,会发往目标业务服务器。

灰度发布

灰度发布 Gray Release(又名金丝雀发布 Canary Release)

  • 概念

不停机旧版本,部署新版本,低比例流量(例如:5%)切换到新版本,高比例流量(例如:95%)走旧版本,通过监控观察无问题,逐步扩大范围,最终把所有流量都迁移到新版本上,下线旧版本。属无损发布

  • 优点

灵活简单,不需要用户标记驱动。安全性高,新版本如果出现问题,只会发生在低比例的流量上

  • 缺点

流量配比递增的配置修改,带来额外的操作成本。用户覆盖狭窄,低比例流量未必能发现所有问题

springcloud 灰度发布 spring gateway 灰度发布_nacos_05

以上是基于流量控制的方式进行灰度发布。

对于我们目前应用,并没有服务集群,没有流量百分比区分的要求。也没有服务监控,无法判断这小部分流量是否正常。

于是可以考虑基于版本控制的灰度发布。它和基于流量控制的方式类似,区别在于前端服务不属于路由范围;更新的功能是否正常由测试人员检测。由此带来一个增加操作复杂性的问题:用户需要经过前端进行标记,但这个问题目前能接受。

基于版本的灰度发布

我们在发布更新时,线上环境维护2个版本:beta和prod版本。

如线上正在运行的旧版本patient v1 和需要更新的新版本patient v2,其中patient v1是prod,patient v2是beta,更新完成后patient v2成为prod,patient v1的prod下线。

基于上图,95%用户 即目前线上的patient v1 5%用户即新版本patient v2。

基于版本控制的灰度发布步骤:

假设当前线上版本v1,需要更新的版本是v2。步骤如下:

  1. 后端发布v2版本至生产环境即beta版本。前端发布beta版本连接后端beta版本;
  2. 测试人员访问beta版本,进行测试;
  3. 测试没有问题后,前端发布beta版本至线上,beta均晋升为prod版本。用户访问v2版本。
  4. 测试人员确认无误后。后端下线v1。

若更新后出现了之前未发现的BUG,视影响范围有2种解决方案。

  1. 影响核心功能的bug,且不能半小时内修复。前后端可选择将v2回滚至v1。
  2. 不影响核心功能的bug,考虑在下一个版本解决。更新步骤如上。

功能实现

上一个章节的gateway介绍中,可以看到前端请求经过一系列GatewayFilter后发送到具体的业务服务。

我们可以在请求发送到业务服务前修改目标地址为符合当前请求版本的服务ip和端口。

实现思路:

  1. 新建带版本控制的灰度路由
  2. 新建自定义filter
  3. 灰度路由从nacos拉取服务实例元数据,根据匹配算法,返回符合要求的实例给filter
  4. filter设置请求地址到exchange,并进入下一个filter,且要保证请求地址不被更改。

实现如下:

添加依赖:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

GrayLoadBalancerClientFilter

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;

/**
 * 自定义filter,通过GrayLoadBalancer 过滤实例
 *
 * @author hy
 * @date 2021/6/1
 */
public class GrayLoadBalancerClientFilter 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 GrayLoadBalancerClientFilter(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 = ((URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)).getScheme();

        //若自定义需要灰度发布的route的shame,可以使用这个
//        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 doFilter(exchange, chain, url);
//        } else {
//            return chain.filter(exchange);
//        }

        return doFilter(exchange, chain, url);
    }

    private Mono<Void> doFilter(ServerWebExchange exchange, GatewayFilterChain chain, URI 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();

                //overrideScheme为空时 转发的路由是http请求
                String overrideScheme = null;

                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));
    }

    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;
    }
}

GrayLoadBalancer

import com.wolwo.gateway.handler.gray.weight.model.WeightMeta;
import com.wolwo.gateway.handler.gray.weight.util.WeightRandomUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.*;

/**
 * 自定义灰度发布的负载均衡,提供了按照版本和权重的方式
 *
 * @author hy
 * @date 2021/6/1
 */
@Slf4j
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    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 getServiceInstanceResponseByVersion(instances, headers);
        }
    }

    /**
     * 根据版本进行分发
     *
     * @param instances
     * @param headers
     * @return
     */
    private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
        String versionNo = headers.getFirst("version");
        log.debug("request head version: {}", 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)) {

            //实例列表不为空则取第一个
            if (CollectionUtils.isEmpty(instances)) {
                return getServiceInstanceEmptyResponse();
            }

            return new DefaultResponse(instances.get(0));
        }

        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();
    }
}

GrayLoadBalancerClientAutoConfig

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置自定义filter给spring管理
 *
 * @author hy
 * @date 2021/6/1
 */
@Configuration
public class GrayLoadBalancerClientAutoConfig {
    public GrayLoadBalancerClientAutoConfig() {
    }

    @Bean
    @ConditionalOnMissingBean({GrayLoadBalancerClientFilter.class})
    public GrayLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
        return new GrayLoadBalancerClientFilter(clientFactory, properties);
    }

}

在服务的metadata中添加自己的version

springcloud 灰度发布 spring gateway 灰度发布_nacos_06

发送请求时head带上version

springcloud 灰度发布 spring gateway 灰度发布_微服务_07

springcloud 灰度发布 spring gateway 灰度发布_nacos_08

若没有version属性,gateway会返回实例列表中端口号最小的实例。

springcloud 灰度发布 spring gateway 灰度发布_微服务_09

调试源码

FilteringWebHandler类中 file 方法中打断点,如图

springcloud 灰度发布 spring gateway 灰度发布_gateway_10

可以看出先执行我们自定义的GrayLoadBalancerClientFilter,后执行gateway内置的ReactiveLoadBalancerClientFilter

ReactiveLoadBalancerClientFilter有可能会改变我的请求地址。于是在GrayLoadBalancerClientFilter类中scheme置为null,如下图

springcloud 灰度发布 spring gateway 灰度发布_微服务_11

ReactiveLoadBalancerClientFilter 源码中可以看出,sheme为空,则不会进行额外处理,直接进入下一个filer

springcloud 灰度发布 spring gateway 灰度发布_nacos_12

这样达到了我们按照版本执行请求的目的,但是缺少了高级负载均衡功能。