spring cloud項目07:網關(Gateway)(2)


JAVA 8

spring boot 2.5.2

spring cloud 2020.0.3

---

 

授人以漁:

1、Spring Cloud PDF版本

最新版本,下載下來,以便查閱。

更多版本的官方文檔:

https://docs.spring.io/spring-cloud/docs/

2、Spring Cloud Gateway

沒有PDF版本,把網頁保存下來。

 

前文:spring cloud項目06:網關(Gateway)(1)

 

本文使用的項目:

主要路徑:前端請求經過 external.gateway 轉發到 adapter.web。在此過程中,會做一些試驗。

external.gateway  網關服務 端口 25001
adapter.web web適配層應用 端口 21001
data.user user數據層應用 端口 20001
eureka.server Eureka注冊中心 端口 10001

 

目錄

1、網關服務化

2、限流

使用RequestRateLimiterGatewayFilterFactory限流

3、編程配置路由

參考文檔

 

1、網關服務化

前文 中,路由配置中的uri使用的是 http://localhost:21001,硬編碼,而且uri無法配置多個(試驗失敗),實現不了負載均衡(LB,Load Balance),需要改進。

在S.C.微服務系統中,所有服務都可以注冊,那么,網關服務是否可以注冊呢?當然可以!

網關服務注冊之后,即可使用注冊中心的服務信息來訪問 已注冊的服務。

 

添加依賴包:

<!-- 注冊到注冊中心 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

添加配置(同其它微服務):

# Eureka客戶端配置
eureka:
  instance.prefer-ip-address: true
  lease-renewal-interval-in-seconds: 15
  lease-expiration-duration-in-seconds: 30
  client:
    service-url:
      defaultZone: http://localhost:10001/eureka/
    registry-fetch-interval-seconds: 20

# 提升日志級別,避免輸出太多注冊相關的正常日志
logging:
  level:
    com.netflix.discovery.DiscoveryClient: warn

spring:
  application:
    # 服務名
    name: external.gateway

檢查注冊中心:網關服務已注冊成功

 

 

檢查 /actuator/gateway/globalfilters端點:來自博客園

和前文對比,多了一個 ReactiveLoadBalancerClientFilter,看起來是做 LB 的。

返回信息
{
    "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@68b9834c": -2147482648,
    "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@7da39774": 10000,
    "org.springframework.cloud.gateway.filter.NettyRoutingFilter@7d7cac8": 2147483647,
    "org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@306f6f1d": 10150,
    "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@441b8382": 2147483647,
    "org.lib.external.gateway.filters.TokenGlobalFilter@6f76c2cc": 0,
    "org.springframework.cloud.gateway.filter.ForwardPathFilter@1df1ced0": 0,
    "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@5349b246": 2147483646,
    "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6fc6deb7": -1,
    "org.springframework.cloud.gateway.filter.GatewayMetricsFilter@32b0876c": 0,
    "org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@367f0121": -2147483648
}

 在上面的配置后,還是只能使用之前配置的路由。來自博客園

 

進一步:添加下面的配置后,可以無需手動添加路由即可訪問注冊中心 所有服務(不建議 生產環境使用)

# spring.cloud.gateway.discovery.locator.enabled=true 默認是 false
    # 路由配置
    gateway:
      discovery:
        locator:
          enabled: true
          lowerCaseServiceId: true

配置后,可以使用下面的鏈接訪問 已注冊的 adapter.web、data.user 服務的端點:

http://localhost:25001/adapter.web/user/get?id=1

http://localhost:25001/data.user/user/get?id=1

注,紅色部分是 小寫了的服務名。

 

此時,/actuator/gateway/routes 也發生了很大的變化,多了很多路由:來自博客園

響應
[
    {
        "predicate": "Paths: [/adapter.web/**], match trailing slash: true",
        "metadata": {
            "jmx.port": "59178",
            "management.port": "21001"
        },
        "route_id": "ReactiveCompositeDiscoveryClient_ADAPTER.WEB",
        "filters": [
            "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
            "[[RewritePath /adapter.web/?(?<remaining>.*) = '/${remaining}'], order = 1]",
            "[[RequestTime logEnabled=true], order = 2]"
        ],
        "uri": "lb://ADAPTER.WEB",
        "order": 0
    },
    {
        "predicate": "Paths: [/data.user/**], match trailing slash: true",
        "metadata": {
            "jmx.port": "59158",
            "management.port": "20001"
        },
        "route_id": "ReactiveCompositeDiscoveryClient_DATA.USER",
        "filters": [
            "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
            "[[RewritePath /data.user/?(?<remaining>.*) = '/${remaining}'], order = 1]",
            "[[RequestTime logEnabled=true], order = 2]"
        ],
        "uri": "lb://DATA.USER",
        "order": 0
    },
    {
        "predicate": "Paths: [/external.gateway/**], match trailing slash: true",
        "metadata": {
            "jmx.port": "53359",
            "management.port": "25001"
        },
        "route_id": "ReactiveCompositeDiscoveryClient_EXTERNAL.GATEWAY",
        "filters": [
            "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
            "[[RewritePath /external.gateway/?(?<remaining>.*) = '/${remaining}'], order = 1]",
            "[[RequestTime logEnabled=true], order = 2]"
        ],
        "uri": "lb://EXTERNAL.GATEWAY",
        "order": 0
    },
    {
        "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
        "route_id": "route1",
        "filters": [
            "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
            "[[AddRequestHeader addHead = 'abc'], order = 1]",
            "[[RequestTime logEnabled=true], order = 2]",
            "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
        ],
        "uri": "http://localhost:21001",
        "order": 0
    }
]

