基於Redis實現Spring Cloud Gateway的動態管理

原文鏈接:https://www.toutiao.com/i6727549720136253966/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1571015419&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&req_id=2019101409101901001005322223B4889C&group_id=6727549720136253966

引言:

Spring Cloud Gateway是當前使用非常廣泛的一種API網關。它本身能力並不能完全滿足企業對網關的期望,人們希望它可以提供更多的服務治理能力。但Spring Cloud Gateway並不提供數據的動態管理,甚至修改個路由都需要重啓。我們如何解決它這個短板,同時實現治理配置數據的高效動態管理呢?本文將帶來我們網關與Redis組合的實踐。

目錄:

1.Spring Cloud Gateway 簡介

2.網關數據管理

3.實現細節

1.Spring Cloud Gateway 簡介

API 網關

API 網關出現的原因是微服務架構的出現,不同的微服務一般會有不同的網絡地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:

  • 客戶端會多次請求不同的微服務,增加了客戶端的複雜性。
  • 存在跨域請求,在一定場景下處理相對複雜。
  • 認證複雜,每個服務都需要獨立認證。
  • 難以重構,隨着項目的迭代,可能需要重新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那麼重構將會很難實施。
  • 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難。

以上這些問題可以藉助 API 網關解決。API 網關是介於客戶端和服務器端之間的中間層,所有的外部請求都會先經過 API 網關這一層。也就是說,API 的實現方面更多的考慮。

 

基於Redis實現Spring Cloud Gateway的動態管理

 

使用 API 網關後的優點如下:

  • 易於監控。可以在網關收集監控數據並將其推送到外部系統進行分析。
  • 易於認證。可以在網關上進行認證,然後再將請求轉發到後端的微服務,而無須在每個微服務中進行認證。
  • 減少了客戶端與各個微服務之間的交互次數。

 

Spring Cloud Gateway

Spring Cloud Gateway是Spring官方基於Spring 5.0,Spring Boot 2.0和Project Reactor等技術開發的網關,Spring Cloud Gateway旨在爲微服務架構提供一種簡單而有效的統一的API路由管理方式。

Spring Cloud Gateway作爲Spring Cloud生態系中的網關,目標是替代Netflix ZUUL,其不僅提供統一的路由方式,並且基於Filter鏈的方式提供了網關基本的功能,例如:安全,監控/埋點,和限流等。

基於Redis實現Spring Cloud Gateway的動態管理

SCG架構

如圖所示,SCG的架構看起來很簡單。

首先,它內部包含了一個高性能的Netty Server,用來接收各類網絡請求。請求進來之後,會根據配置的各個路由進行匹配並處理請求。每個路由都可以定義多個斷言(Predicate),用於路由匹配。

SCG默認提供了10多個內建的斷言,可以基於請求的各個方面(請求頭,路徑,路徑,時間,Cookie,http方法等)進行路由匹配。如果還不夠,用戶還可以自已擴展。

請求匹配到了合適的路由之後,就會按照路由中配置的各過濾器(filter),按順序對請求進行處理。Filter也基本上可以對請求的所有屬性做處理,修改,添加或者除請求頭,修改請求數據,修改返回的數據等,幾乎無所不能。當然,修改請求也只是一方面的用途,認證,鑑權,記錄日誌等也都可以在網關中統一來做。

所有filter形成處理鏈,直到所有的filter處理完,纔會交給最後面的 Netty Client,由它將處理過的請求發送至對應的微服務。

在請求發送至微服務之前,還可以定義它的負載均衡策略(LoadBalancerRule),以決定請求至底發往微服務的哪個實例。

Filter 與 LoadBalancerRule 都支持自行擴展。

2.網關數據管理

實現一個適合自已的網關,對數據管理需要考慮哪些方面的東西呢?

1.首先,我們要考慮一下,我們需要管理些什麼數據。

SCG本身對數據管理的管理是很弱的。它沒有提供數據的持久化方案,它所有的數據都來自初始化,來自它的配置文件(application.yml)。它本身雖然也對外提供了一些管理接口(Actuator API)能力不夠,但能力不夠,且這些修改都是暫時的,網關一停,數據就消失了。這就要求我們要用一套更完善的方案,把網關的這些數據管理起來,不能讓它只能寫在配置文件中,而要支持持久化,支持動態變更。再有就是我們對各微服務的治理數據。網關只用來做路由轉發,那就太浪費了,統一認證,統一鑑權,訪問日誌記錄,應用訪問統計,黑白名單過濾,API訂閱管理,流量限制,甚至數據格式轉換,網絡協議轉換,都可以在網關中來做。而所有的這些能力,無不需要數據的支持。因此,這些服務的治理配置,也是網關需要管理的數據。

