深入Java微服務之網關系列3: SpringCloudalibaba gateway詳解(史上最全)


九、服務網關:Gateway

image-20210507115602441

9.1、網關簡介

大家都都知道在微服務架構中,一個系統會被拆分為很多個微服務。那么作為客戶端要如何去調用這么多的微服務呢?如果沒有網關的存在,我們只能在客戶端記錄每個微服務的地址,然后分別去調用。

image-20210507115048632

這樣的架構會存在許多的問題:

  1. 客戶端多次請求不同的微服務,增加客戶端代碼或配置編寫的復雜性。
  2. 認證復雜,每個服務都需要獨立認證。
  3. 存在跨域請求,在一定場景下處理相對復雜。

網關就是為了解決這些問題而生的。所謂的API網關,就是指系統的統一入口,它封裝了應用程序的內部結構,為客戶端提供統一服務,一些與業務本身功能無關的公共邏輯可以在這里實現,諸如認證、鑒權、監控、路由轉發等等。

image-20210507115517531

9.2、常用的網關

9.2.1、Ngnix+lua

使用nginx的反向代理和負載均衡可實現對api服務器的負載均衡及高可用。

lua是一種腳本語言,可以來編寫一些簡單的邏輯, nginx支持lua腳本

9.2.2、Kong

基於Nginx+Lua開發,性能高,穩定,有多個可用的插件(限流、鑒權等等)可以開箱即用。

他的缺點:

  1. 只支持Http協議。
  2. 二次開發,自由擴展困難。
  3. 提供管理API,缺乏更易用的管控、配置方式。

9.2.3、Zuul

Netflix開源的網關,功能豐富,使用JAVA開發,易於二次開發。

他的缺點:

  1. 缺乏管控,無法動態配置。
  2. 依賴組件較多。
  3. 處理Http請求依賴的是Web容器,性能不如Nginx。

9.2.4、Spring Cloud Gateway

Spring公司為了替換Zuul而開發的網關服務,SpringCloud alibaba技術棧中並沒有提供自己的網關,我們可以采用Spring Cloud Gateway來做網關

9.3、Gateway簡介

Spring Cloud Gateway是Spring公司基於Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。它的目標是替代Netflix Zuul,其不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了網關基本的功能,例如:安全,監控和限流。

他的主要功能是:

  1. 進行轉發重定向。
  2. 在開始的時候,所有類都需要做的初始化操作。
  3. 進行網絡隔離。

9.4、快速入門

需求:通過瀏覽器訪問api網關,然后通過網關將請求轉發到商品微服務。

9.4.1、基礎版

創建一個api-gateway 模塊,並且導入下面的依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    Shop-parent
    <groupId>cn.linstudy</groupId>
    <version>1.0.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  api-gateway

  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <!--gateway網關-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      spring-cloud-starter-gateway
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      lombok
    </dependency>
  </dependencies>

</project>
復制代碼

編寫配置文件