甚至通過 其自身(/external.gateway/**) 來訪問——不會死循環嗎?!

還好,spring.cloud.gateway.discovery.locator.* 下還有很多配置(可以在 官文 查到):

 

負載均衡配置:

開啟3個adapter.web服務,端口分別為:21001、21002、21003。來自博客園

去掉前面的spring.cloud.gateway.discovery.locator.* 的配置。

修改路由中的uri為下面的:lb://adapter.web

      - id: route1
        # 1)服務
#        uri: http://localhost:21001
        # 負載均衡訪問服務 adapter.web
        uri: lb://adapter.web

訪問 /web/user/get?id=1,檢查 3個adapter.web服務 是否均衡地收到並處理了請求:成功,均衡地處理了請求。

 

小結,

網關服務化后,可以很方便地實現負載均衡地訪問代理服務。來自博客園

spring.cloud.gateway.discovery.locator.* 的最佳實踐還有待探索,比如,根據前綴只允許訪問服務中的部分請求,這就需要開發不同的斷言、過濾器了吧。

 

2、限流

限流,限制進入系統的流量。

限流的作用:1)防止流量突發使服務器過載;2)防止流量攻擊。

常見限流維度:IP限流、請求URL限流、用戶訪問頻次限流。(注:在使用微信公眾平台接口時,還可以限制每個賬號每小時、每天的調用次數等)

限流發生的位置:1)網關層(Nginx、Zuul、S.C.Gateway等),2)應用層。

本文介紹在S.C.Gateway中實現限流

搜索:常見限流算法——計數器算法、漏桶算法、令牌桶算法

 

自定義pre類型的過濾器,可以實現需要的限流算法。來自博客園

在S.C.Gateway中,已經提供了一個 RequestRateLimiterGatewayFilterFactory,其使用 Redis和Lua腳本實現令牌桶算法進行限流。

@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
		extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {
        
    // ...
    
	private final RateLimiter defaultRateLimiter;

	private final KeyResolver defaultKeyResolver;
    
    // ...
}

Lua腳本位置:

注,官文中的 The RequestRateLimiter GatewayFilter Factory 一節有它詳細的介紹。來自博客園

6.10. The RequestRateLimiter GatewayFilter Factory

The RequestRateLimiter GatewayFilter factory uses a RateLimiter implementation to determine if the
current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by
default) is returned.

This filter takes an optional keyResolver parameter and parameters specific to the rate limiter
(described later in this section).

 

使用RequestRateLimiterGatewayFilterFactory限流

實現根據遠程主機地址限流。

由於S.C.Gateway基於Netty,因此,需要引入reactive版本的redis:

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

配置Redis:來自博客園

spring:
  # Redis配置-限流使用
  redis:
    host: mylinux
    port: 6379

建立KeyResolver類並注冊到Spring容器:

# HostAddrKeyResolver.java
public class HostAddrKeyResolver implements KeyResolver {

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

}

# AppConfig.java
@Configuration
public class APPConfig {

	/**
	 * 限流的鍵解析器
	 * @author ben
	 * @date 2021-09-13 16:48:26 CST
	 * @return
	 */
	@Bean
	public HostAddrKeyResolver hostAddrKeyResolver() {
		return new HostAddrKeyResolver();
	}
    
}

配置路由使用RequestRateLimiterGatewayFilterFactory:

配置方式和其它的不太一樣,具體需要看看源碼。

配置參數已在下面的注釋中有說明(SpEL真的很重要,使用Spring時鍵值無處不在啊)!

        # 過濾器配置
        filters:
        # 限流過濾器
        - name: RequestRateLimiter
          args:
            # 用於限流的鍵的解析器的Bean對象的名稱——SpEL表達式
            # 默認有一個 PrincipalNameKeyResolver類,下面的hostAddrKeyResolver 需要自行實現
            key-resolver: '#{@hostAddrKeyResolver}'
            # 令牌桶每秒的平均填充速率
            redis-rate-limiter.replenishRate: 1
            # 令牌桶總量
            redis-rate-limiter.burstCapacity: 3

測試限流效果:來自博客園

1)Postman:快速點擊(要足夠快),以此觸發限流機制

測試期間發現響應的狀態為:429 Too Many Requests,此時觸發了限流規則。

2)Apache JMeter:配置多個線程快速訪問

 

在Redis中,限流的數據是怎么保存的呢?檢查下。來自博客園

redis-cli檢查
# 多了兩個key
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04set1"
2) "request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens"
3) "\xac\xed\x00\x05t\x00\x05test3"
4) "request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp"
5) "\xac\xed\x00\x05t\x00\x05test1"
127.0.0.1:6379>