2.數據有了,我們還得考慮怎麼把它保存起來,不能網關一重啓,所有數據就沒了。

3.還得再考慮一下數據的讀取。網關對性能的要求是很高的,每次對過關的數據進行治理,都需要去讀取這些配置信息。如果配置信息讀取太消耗資源,無疑對網關是不利的。所以,我們還得考慮數據如何緩存,以提高數據的讀取性能。

4.單個網關,可以處理的請求量是有上限的。爲了應對大的流量,我們可能會需要對網關做水平擴容。當多個網關實例共存時,如何保障對網關的修改,能快速同步到每個網關實例呢?數據變更通知也得考慮。

5.最多,我們還得考慮一下方案的擴展,數據存儲能不能改個地方,通知能不能換種方式?

綜合考慮了這些方面之後,我們的網關的架構如下:

基於Redis實現Spring Cloud Gateway的動態管理

gateway-arch

如圖,以上就是我們網關的整體設計。方案設計要點如下:

  1. 網關對外提供治理數據管理接口, 微服務治理平臺可通過這些接口, 將治理配置推送到網關
  2. 網關通過治理數據統一存儲接口, 將治理配置數據保持至治理數據持久存儲(這裏我們默認爲Redis)
  3. Redis通過發佈訂閱能力, 將數據的變更通知到各網關實例
  4. 各網關實例收到通知後, 將數據從持久存儲同步至內部高速緩存
  5. 內部緩存在網關啓動時, 會自動從持久存儲加載對應配置進入緩存. 同時它也支持清空, 以及按需加載
  6. 外部業務請求經過網關時, 對數據執行鑑權,處理轉換, 以及灰度策略時,所需要治理配置,都從內部緩存中獲取, 以提升性能
  7. 方案中, 外部持久存儲(默認用的Redis, 可以換成Mysql, 文件, Appolo等), 以及數據變更通知(默認使用的是Redis的發佈訂閱, 可以換成Appolo通知, 消息隊列, 定時掃描等), 都是可以擴展的

3.實現細節

動態路由管理

Spring Cloud Gateway作爲所有請求流量的入口,在實際生產環境中爲了保證高可靠和高可用,儘量避免重啓, 需要實現Spring Cloud Gateway動態路由配置。實現動態路由其實很簡單, 重點在於 RouteDefinitionRepository 這個接口. 這個接口繼承自兩個接口, 其中 RouteDefinitionLocator 是用來加載路由的. 它有很多實現類, 其中的 PropertiesRouteDefinitionLocator 就用來實現從yml中加載路由. 另一個 RouteDefinitionWriter 用來實現路由的添加與刪除. 通過查看spring cloud gateway的源碼可以發現, 在 org.springframework.cloud.gateway.config.GatewayAutoConfiguration中這麼一段:

@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
 return new InMemoryRouteDefinitionRepository();
}

可以看出, 網關中如果沒有RouteDefinitionRepository的Bean, 就會採用InMemoryRouteDefinitionRepository做爲實現。這個 InMemoryRouteDefinitionRepository有一個問題, 就是數據沒有持久化, 網關重啓之後,原來通過接口設置的路由就會丟失了。

這當然是不可接受的, 所以我們需要實現自已的 RouteDefinitionRepository, 來提供路由配置信息。如使用redis做爲存儲, 來實現路由的存儲。實現請參考文章:https://dwz.cn/tsHfKwMe

除此以外, 每當路由更改之後, 還需要通知網關刷新路由。這需要發送 RefreshRoutesEvent 來通知網關。如下列示例:

@Component
public class RouteDynamicService implements ApplicationEventPublisherAware {
 private ApplicationEventPublisher publisher;
​
 @Override
 public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
 this.publisher = publisher;
 }
​
 /**
 * 刷新路由表
 */
 public void refreshRoutes() {
 publisher.publishEvent(new RefreshRoutesEvent(this));
 }
}

刷新可以通過消息通知機制來觸發, 當然, 也可以對外接供rest接口, 手動觸發。### 數據存儲

基於Redis實現Spring Cloud Gateway的動態管理

 

如上述類圖所示, IGovernDataRepository爲治理數據統一存儲接口。RedisGovernDataRepository爲實現的它的抽像類, 它需要依賴兩個, 一個是StringRedisTemplate,用來實現redis數據的存儲。另一個爲 RedisKeyGenerator, 用來爲各治理對象生成對應的key。RedisGovernDataRepository下面則爲各個治理數據存儲的實現類。使用Redis做爲持久存儲時, 需要注意以下幾點:

  1. 爲對象生成key時, 建議爲key添加一個命名空間(就是加一段有意義的前綴)
  2. 在redis中進行模糊搜索時, 提供給Redis的pattern, 不能是一個正則的通配, 它支持三種通配 *(多個), ?(單個)
  3. 如果數據量比較大, 不建議使用keys進行模糊查詢, 應該使用scan方式

