微服務-gateway-Filter-熔斷、限流、統一鑑權

學習思路

  1. 源碼自帶各種過濾器具體實現
  2. 自定義功能過濾器實現

一、源碼自帶各種過濾器具體實現

1、RouteToRequestUrlFilter:根據配置的路由規則匹配成功後,合併訪問地址

我們來簡單看一下源碼

/**
 * 根據配置的路由規則匹配成功後,合併訪問地址
 * @author Spencer Gibb
 */
public class RouteToRequestUrlFilter implements GlobalFilter, Ordered {

	/**
	 * Order of Route to URL.
	 * 執行順序
	 */
	public static final int ROUTE_TO_URL_FILTER_ORDER = 10000;

	private static final Log log = LogFactory.getLog(RouteToRequestUrlFilter.class);

	private static final String SCHEME_REGEX = "[a-zA-Z]([a-zA-Z]|\\d|\\+|\\.|-)*:.*";
	static final Pattern schemePattern = Pattern.compile(SCHEME_REGEX);

	/* for testing */
	static boolean hasAnotherScheme(URI uri) {
		return schemePattern.matcher(uri.getSchemeSpecificPart()).matches()
				&& uri.getHost() == null && uri.getRawPath() == null;
	}

	@Override
	public int getOrder() {
		return ROUTE_TO_URL_FILTER_ORDER;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		//獲得路由
		Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
		if (route == null) {
			return chain.filter(exchange);
		}
		log.trace("RouteToRequestUrlFilter start");
		URI uri = exchange.getRequest().getURI();
		boolean encoded = containsEncodedParts(uri);
		URI routeUri = route.getUri();

		if (hasAnotherScheme(routeUri)) {
			// this is a special url, save scheme to special attribute
			// replace routeUri with schemeSpecificPart
			exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR,
					routeUri.getScheme());
			routeUri = URI.create(routeUri.getSchemeSpecificPart());
		}

		if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
			// Load balanced URIs should always have a host. If the host is null it is
			// most
			// likely because the host name was invalid (for example included an
			// underscore)
			throw new IllegalStateException("Invalid host: " + routeUri.toString());
		}
		//合併Url
		URI mergedUrl = UriComponentsBuilder.fromUri(uri)
				// .uri(routeUri)
				.scheme(routeUri.getScheme()).host(routeUri.getHost())
				.port(routeUri.getPort()).build(encoded).toUri();
		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
		return chain.filter(exchange);
	}

}

解釋:

  1. 獲取請求地址http://localhost:8001/message-info/message?userId=100001
  2. 獲取路由規則,這裏我們配置的是lb://message-info/**
  3. 根據路由規則發現能訪問url能匹配上規則
  4. 替換url爲:lb://message-server/message-info/message?userId=100001
  5. 進入下一個Filter

2、LoadBalancerClientFilter:過濾lb開通的Url,根據serviceId選擇一個實例,從而實現負載均衡

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		//獲取RouteToRequestUrlFilter生成的Url
		URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		//獲取前綴
		String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
		//如果不是lb開頭直接放過到下一個Filter
		if (url == null
				|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
			return chain.filter(exchange);
		}
		// preserve the original url
		//保留原始的url
		addOriginalRequestUrl(exchange, url);

		if (log.isTraceEnabled()) {
			log.trace("LoadBalancerClientFilter url before: " + url);
		}
		//獲取服務實例
		final ServiceInstance instance = choose(exchange);

		if (instance == null) {
			throw NotFoundException.create(properties.isUse404(),
					"Unable to find instance for " + url.getHost());
		}
		//獲取原始請求Url
		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 = instance.isSecure() ? "https" : "http";
		if (schemePrefix != null) {
			overrideScheme = url.getScheme();
		}
		//組裝真正的url
		URI requestUrl = loadBalancer.reconstructURI(
				new DelegatingServiceInstance(instance, overrideScheme), uri);

		if (log.isTraceEnabled()) {
			log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
		}

		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
		return chain.filter(exchange);
	}

解釋:

  1. 這個Filter的Order=10100緊接RouteToRequestUrlFilter
  2. 本過濾器只處理lb開頭的url
  3. 後端通過RibbonLoadBalancerClient實現負載均衡

我們來看一下loadBalancer.reconstructUrl

就是把lb://message-info/這個url轉換成http://ip:port/message-info/message;內部經過了一些拼裝

 public URI reconstructURI(ServiceInstance instance, URI original) {
        Assert.notNull(instance, "instance can not be null");
        String serviceId = instance.getServiceId();
        RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
        URI uri;
        Server server;
        if (instance instanceof RibbonLoadBalancerClient.RibbonServer) {
            RibbonLoadBalancerClient.RibbonServer ribbonServer = (RibbonLoadBalancerClient.RibbonServer)instance;
            server = ribbonServer.getServer();
            uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, ribbonServer);
        } else {
            server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
            IClientConfig clientConfig = this.clientFactory.getClientConfig(serviceId);
            ServerIntrospector serverIntrospector = this.serverIntrospector(serviceId);
            uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, clientConfig, serverIntrospector, server);
        }

        return context.reconstructURIWithServer(server, uri);
    }

3、ForwardRoutingFilter:以forward開通的url轉發到本地DispatcherHandler

