Java API网关异步化演进流程

605 阅读16分钟

背景

阻塞是对资源的浪费

现代应用需要应对大量的并发用户,而且即使现代硬件的处理能力飞速发展,软件性能仍然是关键因素。

广义来说我们有两种思路来提升程序性能:

  1. 并行化(parallelize) :使用更多的线程和硬件资源。
  2. 基于现有的资源来 提高执行效率

通常,Java开发者使用阻塞式(blocking)编写代码。这没有问题,在出现性能瓶颈后, 我们可以增加处理线程,线程中同样是阻塞的代码。但是这种使用资源的方式会迅速面临 资源竞争和并发问题。

更糟糕的是,阻塞会浪费资源。具体来说,比如当一个程序面临延迟(通常是I/O方面, 比如数据库读写请求或网络调用),所在线程需要进入 idle 状态等待数据,从而浪费资源。

所以,并行化方式并非银弹。这是挖掘硬件潜力的方式,但是却带来了复杂性,而且容易造成浪费。

举例说明:

在传统的 MVC 应用程序中,当请求到达服务器时,将创建一个 servlet 线程。 它将请求委托给工作线程进行 I/O 操作(例如数据库访问等)。在工作线程忙时,servlet 线程(请求线程)保持等待状态,因此被阻塞。 也称为同步请求处理

image.png

由于服务器可以有一定数量的请求线程,因此限制了服务器在最大服务器负载下处理该数量请求的能力。 它可能会影响性能并限制服务器功能的完全利用。

异步可以解决问题吗?

第二种思路——提高执行效率——可以解决资源浪费问题。通过编写 异步非阻塞 的代码, (任务发起异步调用后)执行过程会切换到另一个 使用同样底层资源 的活跃任务,然后等 异步调用返回结果再去处理。

但是在 JVM 上如何编写异步代码呢?Java 提供了两种异步编程方式:

  • 回调(Callbacks) :异步方法没有返回值,而是采用一个 callback 作为参数(lambda 或匿名类),当结果出来后回调这个 callback。常见的例子比如 Swings 的 EventListener
  • Futures :异步方法 立即 返回一个 Future<T>,该异步方法要返回结果的是 T 类型,通过 Future`封装。这个结果并不是 *立刻* 可以拿到,而是等实际处理结束才可用。比如, `ExecutorService 执行 Callable<T> 任务时会返回 Future 对象。

这些技术够用吗?并非对于每个用例都是如此,两种方式都有局限性。

回调(Callbacks)

回调很难组合起来,因为很快就会导致代码难以理解和维护(即所谓的“回调地狱(callback hell)”)。

Futures

Futures 比回调要好一点,但即使在 Java 8 引入了 CompletableFuture,它对于多个处理的组合仍不够好用。 编排多个 Futures 是可行的,但却不易。此外,Future 还有一个问题:当对 Future 对象最终调用 get() 方法时,仍然会导致阻塞,并且缺乏对多个值以及更进一步对错误的处理。

回调或 Future 遇到的窘境是类似的,这也是响应式编程要通过 Publisher-Suscriber 方式来解决的。

从命令式编程到响应式编程

响应式库的目标就是要弥补上述“经典”的 JVM 异步方式所带来的不足, 此外还会关注一下几个方面:

  • 可编排性(Composability) 以及 可读性(Readability)
  • 使用丰富的 操作符 来处理形如 的数据
  • 订阅(subscribe) 之前什么都不会发生
  • 背压(backpressure) 具体来说即 消费者能够反向告知生产者生产内容的速度的能力

高层次 (同时也是有高价值的)的抽象,从而达到 并发无关 的效果

流行的响应式库:RxJava 、Reactor、Vert.x、Akka....

Reactor

核心特性

Reactor 项目的主要 artifact 是 reactor-core,这是一个基于 Java 8 的实现了响应式流规范 (Reactive Streams specification)的响应式库。

Reactor 引入了实现 Publisher 的响应式类 FluxMono,以及丰富的操作方式。 一个 Flux 对象代表一个包含 0..N 个元素的响应式序列,而一个 Mono 对象代表一个包含 零/一个(0..1)元素的结果。

这种区别为这俩类型带来了语义上的信息——表明了异步处理逻辑所面对的元素基数。比如, 一个 HTTP 请求产生一个响应,所以对其进行 count 操作是没有多大意义的。表示这样一个 结果的话,应该用 Mono<HttpResponse> 而不是 Flux<HttpResponse>,因为要置于其上的 操作通常只用于处理 0/1 个元素。

