同時爲了應對業務的細分以及高併發的挑戰,微服務的架構被普遍使用,因爲微服務架構中應用會被拆分紅多個服務。css
爲了方便客戶端對這些服務的調用因而引入了 API 的概念。今天咱們就來看看API 網關的原理以及它是如何應用的。git
網關一詞最先出如今網絡設備,好比兩個相互獨立的局域網之間經過路由器進行通訊, 中間的路由被稱之爲網關。github
落實在開發層面來講,就是客戶端與微服務系統之間存在的網關。從業務層面來講,當客戶端完成某個業務的時候,須要同時調用多個微服務。spring
如圖 1 所示,當客戶端發起下單請求須要調用:商品查詢、庫存扣減以及訂單更新等服務。
圖1 :API 網關加入先後對比小程序
若是這些服務須要客戶端分別調用才能完成,會增長請求的複雜度,同時也會帶來網絡調用性能的損耗。所以,針對微服務的應用場景就推出了 API 網關的調用。設計模式
在客戶端與微服務之間加入下單 API 網關,客戶端直接給這個 API 網關下達命令,因爲後者完成對其餘三個微服務的調用而且返回結果給客戶端。緩存
從系統層面來講,任何一個應用系統若是須要被其餘系統調用,就須要暴露 API,這些 API 表明着的功能點。網絡
正如上面下單的例子中提到的,若是一個下單的功能點須要調用多個服務的時候,在這個下單的 API 網關中就須要聚合多個服務的調用。架構
這個聚合的方式有點像設計模式中的門面模式(Facade),它爲外部的調用提供了一個統一的訪問入口。併發
不只如此,如圖 2 所示,API 網關還能夠協助兩個系統的通訊,在系統之間加上一個中介者協助 API 的調用。
圖 2:對接兩個系統的 API 網關
從客戶端類型層面來講,爲了屏蔽不一樣客戶端調用差別也能夠加入 API 網關。
如圖 3 所示,在實際開發過程當中 API 網關還能夠根據不一樣的客戶端類型(iOS、Android、PC、小程序),提供不一樣的 API 網關與之對應。
圖 3:對接客戶端和服務端的 API 網關
因爲 API 網關所處的位置是客戶端與微服務交界的地方,所以從功能上它還包括:路由,負載均衡,限流,緩存,日誌,發佈等等。
API 網關的定義中咱們提到了爲何要使用 API 網關,是爲了解決客戶端對多個微服務進行訪問的問題。
因爲服務的切分致使一個操做須要同時調用多個服務,所以爲這些服務的聚合提供一個統一的門面,這個門面就是 API 網關。
針對於 API 網關有不少的實現方式,例如:Zuul,Kong 等等。這裏咱們以及 Spring Cloud Gateway 爲例展開給你們介紹其具體實現。
通常來講,API 網關對內將微服務進行集合,對外暴露的統一 URL 或者接口信息供客戶端調用。
那麼客戶端是如何與微服務進行鏈接,而且進行溝通的,須要引入下面幾個重要概念 。
圖 4:路由、斷言和過濾器
如圖 4 所示,Spring Cloud Gateway 由三部分組成:
①路由(Route):任何一個來自於客戶端的請求都會通過路由,而後到對應的微服務中。
每一個路由會有一個惟一的 ID 和對應的目的 URL。同時包含若干個斷言(Predicate)和過濾器(Filter)。
②斷言(Predicate):當客戶端經過 Http Request 請求進入 Spring Cloud Gateway 的時候,斷言會根據配置的路由規則,對 Http Request 請求進行斷言匹配。
說白了就是進行一次或者屢次 if 判斷,若是匹配成功則進行下一步處理,不然斷言失敗直接返回錯誤信息。
③過濾器( Filter):簡單來講就是對流經的請求進行過濾,或者說對其進行獲取以及修改的操做。注意過濾器的功能是雙向的,也就是對請求和響應都會進行修改處理 。
通常來講 Spring Cloud Gateway 中的過濾器有兩種類型:
Gateway Filter
Global Filter
Gateway Filter 用在單個路由和分組路由上。Global Filter 能夠做用於全部路由,是一個全局的 Filter。
說完了 Spring Cloud Gateway 定義和要素,再來看看其工做原理。總的來講是對客戶端請求的處理過程。
圖 5:Spring Cloud Gateway 處理請求流程圖
如圖 5 所示,當客戶端向 Spring Cloud Gateway 發起請求,該請求會被 HttpWebHandlerAdapter 獲取,而且對請求進行提取,從而組裝成網關上下文。
將組成的上下文信息傳遞到 DispatcherHandler 組件。DispatcherHandler 做爲請求分發處理器,主要負責將請求分發到對應的處理器進行處理。
這裏請求的處理器包括 RoutePredicate HandlerMapping (路由斷言處理映射器) 。
路由斷言處理映射器用於路由的查找,以及找到 路由後返回對應的 FilteringWebHandler。
其負責組裝 Filter 鏈表並執行過濾處理,以後再將請求轉交給應用服務,應用服務處理完後,最後返回 Response 給客戶端 。
其中 FilteringWebHandler 處理請求的時候會交給 Filter 進行過濾的處理。
這裏須要注意的是因爲 Filter 是雙向的因此,當客戶端請求服務的時候,會經過 Pre Filter 中的 Filter 處理請求。
當服務處理完請求之後返回客戶端的時候,會經過 Post Filter 再進行一次處理。
上面介紹了 Spring Cloud Gateway 的定義和實現原理,下面根據幾個經常使用的場景介紹一下 Spring Cloud Gateway 如何實現網關功能的。
咱們會根據基本路由、權重路由、限流、動態路由幾個方面給你們展開介紹。
基本路由,主要功能就是在客戶端請求的時候,根據定義好的路徑指向到對應的 URI。這個過程當中須要用到 Predicates(斷言)中的 Path 路由斷言處理器。
首先在 POM 文件中加入對應的依賴,以下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
加入以下代碼,其中定義的 Path 的路徑「/baidu」就是請求時的路徑地址。對應的 URI,http://www.baidu.com/ 就是要跳轉到的目標地址。
@Bean public RouteLocator routeLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r ->r.path("/baidu") .uri("http://www.baidu.com/").id("baidu_route") ).build(); }
一樣上面的功能也能夠在 yml 文件中實現。配置文件以下,說白了就是對 Path 和 URI 參數的設置,實現的功能和上面代碼保持一致。
spring: cloud: gateway: routes: - id: baidu_route uri: http://baidu.com:80/ predicates: - Path=/baidu
此時啓動 API 網關,假設網關的訪問地址是「localhost:8080/baidu」,當用戶請求這個地址的時候就會自動請求「www.baidu.com」這個網站。這個配置起來很簡單,有 Nginx 基礎的朋友應該很快就能上手。
這個使用場景相對於上面的簡單路由要多一些。因爲每一個微服務發佈新版本的時候,一般會保持老版本與新版版同時存在。
而後經過網關將流量逐步從老版本的服務切換到新版本的服務。這個逐步切換的過程就是常說的灰度發佈。
此時,API 網關就起到了流量分發的做用,一般來講最開始的老版本會承載多一些的流量,例如 90% 的請求會被路由到老版本的服務上,只有 10% 的請求會路由到新服務上去。
從而觀察新服務的穩定性,或者獲得用戶的反饋。當新服務穩定之後,再將剩下的流量一塊兒導入過去。
圖 6:灰度發佈,路由到新/老服務
以下代碼所示,假設 API 網關仍是採用 8080 端口,須要針對兩個不一樣的服務配置路由權重。所以在 routes 下面分別配置 service_old 和 service_new。
server.port: 8080 spring: application: name: gateway-test cloud: gateway: routes: - id: service_old uri: http://localhost:8888/v1 predicates: - Path=/gatewaytest - Weight=service, 90 - id: service_new uri: http://localhost:8888/v2 predicates: - Path=/gatewaytest - Weight=service, 10
在兩個配置中對應的 URI 分別是新老兩個服務的訪問地址,經過「http://localhost:8888/v1」和「http://localhost:8888/v2」來區別。
在 Predicates(斷言)中定義了的 Path 是想通的都是「/gatewaytest」,也就是說對於客戶端來講訪問的路徑都是同樣的,從路徑上客戶不會感知他們訪問的是新服務或者是老服務。
主要參數是在 Weight,針對老/新服務分別配置的是 90 和 10。也就是有 90% 的流量會請求老服務,有 10% 的流量會請求新服務。
簡單點說,若是有 100 次請求,其中 90 次會請求 v1(老服務),另外的 10 次會請求 v2(新服務)。
當服務在短期內迎來高併發,併發量超過服務承受的範圍就須要使用限流。例如:秒殺、搶購、下單服務。
經過請求限速或者對一個時間窗口內的請求進行限速來保護服務。當達到限制速率則能夠拒絕請求,返回錯誤代碼,或者定向到友好頁面。
通常的中間件都會有單機限流框架,支持兩種限流模式:
控制速率
控制併發
這裏經過 Guava 中的 Bucket4j 來實現限流操做。按照慣例引入 Bucket4j 的依賴:
<dependency> <groupId>com.github.vladimir-bukhtoyarov</groupId> <artifactId>bucket4j-core</artifactId> <version>4.0.0</version> </dependency>
因爲須要對於用戶請求進行監控,所以經過實現 GatewayFilter 的方式自定義 Filter,而後再經過 Gateway API Application 應用這個自定義的 Filter。
這裏咱們使用的是令牌桶的方式進行限流,所以須要設置桶的容量(capacity),每次填充的令牌數量(refillTokens)以及填充令牌的間隔時間(refillDuration)。
初始化這三個參數之後,經過 createNewBucket 方法針對請求創建令牌桶(bucket),在 Filter 方法中實現限流的主要邏輯。
經過 ServerWebExchange 獲取請求的上下文中的 IP 信息,針對 IP 創建對應的令牌桶,這個 IP 與令牌桶的對應關係放到了 LOCAL_CACHE 中。
每次請求通過的時候經過 tryConsume(1) 方法消費一個令牌,直到沒有令牌的時候返回 HttpStatus.TOO_MANY_REQUESTS 的狀態碼(429),此時網關直接返回請求次數太多,即使是再有請求進來也不會路由到對應的服務了。
只有等待下一個時間間隔,必定數量的令牌放到桶裏的時候,請求拿到桶中的令牌才能再次請求服務。
public class GatewayRateLimitFilterByIp implements GatewayFilter, Ordered { private static final Map<String, Bucket> LOCAL_CACHE = new ConcurrentHashMap<>(); int capacity; int refillTokens; Duration refillDuration; public GatewayRateLimitFilterByIp() { } public GatewayRateLimitFilterByIp(int capacity, int refillTokens, Duration refillDuration) { this.capacity = capacity; this.refillTokens = refillTokens; this.refillDuration = refillDuration; } private Bucket createNewBucket() { Refill refill = Refill.of(refillTokens, refillDuration); Bandwidth limit = Bandwidth.classic(capacity, refill); return Bucket4j.builder().addLimit(limit).build(); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); Bucket bucket = LOCAL_CACHE.computeIfAbsent(ip, k -> createNewBucket()); if (bucket.tryConsume(1)) { return chain.filter(exchange); } else { exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } } }
上面的代碼定義了 Filter 其中針對訪問的 IP 生成令牌桶,而且定義了桶的大小、每次放入桶令牌的個數、放入令牌的間隔時間。
而且經過 Filter 方法重寫了過濾的邏輯,那麼下面只須要將這個 Filter 應用到 Spring Cloud Gateway 的規則上去就能夠了。經過下面代碼定義網關的路由斷言和過濾器。
在 Filters 中新建一個上面代碼定義的過濾器,指定容量是 20,每兩秒放入令牌,每次放入一個令牌。
那麼當用戶訪問 rateLimit 路徑的時候就會根據客製化的 Filter 進行限流。
@Bean public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/rateLimit") .filters(f -> f.filter(new GatewayRateLimitFilterByIp(20,1,Duration.ofSeconds(2)))) .uri("http://localhost:8888/rateLimit") .id("rateLimit_route") ).build(); }
這裏的限流只是給你們提供一種思路,經過實現 GatewayFilter,重寫其中的 Filter 方法,加入對流量的控制代碼,而後在 Spring Cloud Gateway 中進行應用就能夠了。
因爲 Spring Cloud Gateway 自己也是一個服務,一旦啓動之後路由配置就沒法修改了。
不管是上面提到的編碼注入的方式仍是配置的方式,若是須要修改都須要從新啓動服務。
若是回到 Spring Cloud Gateway 最初的定義,咱們會發現每一個用戶的請求都是經過 Route 訪問對應的微服務,在 Route 中包括 Predicates 和 Filters 的定義。
只要實現 Route 以及其包含的 Predicates 和 Filters 的定義,而後再提供一個 API 接口去更新這個定義就能夠動態地修改路由信息了。
按照這個思路須要作如下幾步來實現:
①定義 Route、Predicates 和 Filters
其中 Predicates 和 Filters 包含在 Route 中。實際上就是 Route 實體的定義,針對 Route 進行路由規則的配置。
public class FilterDefinition { //Filter Name private String name; //對應的路由規則 private Map<String, String> args = new LinkedHashMap<>(); } public class PredicateDefinition { //Predicate Name private String name; //對應的斷言規則 private Map<String, String> args = new LinkedHashMap<>(); } public class RouteDefinition { //斷言集合 private List<PredicateDefinition> predicates = new ArrayList<>(); //路由集合 private List< FilterDefinition > filters= new ArrayList<>(); //uri private String uri; //執行次序 private int order = 0; }
②實現路由規則的操做,包括添加,更新,刪除
有了路由的定義(Route,Predicates,Filters),而後再編寫針對路由定義的操做。
例如:添加路由,刪除路由,更新路由之類的。編寫 RouteServiceImpl 實現 ApplicationEventPublisherAware。
主要須要 override 其中的 setApplicationEventPublisher 方法,這裏會傳入 ApplicationEventPublisher 對象,經過這個對象發佈路由定義的事件包括:add,update,delete。
貼出部分代碼以下:
@Service public class RouteServiceImpl implements ApplicationEventPublisherAware { @Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher publisher; //添加路由規則 public String add(RouteDefinition definition) { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "success"; } public String update(RouteDefinition definition) { try { this.routeDefinitionWriter.delete(Mono.just(definition.getId())); } catch (Exception e) { } try { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "success"; } catch (Exception e) { } } public String delete(String id) { try { this.routeDefinitionWriter.delete(Mono.just(id)); return "delete success"; } catch (Exception e) { } } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; }
③對外部提供 API 接口可以讓用戶或者程序動態修改路由規則
從代碼上來講就是一個 Controller。這個 Controller 中只須要調用 routeServiceImpl 就好了,主要也是用到客製化路由實現類中的 add,update,delete 方法。
說白了就是對其進行了一次包裝,讓外部系統能夠調用,而且修改路由的配置。
通過簡化之後的代碼以下,這裏只對 add 方法進行了包裝,關於 update 和 delete 方法在這裏不展開說明,調用方式相似 add。
public class RouteController { @Autowired private routeServiceImpl routeService; @PostMapping("/add") public String add(@RequestBody RouteDefinition routeDefinition) { try { RouteDefinition definition = assembleRouteDefinition(routeDefinition); return this.dynamicRouteService.add(definition); } catch (Exception e) { } return "succss"; } }
④啓動程序進行路由的添加和更新操做
假設更新 API 網關配置的服務在 8888 端口上。因而經過 http://localhost:8888/actuator/gateway/routes 訪問當前的路由信息,因爲如今沒有配置路由這個信息是空。
那麼經過 http://localhost:8888/route/add 方式添加一條路由規則,這裏選擇 Post 請求,輸入類型爲 Json 以下:
{ "filter":[], "id":"baidu_route", "order":0, "predicates":[{ "args":{ "pattern":"/baidu" }, "name":"Path" }], "uri":"https://www.baidu.com" }
Json 中配置的內容和簡單路由配置的內容很是類似。設置了 Route,當 Predicates 爲 baidu 的時候,將請求引導到 www.baidu.com 的網站進行響應。
此時再經過訪問 http://localhost:8888/baidu 的路徑訪問的時候,就會被路由到 www.baidu.com 的網站。
此時若是須要修改路由配置,能夠經過訪問 http://localhost:8888/route/update 的 API 接口,經過 Post 方式傳入 Json 結構,例如:
{ "filter":[], "id":"CTO_route", "order":0, "predicates":[{ "args":{ "pattern":"/CTO" }, "name":"Path" }], "uri":"https://www.51CTO.com" }
在更新完成之後,再訪問 http://localhost:8888/CTO 的時候就會把引導到 www.51CTO.com 的網站了。
經過上面四步操做,即便不重啓 Spring Cloud Gateway 服務也能夠動態更改路由的配置信息。
因爲微服務的盛行,API 網關悄然興起。針對 API 網關自己講述了其存在的緣由,它不只提供了服務的門面,並且能夠協調不一樣的系統之間的通信以及服務不一樣的客戶端接口。
針對 API 網關的最佳時間 Spring Cloud Gateway 的定義和概念的解釋,其實現了路由、過濾器、斷言,針對不一樣的客戶端請求能夠路由到不一樣的微服務,以及其中幾個組件是如何分工合做完成路由工做的。
在最佳實踐的介紹中分別從:基本路由、權重路由、限流和動態路由幾個方面進行了闡述。