數據緩存

我們提供了內部緩存,它處於使用者與持久存儲之間,緩存數據以提升性能。緩存的實現主要有如下幾點:

  1. 實現了 InitializingBean 以實現在網關啓動時, 自動加載數據
  2. 內部使用了ConcurrentHashMap, 保證寫時的線程同步, 又保證了get時的高效(get整個過程不需要加鎖)
  3. 從緩存中取數據時, 如果需要懶加載, 當從持久存儲中加載不到數據時, 建議使用空數據, 或空集合佔位, 避免每次都去持久存儲中查詢

代碼示例如下:

/**
 * 根據 appCode 獲取流量策略
 * 
 * @param appCode
 * @return
 */
public Set<ApplicationTrafficPolicy> getAppTrafficPolicies(String appCode) {
 // 從緩存加載
 Map<String, ApplicationTrafficPolicy> map = policyMap.get(appCode);
 // 緩存中沒有
 if (map == null) {
 // 嘗試從持久存儲中加載所有此網關的流量策略
 Set<ApplicationTrafficPolicy> policies = trafficPolicyRepository.fuzzyQuery();
 // 持久存儲中沒有任何流量策略,佔個位置,防止緩存重複去加載
 if (policies == null || policies.size() == 0) {
 map = new ConcurrentHashMap<>();
 policyMap.put(appCode, map);
 } else {
 // 持久存儲中有流量策略,放入緩存
 for (ApplicationTrafficPolicy policy : policies) {
 setTrafficPolicy(policy);
 }
 // 重新從緩存中加載一次
 map = policyMap.get(appCode);
 // 如果還是沒有,使用空 map 佔位子
 if (map == null) {
 map = new ConcurrentHashMap<>();
 policyMap.put(appCode, map);
 }
 }
 }
 return map.values().stream().collect(Collectors.toSet());
}

事件通知

事件通知,這裏我們使用的是redis的發佈與訂閱能力。Redis默認是不發送事件的,要讓它發佈事件,需要先修改它的配置文件redis.conf,添加一個配置:

notify-keyspace-events "K$g"

上面的配置將使得Redis中發生數據的添加,修改或刪除時,發送set或del事件。

然後,我們需要配置一個RedisMessageListenerContainer,用來訂閱我們感興趣的事件。

@Bean
RedisMessageListenerContainer container(MessageListenerAdapter listenerAdapter) {
 String gtwReidsPattern = "__keyspace@*__:" + GTW + keyGenerator.getGatewayCode() + "]*";
 String cofRedisPattern = "__keyspace@*__:" + COF + cacheKey.getKeyNameSpace() + USER_NAME + "*";
 log.info("Add gateway redis message listener, patternTopic is {}", gtwReidsPattern);
 log.info("Add coframe redis message listener, patternTopic is {}", cofRedisPattern);
 RedisMessageListenerContainer container = new RedisMessageListenerContainer();
 container.setConnectionFactory(redisTemplate.getConnectionFactory());
 // PatternTopic 參考:http://redisdoc.com/topic/notification.html
 container.addMessageListener(listenerAdapter, Arrays.asList(new PatternTopic(PatternUtil.fmt(gtwReidsPattern)), new PatternTopic(PatternUtil.fmt(cofRedisPattern))));
 return container;
}
當redis事件訂閱好了之後, 每次其中我們關心的數據有變更, 都會發送set或del事件.
我們需要定義一個 MessageListener, 來接收事件:
@Service(value = RedisMessageListener.REDIS_LISTENER_NAME)
public class RedisMessageListener implements MessageListener {
 @Override
 public void onMessage(Message message, byte[] pattern) {
 String ops = new String(message.getBody());
 String channel = new String(message.getChannel());
 String key = channel.split(":")[1];
​
 if ("set".equals(ops)) {
 String value = redisTemplate.opsForValue().get(key);
 handleSet(key, value);
 } else if ("del".equals(ops)) {
 handleDel(key);
 }
 }
 ...
}

接收到事件後,會調用相應的內部緩存,更新內部緩存中的數據,以實現治理數據變更的及時生效。

基於Redis實現Spring Cloud Gateway的動態管理

 

關於作者:將曉漁,現任普元雲計算架構師。曾在PDM,雲計算,數據備份,移動互聯相關領域公司工作,十年以上IT工作經驗。曾爲科企桌面虛擬化產品的核心工程師,愛數容災備份雲櫃系統設計師,萬達信息的食安管理與追溯平臺開發經理。國內IAAS雲計算的早期實踐者,容器技術專家。

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