# 生存期很短
127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
(integer) 4
127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
"2"

127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
"1631535157"
127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
(integer) 4

 

小結,

就這樣,在S.C.Gateway中把 限流 用起來了。

上面的用法很簡單,真實的限流則有各種各樣的規則,比如,服務器彈性部署時,網關怎么彈性更改配置呢?更改配置文件嗎?這個時候就需要編程來實現了。

Gateway中有限流,底層應用是否也要有限流呢?兩者如何互補?

Gateway中的令牌桶限流算法的實現原理是怎樣的?那個Lua腳本是怎么寫的?都需要繼續探索的。

先讀一遍官文才好。

 

3、編程配置路由

在前面的示例中,網關的配置都是在 配置文件中完成的。

是否可以通過編程來實現路由配置呢?

配置文件配置 和 編程配置,兩種方式的優缺點分別是什么?來自博客園

 

編程配置路由方式:使用Spring容器中routeLocatorBuilder Bean生成一個RouteLocator Bean即可

默認下,已經有 routeDefinitionRouteLocator、cachedCompositeRouteLocator 兩個Bean了,是用來做什么的呢?

示例代碼:用各種方式,配置了 3個路由

package org.lib.external.gateway.routes;

import java.util.function.Function;

import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.Buildable;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 應用路由配置(編程方式)
 * @author ben
 * @date 2021-09-13 20:47:31 CST
 */
@Configuration
public class AppRoutesConfig {

	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder
				.routes()
				// 路由1
				.route("routeP1", new Function<PredicateSpec, Buildable<Route>>() {
					
					@Override
					public Buildable<Route> apply(PredicateSpec t) {
						return t.order(3)
								.path("/user/**")
								.filters(f->f.addResponseHeader("program-header", "routeP1"))
								.uri("lb://adapter.web");
					}
				})
				// 路由2
				.route("routeP2", r->r.order(2)
						.path("/routeP2/**")
						.filters(f->f.addResponseHeader("program-header", "routeP2")
								.retry(3)
								.rewritePath("/routeP2/(?<segment>.*)", "/$\\{segment}"))
						.uri("lb://adapter.web")
				)
				// 路由3
				.route(r->r.order(-10)
						.path("/routeP3/**")
						.filters(f->f.addResponseHeader("program-header", "routeP3")
								.rewritePath("/routeP3/(?<segment>.*)", "/$\\{segment}"))
						.uri("lb://adapter.web")
				)
				.build();
	}
	
}

 

訪問/actuator/gateway/routes:按優先級 從高到低 展示了系統中的路由,其中,第二的route1 是 配置文件中的,看來可以共存。

[
    {
        "predicate": "Paths: [/routeP3/**], match trailing slash: true",
        "route_id": "c48fc06f-380c-405d-abf4-791df2008e37",
        "filters": [
            "[[RewritePath /routeP3/(?<segment>.*) = '/${segment}'], order = 0]"
        ],
        "uri": "lb://adapter.web",
        "order": -10
    },
    {
        "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
        "route_id": "route1",
        "filters": [
            "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
            "[[AddRequestHeader addHead = 'abc'], order = 1]",
            "[[RequestTime logEnabled=true], order = 2]",
            "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
            "[org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory$$Lambda$978/889546737@2cad0ced, order = 3]"
        ],
        "uri": "lb://adapter.web",
        "order": 0
    },
    {
        "predicate": "Paths: [/routeP2/**], match trailing slash: true",
        "route_id": "routeP2",
        "filters": [
            "[[AddRequestHeader program-header = '210913'], order = 0]",
            "[[Retry routeId = 'routeP2', retries = 3, series = list[SERVER_ERROR], statuses = list[[empty]], methods = list[GET], exceptions = list[IOException, TimeoutException]], order = 0]",
            "[[RewritePath /routeP2/(?<segment>.*) = '/${segment}'], order = 0]"
        ],
        "uri": "lb://adapter.web",
        "order": 2
    },
    {
        "predicate": "Paths: [/user/**], match trailing slash: true",
        "route_id": "routeP1",
        "filters": [
            "[[AddRequestHeader program-header = '210913'], order = 0]"
        ],
        "uri": "lb://adapter.web",
        "order": 3
    }
]

測試使用4個路由訪問 web適配層應用:都能成功獲取數據

http://localhost:25001/user/get?id=1
http://localhost:25001/web/user/get?id=1
http://localhost:25001/routeP2/user/get?id=1
http://localhost:25001/routeP3/user/get?id=1

 

小結:

編程添加路由,策略有變化,需要重啟服務:確定。來自博客園

配置文件中添加路由。策略有變化,是否不需要重啟服務?更新配置即可?TODO

對了,上面說的配置文件更新,是指存放於外部的配置文件(S.C.Config)更改后,是否可以更新到 正在運行的 網關服務?

哪些路由需要使用 編程添加,哪些通過 配置文件添加?

還是再看看官文吧,最權威的。來自博客園

 

》》》全文完《《《

 

參考文檔

1、《深入理解Spring Cloud與微服務構建》

2019年9月第2版,作者:方志朋

2、

 


免責聲明!

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



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