@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

		String scheme = requestUrl.getScheme();
		if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
			return chain.filter(exchange);
		}

		// TODO: translate url?

		if (log.isTraceEnabled()) {
			log.trace("Forwarding to URI: " + requestUrl);
		}

		return this.getDispatcherHandler().handle(exchange);
	}

解釋:

  1. order:Integer.MAX_VALUE;說明優先級最低,最後執行
  2. 過濾forward開頭的url;直接轉發到DispatcherHandler,進行本地handler處理

4、webSocketRoutingFilter:ws或者wss開通,可轉發webSocket服務:生產沒用過,用到可研究

5、HystrixGatewayFilterFactory:基於路由做服務熔斷

我們先來看一下本地實現git地址:https://gitee.com/carpentor/spring-cloud-example

熔斷:如果服務因外部原因一直請求失敗(超時),那麼會阻塞大量的線程,最後導致資源被大量佔用,最終導致其他服務無可用資源,爲了解決這個問題,引入熔斷,請求服務異常達到一定頻率後服務進入半開(部分請求,部分快速失敗),如果服務繼續不可用,進入快速失敗(請求進入後不請求,直接快速失敗),等待一段時間後嘗試訪問,成功進入半開,失敗進入快速失敗

spring:
  application:
    name: gateway-demo
  cloud:
    gateway:
      routes:
        - id: message-server
          uri: lb://message-server
          order: 1000
          predicates:
          - Path=/message-info/**
          filters:
            - name: Hystrix
              args:
                name: myHystrixMessage
                fallbackUri: forward:/message/fallback

需要在配置時新增filters配置

fallbackUrl快速失敗url:需要我們新增Controller:mappingUrl=/message/fallback

@RestController
@Log4j2
public class MessageHystrixFallbackController {

	@GetMapping("/message/fallback")
	public String fallback(ServerWebExchange exchange,Throwable throwable){
		if (throwable.getCause()!=null){
			log.info("==========1{}",throwable.getCause().getMessage(),throwable);
		}else {
			log.info("==========2{}",throwable.getMessage(),throwable);
		}
		ServerHttpRequest request = exchange.getRequest();
		return "123"+request.getMethodValue();
	}
}

這裏只是稍微看一下功能,咱不說細節,應爲在網關層做容錯有已經的侷限

  1. 如果按路由規則全局容錯,那麼牽扯下游一個系統,肯定不能這麼幹
  2. 如果做單接口容錯,需要擴展過濾器,並且業務的mock數據或者快速失敗需要在網關定製開發

自己的見解,可反駁

6、RequestRateLimiterGatewayFilterFactory :限流

限流:限制訪問系統的流量,防止流量過大,系統資源被佔用完畢,導致系統整體不可用

可以在ip、用戶、系統資源等維度去做一些限流

開發實例:

gateway引入限流有包裝filter,依賴於redis首先引入jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

在application.yml添加配置

spring:
  application:
    name: gateway-demo
  cloud:
    gateway:
      routes:
        - id: message-server
          uri: lb://message-server
          order: 1000
          predicates:
          - Path=/message-info/**
          filters:
            - name: RequestRateLimiter
              args:
                ## 限流的維度,比如一個用戶操作的頻率,或者一個ip操作的頻率
                key-resolver: '#{@hostKeyResolver}'
                ## 每秒填充速度1
                redis-rate-limiter.replenishRate: 1
                ## 令牌桶總容量
                redis-rate-limiter.burstCapacity: 1
  redis:
    host: localhost
    port: 6379

hostKeyResolver:自定義限流維度,對應spring的beanname

/**
	 * 根據HostAddress進行限流
	 * 實際業務中可根據業務需求自定義
	 * @return
	 */
	@Bean("hostKeyResolver")
	KeyResolver hostKeyResolver() {
		return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
	}

這裏只是簡單實現一下限流,後面會單獨講:全侷限流、令牌桶算法、漏銅算法

二、自定義功能過濾器實現

以上的的過濾器都是內部提供,通過簡單的配置就可以實現相關的容錯、限流等

接下來我們來具體時間一個過濾器

/**
	 * 過濾器
	 * order越大優先級越低
	 * 跟LoginFiter功能一樣,兩種實現方式
	 * @return
	 */
	@Bean
	@Order(3)
	public GlobalFilter login(){
		log.info(this.getClass().getSimpleName()+"===login");
		return ((exchange, chain) -> {
			//pre filter:在業務執行前執行
			log.info("pre filter");
			return chain.filter(exchange).then(Mono.fromRunnable(()->{
				//業務執行後執行
				log.info("post filter");
			}));
		});
	}

我們可以在過濾器前去實現一些白名單、鑑權等操作,通過後再繼續往後執行,這樣的好處就是可以把校驗從下游系統統一提取出來,減少了重複代碼,讓業務系統只關心業務

總結:

  1. 網關的基礎功能:Filter
  2. 基於Filter內部實現了各種協議路由轉發、容錯、限流、等基礎功能,我們只需要簡單配置即可使用
  3. 基於Filter實現附加功能,比如白名單、驗籤、安全等操作

公衆號主要記錄各種源碼、面試題、微服務技術棧,幫忙關注一波,非常感謝

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