有些操作可以改变基数,从而需要切换类型。比如,count 操作用于 Flux,但是操作 返回的结果是 Mono<Long>

Flux, 包含 0-N 个元素的异步序列

image.png

Flux 是一个能够发出 0 到 N 个元素的标准的 Publisher,它会被一个“错误(error)” 或“完成(completion)”信号终止。因此,一个 flux 的可能结果是一个 value、completion 或 error。 就像在响应式流规范中规定的那样,这三种类型的信号被翻译为面向下游的 onNext,onCompleteonError方法。

由于多种不同的信号可能性,Flux 可以作为一种通用的响应式类型。注意,所有的信号事件, 包括代表终止的信号事件都是可选的:如果没有 onNext 事件但是有一个 onComplete 事件, 那么发出的就是 空的 有限序列,但是去掉 onComplete 那么得到的就是一个 无限的 空序列。 当然,无限序列也可以不是空序列,比如,Flux.interval(Duration) 生成的是一个 Flux<Long>, 这就是一个无限地周期性发出规律 tick 的时钟序列。

Mono, 异步的 0-1 结果

image.png

Mono<T> 是一种特殊的 Publisher<T>, 它最多发出一个元素,然后终止于一个 onComplete 信号或一个 onError 信号。

它只适用其中一部分可用于 Flux 的操作。比如,(两个 Mono 的)结合类操作可以忽略其中之一 而发出另一个 Mono,也可以将两个都发出,对于后一种情况会切换为一个 Flux

例如,Mono#concatWith(Publisher) 返回一个 Flux,而 Mono#then(Mono) 返回另一个 Mono

注意,Mono 可以用于表示“空”的只有完成概念的异步处理(比如 Runnable)。这种用 Mono<Void> 来创建。

并发模型

调用阻塞API

如果您确实需要使用阻塞库,该怎么办?Reactor和RxJava都提供了publishOn操作符,以便在不同的线程上继续处理。这意味着有一个容易逃生的舱口。但是,请记住,阻塞API并不适合这种并发模型。

可变状态

在Reactor和RxJava中,您可以通过运算符声明逻辑。在运行时,会形成一个反应式管道,在其中按不同阶段顺序处理数据。这样做的一个关键好处是,它使应用程序不必保护可变状态,因为该管道中的应用程序代码永远不会被并发调用。

Reactor-Netty

Reactor Netty适用于微服务体系结构,为HTTP(包括Websockets)、TCP和UDP提供背压就绪的网络引擎。

核心组件

TCP Server

Reactor Netty提供了一个易于使用和配置的TcpServer。它隐藏了创建TCP服务器所需的大部分Netty功能,并添加了Reactive Streams背压。

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.tcp.TcpServer;
public class Application {
	public static void main(String[] args) {
		DisposableServer server =
				TcpServer.create()
				         .handle((inbound, outbound) -> outbound.sendString(Mono.just("hello"))) 
				         .bindNow();
		server.onDispose()
		      .block();
	}
}

TCP Client

Reactor Netty提供了易于使用且易于配置的TcpClient。它隐藏了创建TCP客户端所需的大部分Netty功能,并添加了Reactive Streams背压。

import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;
public class Application {
	public static void main(String[] args) {
		Connection connection =
				TcpClient.create()
				         .host("example.com")
				         .port(80)
				         .handle((inbound, outbound) -> outbound.sendString(Mono.just("hello"))) 
				         .connectNow();
		connection.onDispose()
		          .block();
	}
}

HTTP Server

Reactor Netty提供了易于使用和配置的HttpServer类。它隐藏了创建HTTP服务器所需的大部分Netty功能,并添加了Reactive Streams背压。

import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
public class Application {
	public static void main(String[] args) {
		DisposableServer server =
				HttpServer.create()
				          .route(routes ->
				              routes.get("/hello",        
				                        (request, response) -> response.sendString(Mono.just("Hello World!")))
				                    .post("/echo",        
				                        (request, response) -> response.send(request.receive().retain()))
				                    .get("/path/{param}", 
				                        (request, response) -> response.sendString(Mono.just(request.param("param"))))
				                    .ws("/ws",            
				                        (wsInbound, wsOutbound) -> wsOutbound.send(wsInbound.receive().retain())))
				          .bindNow();
		server.onDispose()
		      .block();
	}
}

HTTP Client

Reactor Netty提供了易于使用且易于配置的HttpClient。它隐藏了创建HTTP客户端所需的大部分Netty功能,并添加了Reactive Streams背压。

import reactor.core.publisher.Mono;
import reactor.netty.ByteBufFlux;
import reactor.netty.http.client.HttpClient;
public class Application {
	public static void main(String[] args) {
		HttpClient client = HttpClient.create();
		client.post()
		      .uri("https://example.com/")
		      .send(ByteBufFlux.fromString(Mono.just("hello"))) 
		      .response()
		      .block();
	}
}

Spring WebFlux

Spring框架中包含的原始web框架SpringWebMVC是专门为Servlet API和Servlet容器构建的。反应式堆栈web框架SpringWebFlux在5.0版的后期添加。它完全无阻塞,支持Reactive Streams背压,并在Netty、Undertow和Servlet容器等服务器上运行。

这两个web框架都反映了它们的源模块(spring-webmvc和spring-webflux)的名称,并在spring框架中共存。每个模块都是可选的。应用程序可以使用一个或另一个模块,在某些情况下,可以同时使用这两个模块 — 例如具有反应式WebClient的Spring MVC控制器。

image.png

性能

反应式和非阻塞式通常不会使应用程序运行得更快。在某些情况下,它们可以(例如,如果使用WebClient并行运行远程调用)。总的来说,以非阻塞的方式做事需要更多的工作,这可能会略微增加所需的处理时间。

反应式和非阻塞的主要预期好处是能够使用少量、固定数量的线程和较少的内存进行扩展。这使得应用程序在负载下更具弹性,因为它们以更可预测的方式进行扩展。然而,为了观察这些好处,您需要有一些延迟(包括缓慢和不可预测的网络I/O的混合)。这就是反应性堆栈开始显示其优势的地方,并且差异可能是巨大的。

核心组件

HttpHandler

HttpHandler是一个简单的契约,具有一个处理请求和响应的方法。它是有意最小化的,其主要也是唯一的目的是成为不同HTTP服务器API上的最小抽象。

下表介绍了支持的服务器API:

Server nameServer API usedReactive Streams support
NettyNetty APIReactor Netty
UndertowUndertow APIspring-web: Undertow to Reactive Streams bridge
TomcatServlet non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[]spring-web: Servlet non-blocking I/O to Reactive Streams bridge
JettyServlet non-blocking I/O; Jetty API to write ByteBuffers vs byte[]spring-web: Servlet non-blocking I/O to Reactive Streams bridge
Servlet containerServlet non-blocking I/Ospring-web: Servlet non-blocking I/O to Reactive Streams bridge

Reactor Netty 适配:

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bindNow();

Undertow适配:

HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

WebHandler API

org.springframework.web.server包建立在HttpHandler合约的基础上,提供一个通用的web API,用于通过多个WebExceptionHandler、多个WebFilter和单个WebHandler组件的链来处理请求。只需指向自动检测组件的SpringApplicationContext,或向生成器注册组件,就可以将该链与WebHttpHandlerBuilder结合在一起。

虽然HttpHandler有一个简单的目标来抽象不同HTTP服务器的使用,但WebHandler API旨在提供更广泛的一组web应用程序中常用的功能,例如:

  • User session with attributes.
  • Request attributes.
  • 已解析请求的区域设置或主体。(本地化)
  • 访问已解析和缓存的表单数据(form data)。
  • 多部分数据(multipart data)的抽象。
  • 等等......
Bean nameBean typeCountDescription
WebExceptionHandler0..NProvide handling for exceptions from the chain of WebFilter instances and the target WebHandler. For more details, see Exceptions.
WebFilter0..NApply interception style logic to before and after the rest of the filter chain and the target WebHandler. For more details, see Filters.
webHandlerWebHandler1The handler for the request.
webSessionManagerWebSessionManager0..1The manager for WebSession instances exposed through a method on ServerWebExchange. DefaultWebSessionManager by default.
serverCodecConfigurerServerCodecConfigurer0..1For access to HttpMessageReader instances for parsing form data and multipart data that is then exposed through methods on ServerWebExchange. ServerCodecConfigurer.create() by default.
localeContextResolverLocaleContextResolver0..1The resolver for LocaleContext exposed through a method on ServerWebExchange. AcceptHeaderLocaleContextResolver by default.
forwardedHeaderTransformerForwardedHeaderTransformer0..1For processing forwarded type headers, either by extracting and removing them or by removing them only. Not used by default.

编程模型

Annotated Controllers

与Spring MVC一致,并基于来自Spring web模块的相同注释。Spring MVC和WebFlux控制器都支持反应式(Reactor和RxJava)返回类型,因此,很难将它们区分开来。一个显著的区别是WebFlux还支持反应式@RequestBody参数。

@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<User> create(@RequestBody User user){
        return userService.createUser(user);
    }
    @GetMapping
    public Flux<User> getAllUsers(){
        return userService.getAllUsers();
    }
    @GetMapping("/{userId}")
    public Mono<ResponseEntity<User>> getUserById(@PathVariable String userId){
        Mono<User> user = userService.findById(userId);
        return user.map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }
    @PutMapping("/{userId}")
    public Mono<ResponseEntity<User>> updateUserById(@PathVariable String userId, @RequestBody User user){
        return userService.updateUser(userId,user)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.badRequest().build());
    }
    @DeleteMapping("/{userId}")
    public Mono<ResponseEntity<Void>> deleteUserById(@PathVariable String userId){
        return userService.deleteUser(userId)
                .map( r -> ResponseEntity.ok().<Void>build())
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }
    @GetMapping("/search")
    public Flux<User> searchUsers(@RequestParam("name") String name) {
        return userService.fetchUsers(name);
    }
}
实现原理

DispatcherHandler

这是一个WebHandler实现,他还要借助其他的bean实现类似SpringMVC路由处理的功能。

Bean typeExplanation
HandlerMappingMap a request to a handler. The mapping is based on some criteria, the details of which vary by HandlerMapping implementation — annotated controllers, simple URL pattern mappings, and others.The main HandlerMapping implementations are RequestMappingHandlerMapping for @RequestMapping annotated methods, RouterFunctionMapping for functional endpoint routes, and SimpleUrlHandlerMapping for explicit registrations of URI path patterns and WebHandler instances.
HandlerAdapterHelp the DispatcherHandler to invoke a handler mapped to a request regardless of how the handler is actually invoked. For example, invoking an annotated controller requires resolving annotations. The main purpose of a HandlerAdapter is to shield the DispatcherHandler from such details.
HandlerResultHandlerProcess the result from the handler invocation and finalize the response. See Result Handling.

Functional Endpoints

基于Lambda的轻量级功能编程模型。您可以将其视为应用程序可以用来路由和处理请求的一个小型库或一组实用程序。与带注释的控制器的最大区别在于,应用程序负责从开始到结束的请求处理,而不是通过注释声明意图并被调用。

@Component
@RequiredArgsConstructor
public class UserHandler {
    private final UserService userService;
    public Mono<ServerResponse> getAllUsers(ServerRequest request) {
        return ServerResponse
                .ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(userService.getAllUsers(), User.class);
    }
    public Mono<ServerResponse> getUserById(ServerRequest request) {
        return userService
                .findById(request.pathVariable("userId"))
                .flatMap(user -> ServerResponse
                        .ok()
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(user, User.class)
                )
                .switchIfEmpty(ServerResponse.notFound().build());
    }
    public Mono<ServerResponse> create(ServerRequest request) {
        Mono<User> user = request.bodyToMono(User.class);
        return user
                .flatMap(u -> ServerResponse
                        .status(HttpStatus.CREATED)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(userService.createUser(u), User.class)
                );
    }
    public Mono<ServerResponse> updateUserById(ServerRequest request) {
        String id = request.pathVariable("userId");
        Mono<User> updatedUser = request.bodyToMono(User.class);
        return updatedUser
                .flatMap(u -> ServerResponse
                        .ok()
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(userService.updateUser(id, u), User.class)
                );
    }
    public Mono<ServerResponse> deleteUserById(ServerRequest request){
        return userService.deleteUser(request.pathVariable("userId"))
                .flatMap(u -> ServerResponse.ok().body(u, User.class))
                .switchIfEmpty(ServerResponse.notFound().build());
    }
}
@Configuration
public class RouterConfig {
    @Bean
    RouterFunction<ServerResponse> routes(UserHandler handler) {
        return route(GET("/handler/users").and(accept(MediaType.APPLICATION_JSON)), handler::getAllUsers)
                .andRoute(GET("/handler/users/{userId}").and(contentType(MediaType.APPLICATION_JSON)), handler::getUserById)
                .andRoute(POST("/handler/users").and(accept(MediaType.APPLICATION_JSON)), handler::create)
                .andRoute(PUT("/handler/users/{userId}").and(contentType(MediaType.APPLICATION_JSON)), handler::updateUserById)
                .andRoute(DELETE("/handler/users/{userId}").and(accept(MediaType.APPLICATION_JSON)), handler::deleteUserById);
    }
}
实现原理

DispatcherHandler --> RouterFunctionMapping --> RouterFunction --> RouterFunction

Spring Cloud Gateway

Spring Cloud Gateway 旨在提供一种简单而有效的途径来发送 API,并为它们提供横切关注点,例如:安全性,监控/指标和弹性。

Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。

Spring Cloud Gateway 核心概念

Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念,如下表。

核心概念描述
Route(路由)网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(Filter)组成。
Predicate(断言)路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。
Filter(过滤器)过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。

注意:其中 Route 和 Predicate 必须同时声明。

image.png

常见的 Predicate 断言如下表(假设转发的 URI 为 http://localhost:8001)。

断言示例说明
Path- Path=/dept/list/**当请求路径与 /dept/list/** 匹配时,该请求才能被转发到 http://localhost:8001 上。
Before- Before=2021-10-20T11:47:34.255+08:00[Asia/Shanghai]在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之前的请求,才会被转发到 http://localhost:8001 上。
After- After=2021-10-20T11:47:34.255+08:00[Asia/Shanghai]在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之后的请求,才会被转发到 http://localhost:8001 上。
Between- Between=2021-10-20T15:18:33.226+08:00[Asia/Shanghai],2021-10-20T15:23:33.226+08:00[Asia/Shanghai]在 2021 年 10 月 20 日 15 时 18 分 33.226 秒 到 2021 年 10 月 20 日 15 时 23 分 33.226 秒之间的请求,才会被转发到 http://localhost:8001 服务器上。
Cookie- Cookie=name,c.biancheng.net携带 Cookie 且 Cookie 的内容为 name=c.biancheng.net 的请求,才会被转发到 http://localhost:8001 上。
Header- Header=X-Request-Id,\d+请求头上携带属性 X-Request-Id 且属性值为整数的请求,才会被转发到 http://localhost:8001 上。
Method- Method=GET只有 GET 请求才会被转发到 http://localhost:8001 上。

Spring Cloud Gateway 内置了多达 31 种 GatewayFilter,下表中列举了几种常用的网关过滤器及其使用示例。

路由过滤器描述参数使用示例
AddRequestHeader拦截传入的请求,并在请求上添加一个指定的请求头参数。name:需要添加的请求头参数的 key;value:需要添加的请求头参数的 value。- AddRequestHeader=my-request-header,1024
AddRequestParameter拦截传入的请求,并在请求上添加一个指定的请求参数。name:需要添加的请求参数的 key;value:需要添加的请求参数的 value。- AddRequestParameter=my-request-param,c.biancheng.net
AddResponseHeader拦截响应,并在响应上添加一个指定的响应头参数。name:需要添加的响应头的 key;value:需要添加的响应头的 value。- AddResponseHeader=my-response-header,c.biancheng.net
PrefixPath拦截传入的请求,并在请求路径增加一个指定的前缀。prefix:需要增加的路径前缀。- PrefixPath=/consumer
PreserveHostHeader转发请求时,保持客户端的 Host 信息不变,然后将它传递到提供具体服务的微服务中。- PreserveHostHeader
RemoveRequestHeader移除请求头中指定的参数。name:需要移除的请求头的 key。- RemoveRequestHeader=my-request-header
RemoveResponseHeader移除响应头中指定的参数。name:需要移除的响应头。- RemoveResponseHeader=my-response-header
RemoveRequestParameter移除指定的请求参数。name:需要移除的请求参数。- RemoveRequestParameter=my-request-param
RequestSize配置请求体的大小,当请求体过大时,将会返回 413 Payload Too Large。maxSize:请求体的大小。- name: RequestSizeargs:maxSize: 5000000

GlobalFilter 全局过滤器

GlobalFilter 是一种作用于所有的路由上的全局过滤器,通过它,我们可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter 组合成一个过滤器链。

Spring Cloud Gateway 为我们提供了多种默认的 GlobalFilter,例如与转发、路由、负载均衡等相关的全局过滤器。

WebClientHttpRoutingFilter NettyRoutingFilter

实现原理

DispatcherHandler --> RoutePredicateHandlerMapping --> FilteringWebHandler --> GatewayFilters 【GatewayFilterChain】

使用示例

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .build();
}

总结

image.png

思考

网关异步化引入的问题?

  1. 学习曲线陡峭
  2. 调试难度增加

网关异步化如何收益预估?

  1. 使用更少的机器支持更高的吞吐量

参考文档