server:
  port: 9000 # 指定網關服務的端口
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes: # 路由數組[路由 就是指定當請求滿足什么條件的時候轉到哪個微服務]
        - id: product_route # 當前路由的標識, 要求唯一
          uri: http://localhost:8081 # 請求要轉發到的地址
          order: 1 # 路由的優先級,數字越小級別越高
          predicates: # 斷言(就是路由轉發要滿足的條件)
            - Path=/product-serv/** # 當請求路徑滿足Path指定的規則時,才進行路由轉發
          filters: # 過濾器,請求在傳遞過程中可以通過過濾器對其進行一定的修改
            - StripPrefix=1 # 轉發之前去掉1層路徑
復制代碼

測試

image-20210507171142491

9.4.2、升級版

我們發現升級版有一個很大的問題,那就是在配置文件中寫死了轉發路徑的地址,我們需要在注冊中心來獲取地址。

加入nacos依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    Shop-parent
    <groupId>cn.linstudy</groupId>
    <version>1.0.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  api-gateway

  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <!--gateway網關-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      spring-cloud-starter-gateway
    </dependency>
    <!--nacos客戶端-->
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      spring-cloud-starter-alibaba-nacos-discovery
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      lombok
    </dependency>
  </dependencies>

</project>
復制代碼

在主類上添加注解

@SpringBootApplication
@EnableDiscoveryClient
public class GateWayServerApp {
  public static void main(String[] args) {
    SpringApplication.run(GateWayServerApp.class,args);
  }
}
復制代碼

修改配置文件

server:
  port: 9000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway可以發現nacos中的微服務
      routes:
        - id: product_route # 路由的名字
          uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,並遵循負載均衡策略
          predicates:
            - Path=/product-serv/** # 符合這個規定的才進行1轉發
          filters:
            - StripPrefix=1 # 將第一層去掉
復制代碼

我們還可以自定義多個路由規則。

spring:
  application:
    gateway:
      routes:
        - id: product_route
          uri: lb://product-service 
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1
        - id: order_route
          uri: lb://order-service 
          predicates:
            - Path=/order-serv/**
          filters:
            - StripPrefix=1
復制代碼

9.4.3、簡寫版

我們的配置文件無需寫的1那么復雜就可以實現功能,有一個簡寫版。

server:
  port: 9000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway可以發現nacos中的微服務
復制代碼

image-20210507201625163

我們發現,就發現只要按照網關地址/微服務名稱/接口的格式去訪問,就可以得到成功響應。

9.5、Gateway核心架構

9.5.1、基本概念

路由(Route) 是 gateway 中最基本的組件之一,表示一個具體的路由信息載體。主要定義了下面的幾個信息:

  1. id:路由標識符,區別於其他 Route。
  2. uri:路由指向的目的地 uri,即客戶端請求最終被轉發到的微服務。
  3. order:用於多個 Route 之間的排序,數值越小排序越靠前,匹配優先級越高。
  4. predicate:斷言的作用是進行條件判斷,只有斷言都返回真,才會真正的執行路由。
  5. filter:過濾器用於修改請求和響應信息。
  6. predicate:斷言,用於進行條件判斷,只有斷言都返回真,才會真正的執行路由。

9.5.2、執行原理

image-20201030161652819

  1. 接收用戶的請求,請求處理器交給處理器映射器,返回執行鏈。
  2. 請求處理器去調用web處理器,在web處理器里面對我們的路徑1進行處理。假設1我們的路徑1是:http://localhost:9000/product-serv/get?id=1 ,根據配置的路由規則,上本地找對應的服務信息:product-service對應的主機ip是192.168.10.130。
  3. 根據1ribbon的負載均衡策略去選擇一個節點,然后拼接好,將路徑中的product-serv替換成192.168.10.130:8081,如果你配置了filter,那么他還會走filter。
  4. 如果你沒有自定義路由的話,默認Gateway會幫你把第一層去掉。網關端口從此一個/開始到第二個/開始算第一層。

image-20210507203258953

9.6、過濾器

Gateway的過濾器的作用是:是在請求的傳遞過程中,對請求和響應做一些手腳。

Gateway的過濾器的生命周期:

  1. PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集群中選擇 請求的微服務、記錄調試信息等。
  2. POST:這種過濾器在路由到微服務以后執行。這種過濾器可用來為響應添加標准的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。

Gateway 的Filter從作用范圍可分為兩種: GatewayFilter與GlobalFilter:

  1. GatewayFilter:應用到單個路由或者一個分組的路由上。
  2. GlobalFilter:應用到所有的路由上。

9.6.1、局部過濾器

局部過濾器是針對單個路由的過濾器。他分為內置過濾器和自定義過濾器。

9.6.1.1、內置過濾器

在SpringCloud Gateway中內置了很多不同類型的網關路由過濾器。

9.6.1.1.1、局部過濾器內容
過濾器工廠 作用 參數
AddRequestHeader 為原始請求添加Header Header的名稱及值
AddRequestParameter 為原始請求添加請求參數 參數名稱及值
AddResponseHeader 為原始響應添加Header Header的名稱及值
DedupeResponseHeader 剔除響應頭中重復的值 需要去重的Header名稱及去重策略
Hystrix 為路由引入Hystrix的斷路器保護 HystrixCommand 的名稱
FallbackHeaders 為fallbackUri的請求頭中添加具體的異常信息 Header的名稱
PrefixPath 為原始請求路徑添加前綴 前綴路徑
PreserveHostHeader 為請求添加一個preserveHostHeader=true的屬性,路由過濾器會檢查該屬性以決定是否要發送原始的Host
RequestRateLimiter 用於對請求限流,限流算法為令牌桶 keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
RedirectTo 將原始請求重定向到指定的URL http狀態碼及重定向的url
RemoveHopByHopHeadersFilter 為原始請求刪除IETF組織規定的一系列Header 默認就會啟用,可以通過配置指定僅刪除哪些Header
RemoveRequestHeader 為原始請求刪除某個Header Header名稱
RemoveResponseHeader 為原始響應刪除某個Header Header名稱
RewritePath 重寫原始的請求路徑 原始路徑正則表達式以及重寫后路徑的正則表達式
RewriteResponseHeader 重寫原始響應中的某個Header Header名稱,值的正則表達式,重寫后的值
SaveSession 在轉發請求之前,強制執行WebSession::save操作
secureHeaders 為原始響應添加一系列起安全作用的響應頭 無,支持修改這些安全響應頭的值
SetPath 修改原始的請求路徑 修改后的路徑
SetResponseHeader 修改原始響應中某個Header的值 Header名稱,修改后的值
SetStatus 修改原始響應的狀態碼 HTTP 狀態碼,可以是數字,也可以是字符串
StripPrefix 用於截斷原始請求的路徑 使用數字表示要截斷的路徑的數量
Retry 針對不同的響應進行重試 retries、statuses、methods、series
RequestSize 設置允許接收最大請求包的大小。如果請求包大小超過設置的值,則返回 413 Payload Too Large 請求包大小,單位為字節,默認值為5M
ModifyRequestBody 在轉發請求之前修改原始請求體內容 修改后的請求體內容
ModifyResponseBody 修改原始響應體的內容 修改后的響應體內容
9.6.1.1.2、局部過濾器的使用
server:
  port: 9000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway可以發現nacos中的微服務
      routes:
        - id: product_route # 路由的名字
          uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,並遵循負載均衡策略
          predicates:
            - Path=/product-serv/** # 符合這個規定的才進行1轉發
          filters:
            - StripPrefix=1 # 將第一層去掉
            - SetStatus=2000 # 這里使用內置的過濾器,修改返回狀態
復制代碼

9.6.1.2、自定義局部過濾器

很多的時候,內置過濾器沒辦法滿足我們的需求,這個時候就必須自定義局部過濾器。我們假定一個需求是:統計訂單服務調用耗時。

編寫一個類,用於實現邏輯

名稱是有固定格式xxxGatewayFilterFactory

@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {

  private static final String BEGIN_TIME = "beginTime";

  //構造函數
  public TimeGatewayFilterFactory() {
    super(TimeGatewayFilterFactory.Config.class);
  }

  //讀取配置文件中的參數 賦值到 配置類中
  @Override
  public List<String> shortcutFieldOrder() {
    return Arrays.asList("show");
  }

  @Override
  public GatewayFilter apply(Config config) {
    return new GatewayFilter() {
      @Override
      public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (!config.show){
          // 如果配置類中的show為false,表示放行
          return chain.filter(exchange);
        }
        exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());
        /**
         *  pre的邏輯
         * chain.filter().then(Mono.fromRunable(()->{
         *     post的邏輯
         * }))
         */
        return chain.filter(exchange).then(Mono.fromRunnable(()->{
          Long startTime = exchange.getAttribute(BEGIN_TIME);
          if (startTime != null) {
            System.out.println(exchange.getRequest().getURI() + "請求耗時: " + (System.currentTimeMillis() - startTime) + "ms");
          }
        }));
      }
    };
  }

  @Setter
  @Getter
  static class Config{
    private boolean show;
  }

}
復制代碼

