生产环境中的高性能API网关 - Karanbir


在Jeevansathi,我们有超过100万的月活跃用户,我们努力为他们提供最好的用户体验。为了处理这样的流量,我们已经从单片机过渡到了基于微服务的架构。

我们面临的问题
随着时间的推移,随着我们的微服务的扩展,我们感到有必要在多个微服务中处理外部请求的认证。这意味着我们必须开发和维护相同的代码,以便在多个地方对用户进行认证和授权。

为了解决这个问题,API网关被证明是在基于微服务的架构中管理外部请求的一个强大模式。

在这篇博文中,我将解释我是如何使用spring-cloud-gateway实现API网关的,以及与普通的基于web-mvc的路由服务相比,它带来的好处。

市场上有哪些不同的API网关实现方式?
Spring-cloud-gateway、Netflix Zuul、Kong、Apigee等是一些可以使用的API网关的实现方式。

Spring-cloud-gateway¹是一个基于spring-webflux和project reactor的反应式API网关。由于它是非阻塞性的,与Zuul1这样的阻塞性网关相比,有限的线程可以处理相同数量的请求。这使其能够使用更少的资源,并有助于扩展应用程序。

Spring生态系统支持spring-cloud-gateway,而不是Zuul2。 由于我们的微服务架构是围绕spring框架建立的,所以使用spring-cloud-gateway是一个更好的选择。

我是如何使用spring-cloud-gateway实现API网关的?
我们的需求涉及从API网关调用一个认证服务,然后根据其响应转发或阻止请求。所有的外部请求都由API网关接收,API网关只将经过认证和授权的请求传递给相应的下游微服务。

Spring-cloud-starter-gateway是我们build.gradle文件中需要添加的依赖:

//https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-gateway
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-gateway', version: '3.1.0'

1、配置路由:
为了配置下游服务,我只需要在application.properties文件中添加这些属性

service.port=8090 //port for apigateway
spring.cloud.gateway.routes[0].id=service1
spring.cloud.gateway.routes[0].uri=http://127.0.0.1:8080/
spring.cloud.gateway.routes[0].predicates=Path=/service1Id/**

这里id是路由的唯一标识符,uri是传入的请求需要被路由的地方。如果传入的请求符合为该路由定义的谓词,它将被路由到下游,否则将返回一个错误。

例如,如果我们调用运行API网关的http://127.0.0.1:8090/service1Id/demoApi,它将把请求路由到我们的微服务所在的http://127.0.0.1:8080/service1Id/demoApi。

2、配置超时:
我在Jeevansathi学到的一件事是,当在一个接收高流量的系统上工作时,拥有适当的超时是非常关键的,否则,它可能很快就会变成多种问题的雪球。

我使用以下属性配置了全局超时。

//global timeout for all routes
spring.cloud.gateway.httpclient.response-timeout= 2s

然而,有一种情况是,我需要为一个特定的路由设置不同的超时。幸运的是,spring-cloud-gateway通过使用以下属性,也为处理这种用例提供了支持。

//timeout for one specific route in ms
spring.cloud.gateway.routes[0].metadata.response-timeout=3000

3、配置过滤器 :
主要有三种类型的过滤器,我们可以根据自己的要求来选择。

GlobalFilter - 适用于所有的路由,但不包括网关中的控制器。Spring为一般的使用情况提供了许多内置的过滤器,如果需要,我们也可以实现自定义过滤器。
例如,我们可以应用一个内置的过滤器 "AddRequestHeader",在所有的请求中添加头 "request-header",其值为 "request-header-value",方法如下:

//Apply filter for all routes using springs inbuilt filter //"AddRequestHeader"
spring.cloud.gateway.default-filters[0]=AddRequestHeader=request-header, request-header-value

GatewayFilter - 为了配置特定的路由,我们可以通过以下方式应用特定的路由过滤器。

//Apply filter for a specific route using spring inbuilt filter //"AddRequestHeader"
spring.cloud.gateway.routes[0].filters[0]=AddRequestHeader=first-request-header, first-request-header-value

WebFilter- 适用于所有路由以及我们在网关中定义的控制器。我们需要在我们的自定义过滤器中实现WebFilter接口,然后通过重写filter()方法来编写我们的自定义逻辑,方法如下。

@Component
@Slf4j
public class CustomFilter implements WebFilter {
@Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        log.trace("inside custom filter");
        ServerHttpRequest request = exchange.getRequest();
        // logic for request validation
return chain.filter(exchange);
    }
}

为了实现对认证服务的API调用,我使用了WebFilter,因为它可以对API网关和路由中定义的控制器的请求进行认证。

像WebClient这样的反应式HTTP客户端的使用有助于反应式地处理整个请求,即使我们的下游微服务在本质上是阻塞的。

由于这种反应式方法涉及到数量有限的线程来处理所有的请求,阻塞任何线程都会导致整个应用程序瘫痪。为了避免这种情况,我们使用了一个叫做Blockhound⁵的工具,它可以检测到任何阻塞的调用并抛出一个异常。

负载测试
为了进行负载测试,我们在下游的微服务中模拟了一个固定的延迟API。我们在基于web-mvc的应用程序和使用spring-webflux的基于spring-cloud-gateway的应用程序之间进行了比较。我们首先从API网关调用固定延迟的API,然后通过基于web-mvc的应用程序进行负载测试。

关键点:

  • 两者的响应时间在较低的并发量(~100)下几乎是一样的。由于固定延迟的API是一个阻塞的API,响应时间预计不会不同。
  • 在较高的并发量下,API网关在吞吐量和99%的百分位数方面表现好2倍。
    这可以归因于这样的原因:tomcat受到可以打开的最大线程数的限制,但API网关运行在一个完全不同的事件循环线程模型上,所以它可以处理明显更多的请求。

由于我们的下游API在本质上是阻塞的,所以我们并不期望响应时间会有不同。我们所期望的是,由于使用了不同的线程模型,反应式网关与非反应式相比,能够有更好的资源利用率,这一点在负载测试结果中得到了证实。

挑战
转向使用spring提供的操作符在反应式模式下编写代码需要改变我们对请求流的思考方式。
我们面临的一个挑战是,基于MDC的日志记录不能在反应式应用中完成。这是因为MDC是基于Threadlocal的,而在一个反应式的世界里,一个请求不一定与一个特定的线程相关。
为了克服这个问题,我们利用了反应器上下文,它在整个反应式管道中传播,并为日志编写了自定义方法。

结论
总而言之,API网关是微服务架构模式中的一个重要组件。它加速了后端团队的开发工作流程,因为他们现在可以专注于业务逻辑的编码,而不是专注于认证、监控等。

在这篇博文中,我们主要关注API网关的路由方面,但还有许多其他功能,如速率限制、服务发现,我们将在接下来的博客中探讨。