1. 程式人生 > 實用技巧 >Spring Cloud Gateway入門demo

Spring Cloud Gateway入門demo

Spring Cloud Gateway入門demo

閘道器描述

​ 在微服務的架構中,每一個服務都是在獨立的執行的,而一個完整的微服務系統,都是由這些一個個獨立執行的服務組成的。每個服務各施其職。各個微服務之間的聯絡通過REST API或者RPC完成通訊。 比如一個場景是: 使用者要檢視一個商品資訊,我們知道一個商品的頁面會有: 商品的資訊,廣告,評論,庫存等等。到這裡就會涉及到有4個服務了,如果我們沒有閘道器的話,可能就要呼叫多個服務去獲取資訊,但是可能會出現一些問題,問題如下:

  • 客戶端需要發起多次請求,增加了網路通訊的成本及客戶端處理的複雜性。(如:多個服務的話就要知道多個服務的url地址)
  • 服務的鑑權會分佈在每個微服務中處理,客戶端對於每個服務的呼叫都需要重複鑑權。
  • 在後端的微服務架構中,可能不同的服務採用的協議不同,比如有 HTTP、RPC 等。客戶端如果需要呼叫多個服務,需要對不同協議進行適配

閘道器的功能

  • 效能:API高可用,負載均衡,容錯機制。
  • 安全:許可權身份認證、脫敏,流量清洗,後端簽名(保證全鏈路可信呼叫),黑名單(非法呼叫的限制)。
  • 日誌:日誌記錄(spainid,traceid)一旦涉及分散式,全鏈路跟蹤必不可少。
  • 快取:資料快取。
  • 監控:記錄請求響應資料,api耗時分析,效能監控。
  • 限流:流量控制,錯峰流控,可以定義多種限流規則。
  • 灰度:線上灰度部署,可以減小風險。
  • 路由:動態路由規則

常見的閘道器方案:

  • OpenResty(Nginx+lua)
  • Kong,是基於openresty之上的一個封裝,提供了更簡單的配置方式。 它還提供了付費的商業外掛
  • Tyk(開源、輕量級),Tyk 是一個開源的、輕量級的、快速可伸縮的 API 閘道器,支援配額和速度限制,支援認證和資料分析,支援多使用者多組織,提供全 RESTful API。它是基於go語言開發的元件。
  • Zuul,是spring cloud生態下提供的一個閘道器服務,效能相對來說不是很高
  • Spring Cloud Gateway,是Spring團隊開發的高效能閘道器

Spring Cloud Gateway概述:

spring-cloud-Gatewayspring-cloud的一個子專案。而zuul則是netflix公司的專案,只是spring將zuul整合在spring-cloud中使用而已。因為zuul2.0連續跳票和zuul1的效能表現不是很理想,所以催生了spring團隊開發了Gateway專案。

zuul1.x和spring gateway對比:

  • Zuul1.x構建於 Servlet 2.5,相容 3.x,使用的是阻塞式的 API,不支援長連線,比如 websockets。
  • Spring Cloud Gateway構建於 Spring 5+,基於 Spring Boot 2.x 響應式的、非阻塞式的 API。同時,它支援 websockets,和 Spring 框架緊密整合,開發體驗相對來說十分不錯。

​ 注意:現在zuul2.x已經開發出來了。但是spring cloud沒有將zuul2.x整合到spring cloud當中,現在的spring cloud zuul元件還是1.x的。所以用閘道器還是優先使用spirng cloud gateway把

spring cloud gateway組成和執行過程

​ spring cloud 的路由資訊可以通過RouteDefinition 這個類去檢視。 這個類裡面包含了 id, predicates斷言, filters過濾器,uri 轉發的地址等等這幾個的成員變數,當請求到達閘道器時 ,首先會基於predicates斷言去判斷該請求滿不滿足需求,滿足的話就進行下面一系列的filter , 然後再轉發到目標uri去

spring cloud gateway的demo搭建

pom.xml

<dependencies>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-dependencies</artifactId>
           <version>Hoxton.SR4</version>
           <type>pom</type>
           <scope>import</scope>
       </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
   		</dependency>
</dependencies>

注意:Hoxton.SR4這個版本是需要新增spring-boot-starter-validation的,不然會報錯的。其他版本不清楚會不會

yml配置:

spring:
  application:
    name: gateway-demo
  cloud:
    gateway:
      enabled: true
      routes: 
      #路徑的匹配,StripPrefix代表信
      - id: path_route
        predicates:
        - Path=/baidu
        filters:
        - StripPrefix=1  
        uri: https://www.bilibili.com
        #cookie的匹配
      - id: cookie_route
        predicates:
        - Cookie=chocolate,ch.p   
        uri: https://www.bilibili.com
      #請求頭匹配
      - id: header_route
        predicates:
        - Header=X-Request-Id, \d+
        uri: https://www.bilibili.com   
      # 組合匹配
      - id: compose
        predicates:
        - Path=/compose
        - Header=name, cong
        uri: https://www.bilibili.com
        filters:
        - StripPrefix=1

