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

EAWorld發表於2020-04-06

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

引言:

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);
}
}
...
}

接收到事件後,會呼叫相應的內部快取,更新內部快取中的資料,以實現治理資料變更的及時生效。

精選提問:

問1:當前閘道器例項因為網路的原因,如果沒有訂閱到訊息,訊息會重發嗎?

答:不會。但記憶體快取會定期清理,以解決這種資料不同步的問題。也可以主動清理。

問2:閘道器使用了zuul了嗎?還是自己實現的閘道器?

答:閘道器於Spring Cloud Gateway開發,他就是一個類似於zuul的API閘道器。

問3:netttyserver是幹嘛的?

答:那是Spring Cloud Gateway本身使用的元件, 用來接收與處理請求的。

問4:檔案上傳的介面也通過閘道器嗎?

答:這個要看具體需求。也可以走閘道器,  但會對效能有一定影響。不走閘道器, 就得在應用那一層來控制許可權。閘道器控制許可權, 只是相當於把許可權校驗前移與統一化了。

問5:在微服務化之後,閘道器路由到服務,呼叫會有超時的情況怎麼處理?有些介面是必須要這麼長時間,例如批量操作 。只能通過加大超時時間嗎?

答:這個一個考慮適當增大超時時間,  另一個,  你可以考慮採用非同步模式, 比如用任務來處理。

問6:我想提問下,目前gateway我看實現是基於netty實現的http協議的,通過相關的mapping處理斷言然後處理過濾器。那有基於netty的tcp協議的實現方案嗎?基於tcp怎麼整合斷言和過濾器呢?

答:TCP的我們也在考慮, 有這方面的需求.  但是直接基於TCP實現斷言與過濾, 工作量估計會比較大.  現在傾向的方案是在閘道器前做一層TCP的協議轉換, 將TCP將成 http 再發往閘道器. 這樣可以直接利用閘道器現有能力。

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

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

轉載本文需註明出處:微信公眾號EAWorld,違者必究。

關於EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享。長按二維碼關注!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562043/viewspace-2654632/,如需轉載,請註明出處,否則將追究法律責任。

相關文章