微服务:SpringCloud GateWay网关

Spring Cloud Gateway Spring 官方基于 Spring 5.0Spring Boot 2.0 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。它是基于Nttey的响应式开发模式。

架构图:

一、搭建环境

   <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
server:
  port: 8080 #端口

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        -id: service-order
        uri: http://127.0.0.1:9001
        predicates:
          -path=/order/**
package com.springcloud.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}

注意这个url和之前zuul的url不同,留给小伙伴自己去对比哦!

动态路由:

引入eureka依赖和配置,懒得写了。

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id: service-order
#          uri: http://127.0.0.1:9001
          uri: lb://service-order
          predicates:
            - Path=/order/**

一个骚操作:

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
          - Path=/service-order/**
          filters:
          - RewritePath=/service-order/(?<segment>.*), /$\{segment}

我先给大家解释一下,这个是什么含义:- Path=/service-order/** 说明想要访问我们的路由,url必须是:localhost:8080/service-order/开头的路径,同时呢下面有一个过滤器(正则表达式),比如:http://localhost:8080/service-order/order/buy/1你访问的url就是这个,那么在过滤器将这个访问编程http://localhost:8080/order/buy/1,然后调用 http://service-order/order/buy/1。

有人这个时候可能就要喷我了,你这不闲的蛋疼吗?直接用上面最开始写的,就是这么搞的,难道不香吗?

首先大家要明确一个事,就是考虑安全的问题,如果按照刚开始这样做,不用正则表达式这种方法,看看会出现什么问题,如果你前端调用接口的代码,让人搞到了。是http:192.168.1.33:8080/order/buy/1(瞎写一个IP端口)。那么整个后面的/order/buy/1就完全暴露了给别人,如果别人知道了你消费者的ip和端口,就可以直接调用你的接口了。这样非常不安全!!!

如果采取刚才的正则替换的方式,别人只能得到/service-order/order/buy/1,那么他即使知道了你消费者的ip和端口,也不知道具体调用的路径,因为他调用这个/service-order/order/buy/1是不正确的。争取的是:/order/buy/1这个路径。这回清楚了吧,这不是蛋疼的操作,是安全的操作。

开启自动配置

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
        # 开启自动配置的根据服务名称进行路由转发
        discovery:
          locator:
            enabled: true   # 开启服务名称自动转发
            lower-case-service-id: true   # 服务名称以小写形式呈现

如果输入localhost:8080/service-order/order/buy/1,那么就会根据service-order匹配对应的服务(从注册中心得到),然后访问/order/buy/1路径。

二、过滤器

Spring Cloud Gateway除了具备请求路由功能之外,也支持对请求的过滤。通过Zuul网关类似,也是通过过滤器的形式来实现的。那么接下来我们一起来研究一下Gateway中的过滤器。
 
过滤器的生命周期

Spring Cloud Gateway Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” “post”

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。 

过滤器类型:
Spring Cloud Gateway Filter 从作用范围可分为另外两种GatewayFilter GlobalFilter
  • GatewayFilter:应用到单个路由或者一个分组的路由上。
  • GlobalFilter:应用到所有的路由上。

局部过滤器

局部过滤器(GatewayFilter),是针对单个路由的过滤器。可以对访问的URL过滤,进行切面处理。在Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器。这里简单将Spring Cloud Gateway内置的所有过滤器工厂整理成了一张表格,虽然不是很详细,但能作为速览使用。如下
 
 
 
 
 
 
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory 结尾,这是Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为AddRequestHeaderGatewayFilterFactory 。对于这些过滤器的使用方式可以参考官方文档。
 

全局过滤器

全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
 

 

自定义全局过滤器

package com.springcloud.demo.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;


public class LoginFilter implements GlobalFilter, Ordered {

    /**
     * 真正执行逻辑的代码
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("进入过滤器");
        String token = exchange.getRequest().getQueryParams().getFirst("access-token");
        if(token == null){
            System.out.println("没有登陆!");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    /**
     * 过滤器链:返回值越小,越靠前执行。
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
 

三、限流算法

1、计数器

计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零。
 
就是说:如果我们限定10s中的阈值是100,那么10s内就只能有100个请求,如果3s的时候已经到了100个请求,那么剩下的7s不再接收任何请求。

2、漏桶算法

漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。画个小图:
 

在网关内部存在一个一定容量的队列,之后又rate程序从队列中取出请求,这个是有要求的,比如每秒调用两次,其余的请求都在队列中,如果突然请求暴增,那么也必须先到队列里,多余的直接丢弃。这种算法主要是保护微服务的安全。

3、令牌桶算法

令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

当网关服务启动的时候,一个令牌队列(不一定非得是队列)中,请求到来的时候,需要获得令牌才能够访问对应的服务接口,初始化的时候有100个令牌,当这个时候有一千个请求过来的时候,可以抵挡钱100个请求,其余请求全部丢弃,这个时候,生成令牌的程序会每秒生成一个令牌,所以请求只能在有令牌的时候才能访问微服务,其实这样是保证了网关服务的安全。

四、基于Filter的限流

SpringCloudGateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过Redislua脚本结合的方式进行流量控制。
 
引入依赖
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

修改配置文件

server:
  port: 8080 #端口
spring:
  application:
    name: api-gateway-server #服务名称
  redis:
    host: localhost
    pool: 6379
    database: 0
  cloud: #配置SpringCloudGateway的路由
    gateway:
      routes:
        - id: product-service
          uri: lb://service-product
          predicates:
            - Path=/product-service/**
          filters:
            - name: RequestRateLimiter
              args:
                # 使用SpEL从容器中获取对象
                key-resolver: '#{@pathKeyResolver}'
                # 令牌桶每秒填充平均速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的上限
                redis-rate-limiter.burstCapacity: 3
            - RewritePath=/product-service/(?<segment>.*), /$\{segment}
#eureka注册中心
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/
  instance:
    prefer-ip-address: true #使用ip地址注册

filters就是我们新增的配置。key-resolver的value需要我们增加一个注入的对象(需要配置)。

package com.springcloud.demo.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @ClassName KeyResolverConfiguration
 * @Description
 * @Author 
 * @Date 2020/5/29 10:54
 * @Version 1.0
 **/