​ 可以看到有許多的路由資訊,大概都是會有:id,predicates,filters,uri這幾個引數。

注意點:

例如下面這個例子:

 - id: path_route1
        predicates:
        - Header=name, cong
        uri: https://www.bilibili.com
      - id: path_route2
        predicates:
        - Path=/abc
        filters:
        - StripPrefix=1  
        uri: https://baidu.com
 

​ 至於為什麼為什麼沒跳到bilbili,是因為最終訪問的是https://www.bilibili.com/abc 所以肯定是返回出錯的.這裡也驗證了注意點的第二點了

斷言和過濾器配置方式:

​ 斷言和過濾配置方式分成兩種:一種是Shortcut Configuration,一種是Fully Expanded Arguments,以下的配置方式功能是相同的,都是如果cookie存在mycookie=mycookievalue的話,斷言判斷成功的。

      - id: Fully_Expanded
        predicates:
        - name: Cookie
          args:
            name: mycookie
            regexp: mycookievalue
        uri: https://www.bilibili.com
      - id: short_cut
        predicates:
        - Cookie=mycookie,mycookievalue
        uri: https://www.bilibili.com

至於args的資訊在哪裡找。因為是通過Cookie斷言的,所以可在CookieRoutePredicateFactory.Config類上面看到相應的引數。

斷言的解析

​ 常用的斷言有:

  • 路徑匹配,實現的類是PathRoutePredicateFactory
  • cookie匹配, 實現的類是CookieRoutePredicateFactory
  • 頭部匹配, 實現的類是HeaderRoutePredicateFactory

其他的可以去到官網上面看。

官網地址:https://docs.spring.io/spring-cloud-gateway/docs/3.0.0-SNAPSHOT/reference/html/#gateway-request-predicates-factories

自定義斷言

​ 自定義一個自己的斷言,其實這個斷言跟Header頭部匹配差不多一樣的。功能就是判斷頭部有沒有key為Authorization,value為token

仿照HeaderRoutePredicateFactory,寫了一個自己的自定義類

程式碼實現:

 * project name : cloud-demo
 * Date:2020/10/3
 * Author: yc.guo
 * DESC:  需要頭部帶上authentication才能讓其通過
 */
@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {

    public AuthRoutePredicateFactory() {
        super(AuthRoutePredicateFactory.Config.class);
    }

    public List<String> shortcutFieldOrder() {
        return Arrays.asList("name", "value");
    }
    
    @Override
    public Predicate<ServerWebExchange> apply(AuthRoutePredicateFactory.Config config) {
        return serverWebExchange -> {
            List<String> list = serverWebExchange.getRequest().getHeaders().get(config.name);
            if(list!=null && list.size() > 0 && list.contains(config.value)){
                return true;
            }
            return false;
        };
    }
    @Validated
    public static class Config {
        @NotEmpty
        private String name;

        private String value;

        public String getName() {
            return name;
        }

        public AuthRoutePredicateFactory.Config setName(String name) {
            this.name = name;
            return this;
        }

        public String getValue() {
            return value;
        }

        public AuthRoutePredicateFactory.Config setValue(String value) {
            this.value = value;
            return this;
        }

        public Config() {
        }
        
    }
}

yml:

      - id: define
        predicates:
        - Auth=Authorization,token
        uri: https://www.bilibili.com

注意點:

  • 要加上@Component,注入到容器中
  • 至於Auth=Authorization,token中的Auth表示式是怎樣得到的,程式會把實現類AuthRoutePredicateFactory後面RoutePredicateFactory的這部分擷取掉,最後就只剩下Auth,所以就是這樣子得到的Auth表示式的
  • 如果想使用shortcut配置方式的話,要重寫shortcutFieldOrder這個方法。比如上面的配置- Auth=Authorization,token , 因為我重寫的實現是這樣的Arrays.asList("name", "value"); 最後按照順序來解析就成了: name=Authorization value=token

過濾器

​ Filter分為全域性過濾器和路由過濾器。路由過濾器只是針對單個路由的,全域性過濾器是針對於所有的路由的,優先順序應該是路由過濾器先執行,再到全域性過濾器執行

路由過濾器

RouteFilter路由過濾器基本有:

  • StripPrefix - 實現類:StripPrefixGatewayFilterFactory 。

    列子:路徑是http://127.0.0.1/a/b/c/d ,StripPrefix=2的話 ,得到的結果http://127.0.0.1/c/d

  • 限流過濾器RequestRateLimiter-實現類:RequestRateLimiterGatewayFilterFactory,注意:這個類不能使用shortCut方式因為他沒有實現shortcutFieldOrder