編寫application.xml

server:
  port: 9000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 讓gateway可以發現nacos中的微服務
      routes:
        - id: product_route # 路由的名字
          uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,並遵循負載均衡策略
          predicates:
            - Path=/product-serv/** # 符合這個規定的才進行1轉發
          filters:
            - StripPrefix=1 # 將第一層去掉
        - id: order_route
          uri: lb://order-service
          predicates:
            - Path=/order-serv/**
          filters:
            - StripPrefix=1
            - Time=true
復制代碼

訪問路徑:http://localhost:9000/order-serv/getById?o=1&pid=1

在這里插入圖片描述

9.6.2、全局過濾器

全局過濾器作用於所有路由, 無需配置。通過全局過濾器可以實現對權限的統一校驗,安全性驗證等功能。SpringCloud Gateway內部也是通過一系列的內置全局過濾器對整個路由轉發進行處理。

網關全局過濾器

開發中的鑒權邏輯:

  • 當客戶端第一次請求服務時,服務端對用戶進行信息認證(登錄)。
  • 認證通過,將用戶信息進行加密形成token,返回給客戶端,作為登錄憑證。
  • 以后每次請求,客戶端都攜帶認證的token。
  • 服務端對token進行解密,判斷是否有效。

image-20210507220009473

我們來模擬一個需求:實現統一鑒權的功能,我們需要在網關判斷請求中是否包含token且,如果沒有則不轉發路由,有則執行正常邏輯。

編寫全局過濾器

@Component
public class AuthGlobalFilter implements GlobalFilter {

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    String token = exchange.getRequest().getQueryParams().getFirst("token");
    if (StringUtils.isBlank(token)) {
      System.out.println("鑒權失敗");
      exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
      return exchange.getResponse().setComplete();
    }
    return chain.filter(exchange);
  }
}
復制代碼

9.6.3、網關限流

網關是所有請求的公共入口,所以可以在網關進行限流,而且限流的方式也很多,我們本次采用前面學過的Sentinel組件來實現網關的限流。Sentinel支持對SpringCloud Gateway、Zuul等主流網關進行限流。

image-20210507220048921

從1.6.0版本開始,Sentinel提供了SpringCloud Gateway的適配模塊,可以提供兩種資源維度的限流:

  • route維度:即在Spring配置文件中配置的路由條目,資源名為對應的routeId
  • 自定義API維度:用戶可以利用Sentinel提供的API來自定義一些API分組

9.6.3.1、網關集成Sentinel

添加依賴

<dependency>
	<groupId>com.alibaba.csp</groupId>
	sentinel-spring-cloud-gateway-adapter
</dependency>
復制代碼

編寫配置類進行限流

配置類的本質是用代碼替代nacos圖形化界面限流。

@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;
    }
    // 配置限流的異常處理器
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }
    // 初始化一個限流的過濾器
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }
    //增加對商品微服務的限流
     @PostConstruct
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("product_route")
                .setCount(3) // 三次
                .setIntervalSec(1) // 一秒,表示一秒鍾1超過了三次就會限流
        );
        GatewayRuleManager.loadRules(rules);
    }
}
復制代碼

修改限流默認返回格式

如果我們不想在限流的時候返回默認的錯誤,那么就需要自定義錯誤,指定自定義的返回格式。我們只需在類中添加一段配置即可。

@PostConstruct
public void initBlockHandlers() {
	BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
	public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
		Map map = new HashMap<>();
		map.put("code", 0);
		map.put("message", "接口被限流了");
			return ServerResponse.status(HttpStatus.OK).
				contentType(MediaType.APPLICATION_JSON).
				body(BodyInserters.fromValue(map));
            }
};
	GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
復制代碼

測試

image-20210507221350190

9.6.3.2、自定義API分組

我們可以發現,上面的這種定義,對整個服務進行了限流,粒度不夠細。自定義API分組是一種更細粒度的限流規則定義,它可以實現某個方法的細粒度限流。

在Shop-order-server項目中添加ApiController

@RestController
@RequestMapping("/api")
public class ApiController {
    @RequestMapping("/hello")
    public String api1(){
        return "api";
    }
}
復制代碼

在GatewayConfiguration中添加配置

@PostConstruct
private void initCustomizedApis() {
	Set<ApiDefinition> definitions = new HashSet<>();
	ApiDefinition api1 = new ApiDefinition("order_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/order-serv/api/**").                 setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});
	definitions.add(api1);
	GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
@PostConstruct
private void initGatewayRules() {
	Set<GatewayFlowRule> rules = new HashSet<>();
	rules.add(new GatewayFlowRule("product_route")
                .setCount(3)
                .setIntervalSec(1)
	);
	rules.add(new GatewayFlowRule("order_api").
                setCount(1).
                setIntervalSec(1));
    GatewayRuleManager.loadRules(rules);
}
復制代碼

測試

直接訪問http://localhost:8082/api/hello 是不會發生限流的,訪問http://localhost:9000/order-serv/api/hello 就會出現限流了。

作者:XiaoLin_Java
鏈接:https://juejin.cn/post/7001816849826447397
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

微信公眾號【程序員黃小斜】作者是前螞蟻金服Java工程師,專注分享Java技術干貨和求職成長心得,不限於BAT面試,算法、計算機基礎、數據庫、分布式、spring全家桶、微服務、高並發、JVM、Docker容器,ELK、大數據等。關注后回復【book】領取精選20本Java面試必備精品電子書。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM