我去,你居然还不会用API网关!

同时为了应对业务的细分以及高并发的挑战,微服务的架构被普遍使用,因为微服务架构中应用会被拆分红多个服务。css

为了方便客户端对这些服务的调用因而引入了 API 的概念。今天咱们就来看看API 网关的原理以及它是如何应用的。git

API 网关的定义

网关一词最先出如今网络设备,好比两个相互独立的局域网之间经过路由器进行通讯, 中间的路由被称之为网关。github

落实在开发层面来讲,就是客户端与微服务系统之间存在的网关。从业务层面来讲,当客户端完成某个业务的时候,须要同时调用多个微服务。spring

如图 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 的定义和概念的解释,其实现了路由、过滤器、断言,针对不一样的客户端请求能够路由到不一样的微服务,以及其中几个组件是如何分工合做完成路由工做的。

在最佳实践的介绍中分别从:基本路由、权重路由、限流和动态路由几个方面进行了阐述。


如何构建高并发架构?请关注个人专栏 秒杀高并发白话实战

相关文章
相关标签/搜索