我去,你竟然還不會用API網關!

同時爲了應對業務的細分以及高併發的挑戰,微服務的架構被廣泛使用,由於微服務架構中應用會被拆分成多個服務。

爲了方便客戶端對這些服務的調用於是引入了 API 的概念。今天我們就來看看API 網關的原理以及它是如何應用的。

API 網關的定義

網關一詞最早出現在網絡設備,比如兩個相互獨立的局域網之間通過路由器進行通信, 中間的路由被稱之爲網關。

落實在開發層面來說,就是客戶端與微服務系統之間存在的網關。從業務層面來說,當客戶端完成某個業務的時候,需要同時調用多個微服務。

如圖 1 所示,當客戶端發起下單請求需要調用:商品查詢、庫存扣減以及訂單更新等服務。
我去,你竟然還不會用API網關!
圖1 :API 網關加入前後對比

如果這些服務需要客戶端分別調用才能完成,會增加請求的複雜度,同時也會帶來網絡調用性能的損耗。因此,針對微服務的應用場景就推出了 API 網關的調用。

在客戶端與微服務之間加入下單 API 網關,客戶端直接給這個 API 網關下達命令,由於後者完成對其他三個微服務的調用並且返回結果給客戶端。

從系統層面來說,任何一個應用系統如果需要被其他系統調用,就需要暴露 API,這些 API 代表着的功能點。

正如上面下單的例子中提到的,如果一個下單的功能點需要調用多個服務的時候,在這個下單的 API 網關中就需要聚合多個服務的調用。

這個聚合的方式有點像設計模式中的門面模式(Facade),它爲外部的調用提供了一個統一的訪問入口。

不僅如此,如圖 2 所示,API 網關還可以協助兩個系統的通信,在系統之間加上一個中介者協助 API 的調用。
我去,你竟然還不會用API網關!
圖 2:對接兩個系統的 API 網關

從客戶端類型層面來說,爲了屏蔽不同客戶端調用差異也可以加入 API 網關。

如圖 3 所示,在實際開發過程中 API 網關還可以根據不同的客戶端類型(iOS、Android、PC、小程序),提供不同的 API 網關與之對應。
我去,你竟然還不會用API網關!
圖 3:對接客戶端和服務端的 API 網關

由於 API 網關所處的位置是客戶端與微服務交界的地方,因此從功能上它還包括:路由,負載均衡,限流,緩存,日誌,發佈等等。

Spring Cloud Gateway 概念與定義

API 網關的定義中我們提到了爲什麼要使用 API 網關,是爲了解決客戶端對多個微服務進行訪問的問題。

由於服務的切分導致一個操作需要同時調用多個服務,因此爲這些服務的聚合提供一個統一的門面,這個門面就是 API 網關。

針對於 API 網關有很多的實現方式,例如:Zuul,Kong 等等。這裏我們以及 Spring Cloud Gateway 爲例展開給大家介紹其具體實現。

一般來說,API 網關對內將微服務進行集合,對外暴露的統一 URL 或者接口信息供客戶端調用。

那麼客戶端是如何與微服務進行連接,並且進行溝通的,需要引入下面幾個重要概念 。
我去,你竟然還不會用API網關!
圖 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 工作原理

說完了 Spring Cloud Gateway 定義和要素,再來看看其工作原理。總的來說是對客戶端請求的處理過程。
我去,你竟然還不會用API網關!
圖 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 的定義和實現原理,下面根據幾個常用的場景介紹一下 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% 的請求會路由到新服務上去。

從而觀察新服務的穩定性,或者得到用戶的反饋。當新服務穩定以後,再將剩下的流量一起導入過去。
我去,你竟然還不會用API網關!
圖 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 的定義和概念的解釋,其實現了路由、過濾器、斷言,針對不同的客戶端請求可以路由到不同的微服務,以及其中幾個組件是如何分工合作完成路由工作的。

在最佳實踐的介紹中分別從:基本路由、權重路由、限流和動態路由幾個方面進行了闡述。


如何構建高併發架構?請關注我的專欄 秒殺高併發白話實戰

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