限流過濾器

​ 限流過濾器通過redis還有令牌桶演算法去實現的。 令牌桶演算法簡單的可以把他認為是: 一個很有特色的奶茶店,但是他是每天只能供應50杯奶茶。 那每天只賣50杯了,想喝的話明天請早。令牌桶的意思就是比如,每秒鐘可以補充10個令牌桶(),總令牌桶容量可以達到30個。

第一秒內拿走了25個 30-25=5; 第一秒之後補充10個:15個

第二秒沒人拿走: 15 第二秒後補充10個: 15+10=25

第三秒沒人拿走: 25 第三秒後補充10個: 這裡因為容量是30,所以就是30

第四秒需要拿走35: 0,還有5個吃閉門羹了 第四秒後補充10個: 10個

pom.xml檔案:

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

yml配置:

          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                deny-empty-key: true
                keyResolver: '#{@ipAddressKeyResolver}'     #通過ip作為key值
                redis-rate-limiter.replenishRate: 1         #每秒補充的令牌數
                redis-rate-limiter.burstCapacity: 2			#令牌容量大小                
                
#redis的配置
spinrg:
  redis:
    host: 127.0.0.1
    port: 6379
  • ​ KeyResolver預設的實現是PrincipalNameKeyResolver,它從ServerWebExchange中檢索Principal,並呼叫Principal.getName()方法。如果你直接限流請求這個路徑的所有請求,keyResolver感覺用預設就行了。當然也有一種比較常見的keyResolver情況,比如: ?userId=admin 這種帶引數的,這裡我們也可以,拿取userId去做為key,這樣就可以做到使用者的限流。

​ 注意點: redis-rate-limiter.replenishRate:2 , redis-rate-limiter.burstCapacity: 1 這樣子雖然每秒補充2個,但是容量只有1個的話,也是隻允許一個請求,所以這裡還是一秒鐘只能訪問一個請求。

ipAddressKeyResolver:

@Component
public class ipAddressKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

現象如下:

可以看到如果不允許訪問的時候會返回一個429的狀態碼。HTTP 429 - Too Many Requests

自定義一個自己的路由過濾器

實現的功能是路由的時候列印一下日誌:

/**
 * project name : cloud-demo
 * Date:2020/10/4
 * Author: yc.guo
 * DESC:
 */
@Component
public class LogsGatewayFilterFactory extends AbstractGatewayFilterFactory<LogsGatewayFilterFactory.Config> {