@Configuration
public class KeyResolverConfiguration {

    /**
     * 基于路径的限流规则
     */
    @Bean
    public KeyResolver pathKeyResolver(){
        //自定义KeyResolver
        return new KeyResolver() {
            /**
             * @param exchange 上下文参数。
             */
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                return Mono.just(exchange.getRequest().getPath().toString());
            }
        };
    }
}

启动redis

开启客户端监控。

我们一直访问就会出现:

配置不同的限流规则:

    /**
     * 基于ip地址的限流
     */
    @Bean
    public KeyResolver ipKeyResolver(){
        return exchange -> Mono.just(
          exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
        );
    }

    /**
     * 基于参数的限流
     */
    @Bean
    public KeyResolver userKeyResolver(){
        return exchange -> Mono.just(
            exchange.getRequest().getQueryParams().getFirst("user")
        );
    }

在配置文件修改即可。

5、基于sentinel的限流

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
            <version>1.6.3</version>
        </dependency>

注释掉配置文件中的一些信息

 添加新的配置类

package com.springcloud.demo.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * sentinel配置
 */
@Configuration
public class GatewayConfiguration {

	private final List<ViewResolver> viewResolvers;

	private final ServerCodecConfigurer serverCodecConfigurer;

	public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
	                            ServerCodecConfigurer serverCodecConfigurer) {
		this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
		this.serverCodecConfigurer = serverCodecConfigurer;
	}

	/**
	 * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
	 */
	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
		return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
	}

	/**
	 * 配置限流过滤器
	 */
	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public GlobalFilter sentinelGatewayFilter() {
		return new SentinelGatewayFilter();
	}

	/**
	 * 配置初始化的限流参数
	 * 用于指定资源的限流规则
	 * 		1、资源名称(路由id)
	 * 		2、统计时间 setIntervalSec
	 * 		3、配置限流阈值 setCount
	 */
	@PostConstruct
	public void initGatewayRules() {
		Set<GatewayFlowRule> rules = new HashSet<>();
		//每秒访问一次
		rules.add(new GatewayFlowRule("service-order")
				.setCount(1)
				.setIntervalSec(1)
		);

		GatewayRuleManager.loadRules(rules);
	}

}

当一秒中访问多次 就会限流。

注意:这个不需要启动sentinel的项目jar包,当然也可以启动之后动态配置。

自定义异常提示

当触发限流后页面显示的是Blocked by Sentinel: FlowException。为了展示更加友好的限流提示,Sentinel支持自定义异常处理。
您可以在 GatewayCallbackManager 注册回调进行定制:
  • setBlockHandler :注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为BlockRequestHandler 。默认实现为 DefaultBlockRequestHandler ,当被限流时会返回类似于下面的错误信息: Blocked by Sentinel: FlowException
@PostConstruct
	public void initBlockHandlers() {
		BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
			public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
				Map map = new HashMap<>();
				map.put("code", 001);
				map.put("message", "对不起,接口限流了");
				return ServerResponse.status(HttpStatus.OK).
						contentType(MediaType.APPLICATION_JSON_UTF8).
						body(BodyInserters.fromObject(map));
			}
		};
		GatewayCallbackManager.setBlockHandler(blockRequestHandler);
	}

参数限流

rules.add(new GatewayFlowRule("order-service")
                .setCount(1)
                .setIntervalSec(1)
                .setParamItem(new GatewayParamFlowItem()
                        .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
                        .setFieldName("id")
                )
        );

 

自定义api分组

/**
	 * 自定义api限流分组
	 * 		1、定义分组
	 * 		2、对小组配置限流规则
	 */
	@PostConstruct
	private void initCustomizedApis() {
		Set<ApiDefinition> definitions = new HashSet<>();
		ApiDefinition api1 = new ApiDefinition("order_api")
				.setPredicateItems(new HashSet<ApiPredicateItem>() {{
					add(new ApiPathPredicateItem().setPattern("/service-order/order/**").
							setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
				}});
		ApiDefinition api2 = new ApiDefinition("person_api")
				.setPredicateItems(new HashSet<ApiPredicateItem>() {{
					add(new ApiPathPredicateItem().setPattern("/service-person/person"));
				}});
		definitions.add(api1);
		definitions.add(api2);
		GatewayApiDefinitionManager.loadApiDefinitions(definitions);
	}

添加到rules

 

四、高可用

我们现在是单点的网关,如果挂掉咋整?要保证高可用那么就必须使用集群这种方式,所以我们可以搞一个网关集群,那么问题来了,如果前端咋调用接口?不同的ip,所以呢要用一个nginx做负载均衡。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章