數據庫

基于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 的實現方面更多的考慮。

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

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

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鏈的方式提供了網關基本的功能,例如:安全,監控/埋點,和限流等。

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.最多,我們還得考慮一下方案的擴展,數據存儲能不能改個地方,通知能不能換種方式?綜合考慮了這些方面之后,我們的網關的架構如下:

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接口, 手動觸發。### 數據存儲 

如上述類圖所示, 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 = "[email protected]*__:" + GTW + keyGenerator.getGatewayCode() + "]*";
String cofRedisPattern = "[email protected]*__:" + 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 再發往網關. 這樣可以直接利用網關現有能力。

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

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

關于EAWorld:微服務,DevOps,數據治理,移動架構原創技術分享。

我還沒有學會寫個人說明!

騰訊財報背后的小秘密:轉型路上的未知

上一篇

Code Review最佳實踐

下一篇

你也可能喜歡

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

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
30岁的男人干啥赚钱快赚钱多