    Logger logger= LoggerFactory.getLogger(LogsGatewayFilterFactory.class);

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("name");
    }

    public LogsGatewayFilterFactory() {
        super(LogsGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange,  chain) ->{
            logger.info("pre: 執行前的日誌!" + config.name);
            chain.filter(exchange);  //繼續走下去的方法
            logger.info("post: 執行後的日誌" + config.name);
            return Mono.empty();
        };
    }


    public static class Config {
        String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

yml

  - id: logs_filter
        predicates:
        - Path=/logs/orders
        filters:
        - StripPrefix=1  
        uri: http://127.0.0.1:8071

這個注意的點和上面的自定義斷言的一樣的。要按照上面自定義斷言的注意點去做。

全域性過濾器

全域性過濾器常用到的有:

  • 負載均衡的全域性過濾器

負載均衡全域性過濾器

​ spirng cloud整合的負載均衡器有ribbon,所以gateway應該是可以無縫對接ribbon的。ribbon可以從yml配置檔案上得到負載均衡的地址,也可以讀取eureka上面得到負載均衡的地址,

spirng cloud gateway和ribbon整合

pom.xml

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

application.yml:

      #結合ribbon的負載均衡
      - id: lb_fiter
        predicates:
        - Path=/lb/**
        filters:
        - StripPrefix=1
        uri: lb://service1
service1:
  ribbon:
    listOfServers: 127.0.0.1:8071        

提醒:uri是lb://serviceId ,負債均衡需要lb://開頭才能識別得鳥

官網上建議使用ReactiveLoadBalancerClientFilter,只需要這樣spring.cloud.loadbalancer.ribbon.enabledtofalse`就行了

注意點:

​ 如果通過檔案配置的方式實現負載均衡,這個不能註冊上eureka。一註冊上了服務地址就會讀取eureka上面了。不會讀取本地配置檔案了。之前我實現ribbon成功了,然後去實驗去讀取eureka的時候,然後回過頭去測試ribbon就不行了

spirng cloud gateway通過讀取eureka上的地址列表實現負載均衡

pom.xml

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
@SpringBootApplication
@EnableEurekaClient //開啟註冊到eureka上
public class GatewayApp {
    public static void main(String[] args) {
        
        SpringApplication.run(GatewayApp.class,args);
    }
}
spring:
  application:
    name: gateway-demo
  cloud:
    gateway:
      enabled: true
      routes: 
      #結合eureka的負載均衡
      - id: discovery_filter
        predicates:
        - Path=/eureka/lb/**
        filters: 
        - StripPrefix=2
        uri: lb://eureka-client
        #設定發現讀取遠端地址為true
      discovery:
        locator:
          enabled: true          
          

這裡也是需要lb://開頭的

預設情況下,當一個服務例項在LoadBalancer中沒有找到時,將返回503。你可以通過配spring.cloud.gateway.loadbalancer.use404=true來讓它返回404。

動態路由

​ 如果使用正常的配置方式,我們都是通過配置application.yml,來配置路由的,但是這樣不太好的就是如果我們需要改變路由資訊的話,就得需要重新修改application.yml並且重新啟動專案才能讓路由生效(我現在的公司用的zuul,也是這樣,這樣做的話缺點很明顯的,因為你修改一次路由就得重啟一次專案)。

​ 按照正常的設定我的想法是:spring cloud通過讀取配置檔案的路由資訊,建立了一些路由物件放到記憶體中,然後當請求閘道器時,再通過這些路由資訊進行一個處理。當專案啟動的時候,那我們也可以直接修改他記憶體裡的路由資訊,不就可以完成動態路由了嗎?

​ spring cloud真的提供了一個接口出來給我們做路由資訊的管理,介面名字就是:RouteDefinitionRepository,而預設的實現類就只有一個:InMemoryRouteDefinitionRepository(利用記憶體管理路由資訊)。下面這個圖是RouteDefinitionRepository的結構圖:

​ 動態路由實際上執行的流程:

  • 首先讀取配置檔案的路由資訊和RouteDefinitionRepository的路由資訊,載入到記憶體中
  • 然後定時去讀取RouteDefinitionRepository的路由資訊(預設30s),做一個合併。提示:RouteDefinitionRepository裡面不會存有配置檔案配置的路由資訊

使用redis來儲存路由資訊的類:(通過仿照InMemoryRouteDefinitionRepository來實現的)

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private final static String GATEWAY_ROUTE_KEY="gateway_dynamic_route";

    Logger logger= LoggerFactory.getLogger(RedisRouteDefinitionRepository.class);



    private ObjectMapper objectMapper = new ObjectMapper();
    
    
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> list = new ArrayList();
        redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route ->{
            try {
                list.add(objectMapper.readValue((String) route,RouteDefinition.class));
            }catch (Exception e){
                logger.error(e.getMessage(),e);
                throw new RuntimeException("解析失敗");
            }
        });
        return Flux.fromIterable(list);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
                try {
                    System.out.println(objectMapper.writeValueAsString(route));
                    redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,
                            routeDefinition.getId(),
                            objectMapper.writeValueAsString(routeDefinition));
                }catch (Exception e){
                    logger.error(e.getMessage(),e);
                    throw new RuntimeException("儲存失敗");
                }
                return Mono.empty();
            }
        );
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap( id -> {
            redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
            return Mono.empty();
        });
    }
}

​ 儲存redis的資料結構,使用hash來儲存。value我是直接使用json格式來儲存路由資訊,路由資訊的json格式可以參考一下:actuator下面新增路由資訊的body裡面的結構。

儲存redis的資料結構:

使用nacos來儲存路由資訊的類:(等我學習完nacos時補上)


提示:如果我們沒有引入actuator的話,我們可以直接操作儲存的媒介來達到目的,比如redis,nacos。

按照官網的說法,可以通過Restful Api來管理動態路由的資訊,不過需要引入spring-boot-starter-actuator。
xml:

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
   </dependency>

配置檔案:

management.endpoint.gateway.enabled=true # default value
management.endpoints.web.exposure.include=gateway

檢視所有的路由資訊:

http://127.0.0.1:8080/actuator/gateway/routes

新增路由資訊

post請求,url: http://127.0.0.1:8080/actuator/gateway/routes/first_route

body

{
  "id": "first_route",
  "predicates": [{
    "name": "Path",
    "args": {"_genkey_0":"/first"}
  }],
  "filters": [{
      "name" : "StripPrefix",
      "args":{"parts":"1"}
  }],
  "uri": "https://www.bilibili.com",
  "order": 0
}

刪除一個路由信息(delete請求)

http://127.0.0.1:8080/actuator/gateway/routes/first_route

重新整理載入RouteDefinitionRepository的路由資訊到快取中(post請求):

http://127.0.0.1:8080/actuator/gateway/refresh

一些小發現:檢視路由資訊:

​ 這兩個路由資訊我是沒有沒有配的,但他卻出現在路由資訊上面,按照我的想法,應該是如果配置了讀取eureka上的地址列表實現負載均衡的話,閘道器就會讀取eureka上面的apps資訊,並且把apps資訊轉換成相應的路由資訊,儲存到記憶體上去。