微服務: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做負載均衡。

 

 

 

 

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