关于后端:云原生Web服务框架ESA-Restlight

ESA Stack(Elastic Service Architecture) 是OPPO云计算中心孵化的技术品牌,致力于微服务相干技术栈,帮忙用户疾速构建高性能,高可用的云原生微服务。产品蕴含高性能Web服务框架、RPC框架、服务治理框架、注册核心、配置核心、调用链追踪零碎,Service Mesh、Serverless等各类产品及钻研方向。

以后局部产品曾经对外开源

开源主站:https://www.esastack.io/

Github: https://github.com/esastack

Restlight我的项目地址:https://github.com/esastack/e…

Restlight文档地址:https://www.esastack.io/esa-r…

欢送各路技术爱好者们退出,一起探讨学习与提高。

本文将不可避免的屡次提到Spring MVC,并没有要与其竞争的意思,Restlight是一个独立Web框架,有着本人的保持。

Java业内传统Web服务框架现状

Spring MVC

说到Web服务框架,在Java畛域Spring MVC堪称是无人不知,无人不晓。在Tomcat(也可能是Jetty,Undertow等别的实现)根底之上实现申请的路由匹配,过滤器,拦截器,序列化,反序列化,参数绑定,返回值解析等能力。因为其丰盛的性能,以及与当今用户量微小的Spring容器及Spring Boot的深度联合,让Spring MVC简直是很多公司Web服务框架的不二抉择。

本文中的Spring MVC泛指Tomcat + Spring MVC的狭义Web服务框架

Resteasy

Resteasy也是Java体系中绝对比拟成熟的Rest框架,JBoss的一个开源我的项目,它残缺的实现了JAX-RS规范,帮忙用户疾速构建Rest服务,同时还提供一个Resteasy JAX-RS客户端框架 ,不便用户进行Rest服务调用。Resteasy在许多三方框架中集成应用的场景较多,如Dubbo,SOFA RPC等出名框架中均有应用。

Spring MVC就是万能的么?

某种意义上来说,还真是万能的。Spring MVC简直具备了传统的一个Web服务应有的绝大多数能力,不论是做一个简略的Rest服务,还是All In One的控制台服务,还是在Spring Cloud中的RPC服务,都能够应用Spring MVC。

可是随着微服务技术的演进和变迁,特地是当今云原生微服务理念的流行,这个全能选手仿佛也呈现了一些水土不服。

性能

性能与性能的折中

Spring MVC设计更多是面向性能的设计,通过查看Spring的源码能够看到各种高水平的设计模式及接口设计,这让Spring MVC成为了一个“全能型选手”。然而简单的设计和性能也是有代价的, 那便是在性能这个点上的折中, 有时候为了性能或者设计不得不放弃一些性能。

Tomcat线程模型

Spring MVC应用单个Worker线程池解决申请

咱们能够应用server.tomcat.threads.max进行线程池大小配置(默认最大为200)。

线程模型中的Worker线程负责从socket读取申请数据,并解析为HttpServletRequest,随后路由到servlet(即经典的DispatcherServlet),最初路由到Controller进行业务调用。

IO读写与业务操作无奈隔离

  • 当业务操作为耗时操作时,将会占用Worker线程资源从而影响到其余的申请的解决,也会影响到IO数据读写的效率
  • 当网络IO读写相干操作耗时也将影响业务的执行效率

线程模型没有好坏之分,只有适宜与不适宜

Restful性能损耗

Restful格调的接口设计是宽广开发者比拟推崇的接口设计,通常接口门路可能会长这样

  • /zoos/{id}
  • /zoos/{id}/animals

然而这样的接口在Spring MVC中的解决形式会带来性能上的损耗,因为其中{id}局部是基于正则表达式来实现的。

拦截器

应用拦截器时能够通过上面的形式去设置匹配逻辑

  • InterceptorRegistration#addPathPatterns("/foo/**", "/fo?/b*r/")
  • InterceptorRegistration#excludePathPatterns("/bar/**", "/foo/bar")

同样的,这个性能也会为每次的申请都带来大量的正则表达式匹配的性能耗费

这里只列出了一些场景,实际上整个Spring MVC的实现代码中还有很多从性能角度来看还有待晋升的中央(当然这只是从性能角度…)

Rest场景的性能过剩

试想一下,当咱们应用Spring Cloud开发微服务的时候,咱们除了应用@RequestMapping, @RequestParam等常见的注解之外,还会应用诸如ModelAndView, JSP, Freemaker等相干性能么?

在微服务这个概念曾经耳熟能详的明天,大多数的微服务曾经不是一个All in One的Web服务,而是多个Rest格调的Web服务了。这使得反对残缺Servlet, JSP等在All in One场景性能的Spring MVC在Rest场景显得有些大材小用了。即使如此,Spring Cloud体系中大家还是毫不犹豫的应用的Spring MVC,因为Spring Cloud就是这么给咱们的。

体积过大

继下面的性能过剩的问题,同样也会引发代码以及依赖体积过大的问题。这在传统微服务场景或者并不是多大的问题,然而当咱们将其打成镜像,则会导致镜像体机较大。同样在FaaS场景这个问题将会被放大,间接影响函数的冷启动。

后续将会探讨FaaS相干的问题

不足规范

这里的规范指的是Rest规范。实际上在Java曾经有了一个通用的规范,即JAX-RS(Java API for RESTful Web Services),JAX-RS一开始就是面试Rest服务所设计的,其中蕴含开发Rest服务常常应用的一些注解,以及一整套Rest服务甚至客户端规范。

注解

JAX-RS中的注解

  • @Path
  • @GET, @POST, @PUT, @DELETE
  • @Produces
  • @Consumes
  • @PathParam
  • @QueryParam
  • @HeaderParam
  • @CookieParam
  • @MatrixParam
  • @FormParam
  • @DefaultValue

Spring MVC中的注解

  • @RequestMapping
  • @RequestParam
  • @RequestHeader
  • @PathVariable
  • @CookieValue
  • @MatrixVariable

实际上JAX-RS注解和Spring MVC中注解从性能上来说并没有太大的差异。

然而JAX-RS的注解相比Spring MVC的注解

  1. 更加简洁:JAX-RS注解格调更加简洁,模式也更加对立,而Spring MVC的注解所有稍显简短。
  2. 更加灵便:JAX-RS的注解并非只能用在Controller上,@Produces, @Consumes更是能够用在序列化反序列化扩大实现等各种中央。@DefaultValue注解也能够和其余注解搭配应用。而@RequestMapping将各种性能都揉在一个注解中,代码显得简短且简单。
  3. 更加通用:JAX-RS注解是规范的Java注解,能够在各种环境中应用,而相似@GetMapping@PostMapping等注解都依赖Spring的@AliasFor注解,只能在Spring环境中应用。

对于习惯了Spring MVC的同学可能无感,然而笔者是亲自实现过Spring MVC注解以及JAX-RS兼容的,整个过程下来更加喜爱JAX-RS的设计。

三方框架亲和性

如果当初你要实现一个RPC框架,筹备去反对HTTP协定的RPC调用,构想着相似Spring Cloud一样用户可能简略标记一些@RequestMapping注解就能实现RPC调用,因而当初你须要一个仅蕴含Spring MVC注解的依赖,而后去实现对应的逻辑。可是遗憾的是,Spring MVC的注解是间接耦合到spring-web依赖中的,如果要依赖,就会将spring-core, spring-beans等依赖一并引入,因而业内的RPC框架的HTTP反对简直都是抉择的JAX-RS(比方SOFA RPC,Dubbo等)。

不够轻量

不得不抵赖Spring的代码都很有设计感,在接口设计上十分的优雅。

然而Spring MVC这样一个Web服务框架却是一个整体,间接的附丽在了Spring这个容器中(或者是策略上的起因?)。因而所有相干能力都须要引入Spring容器,甚至是Spring Boot。可能有人会说:“这不是很失常的嘛,咱们我的项目都会引入Spring Boot啊”。然而

如果我是一名框架开发者,我想在我的框架中启动一个Web服务器去裸露相应的Http接口,然而我的框架非常的简洁,不想引入任何别的依赖(因为会传递给用户),这个时候便无奈应用Spring MVC。

如果我是一名中间件开发者,同样想在我的程序中启动一个Web服务器去裸露相应的Metrics接口,然而不想因为这个性能就引入Spring Boot以及其余相干的一大块货色,这个时候我只能相似原生的嵌入式Tomcat或者Netty本人实现,然而这都有些太简单了(每次都要本人实现一遍)。

ESA Restlight介绍

基于上述一些问题及痛点,ESA Restlight框架便诞生了。

ESA Restlight是基于Netty实现的一个面向云原生的高性能,轻量级的Web开发框架。

以下简称Restlight

Quick Start

创立Spring Boot我的项目并引入依赖

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-starter</artifactId>
    <version>0.1.1</version>
</dependency>

编写Controller

@RestController
@SpringBootApplication
public class RestlightDemoApplication {

    @GetMapping("/hello")
    public String hello() {
        return "Hello Restlight!";
    }

    public static void main(String[] args) {
        SpringApplication.run(RestlightDemoApplication.class, args);
    }
}

运行我的项目并拜访http://localhost:8080/hello

能够看到,在Spring Boot中应用Restlight和应用Spring MVC简直没有什么区别。用法十分的简略

性能体现

测试场景

别离应用Restlight以及spring-boot-starter-web(2.3.2.RELEASE) 编写两个web服务,实现一个简略的Echo接口(间接返回申请的body内容),别离在申请body为16B, 128B, 512B, 1KB, 4KB, 10KB场景进行测试

测试工具

  • wrk4.1.0
  • OS CPU Mem(G)
    server centos:6.9-1.2.5(docker) 4 8
    client centos:7.6-1.3.0(docker) 16 3

JVM参数

-server -Xms3072m -Xmx3072m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:logs/gc-${appName}-%t.log -XX:NumberOfGCLogFiles=20 -XX:GCLogFileSize=480M -XX:+UseGCLogFileRotation -XX:HeapDumpPath=.

参数配置

Framework Options
Restlight restlight.server.io-threads=8<br/>restlight.server.biz-threads.core=16<br/>restlight.server.biz-threads.max=16<br/>restlight.server.biz-threads.blocking-queue-length=512
Spring Web server.tomcat.threads.max=32<br/>server.tomcat.accept-count=128

测试后果(RPS)

16B 128B 512B 1KB 4KB 10KB
Restlight(IO) 129457.26 125344.89 125206.74 116963.24 85749.45 49034.57
Restlight(BIZ) 101385.44 98786.62 97622.33 96504.81 68235.2 46460.79
Spring Web 35648.27 38294.94 37940.3 37497.58 32098.65 22074.94

能够看到Restlight的性能相较于Spring MVC有2-4倍的晋升。

Restlight(IO)以及Restlight(BIZ)为Restlight中特有的线程调度能力,应用不同的线程模型

性能个性

  • HTTP1.1/HTTP2/H2C/HTTPS反对
  • SpringMVC 及 JAX-RS注解反对
  • 线程调度:随便调度Controller在任意线程池中执行
  • 加强的SPI能力:依照分组,标签,程序等多种条件加载及过滤
  • 自我爱护:CPU过载爱护,新建连接数限度
  • Spring Boot Actuator反对
  • 全异步过滤器,拦截器,异样处理器反对
  • Jackson/Fastjson/Gson/Protobuf序列化反对:反对序列化协商及注解随便指定序列化形式
  • 兼容不同运行环境:原生Java,Spring,Spring Boot环境均能反对
  • AccessLog
  • IP白名单
  • 疾速失败
  • Mock测试

ESA Restlight架构设计

设计准则

  • 云原生:疾速启动、省资源、轻量级
  • 高性能:继续不懈谋求的指标 & 外围竞争力,基于高性能网络框架Netty实现
  • 高扩展性:凋谢扩大点,满足业务多样化的需要
  • 低接入老本:兼容SpringMVC 和 JAX-RS罕用注解,升高用户应用老本
  • 全链路异步:基于CompletableFuture提供欠缺的异步解决能力
  • 监控与统计:欠缺的线程池等指标监控和申请链路追踪与统计

分层架构设计

通过分层架构设计让Restlight具备十分高的扩展性,同时针对原生Java, Spring, Spring Boot等场景提供不同实现,适宜Spring Boot业务,三方框架,中间件,FaaS等多种场景。

架构图中ESA HttpServer, Restlight Server, Restlight Core, Restlight for Spring, Restlight Starter几个模块均可作为一个独立的模块应用, 满足不同场景下的需要

ESA HttpServer

基于Netty 实现的一个繁难的HttpServer, 反对Http1.1/Http2以及Https等

该我的项目曾经同步开源到Github:https://github.com/esastack/e…

Restlight Server

ESA HttpServer根底之上封装了

  • 引入业务线程池
  • Filter
  • 申请路由(依据url, method, header等条件将申请路由到对应的Handler)
  • 基于CompletableFuture的响应式编程反对
  • 线程调度

eg.

引入依赖

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-server</artifactId>
    <version>0.1.1</version>
</dependency>

一行代码启动一个Http Server

Restlite.forServer()
        .daemon(false)
        .deployments()
        .addRoute(route(get("/hello"))
                .handle((request, response) ->
                        response.sendResult("Hello Restlight!".getBytes(StandardCharsets.UTF_8))))
        .server()
        .start();

适宜各类框架,中间件等根底组建中启动或冀望应用代码嵌入式启动HttpServer的场景

Restlight Core

Restlight Server之上, 扩大反对了Controller形式(在Controller类中通过诸如@RequestMappng等注解的形式结构申请解决逻辑)实现业务逻辑以及诸多罕用性能

  • HandlerInterceptor: 拦截器
  • ExceptionHandler: 全局异样处理器
  • BeanValidation: 参数校验
  • ArgumentResolver: 参数解析扩大
  • ReturnValueResolver: 返回值解析扩大
  • RequestSerializer: 申请序列化器(通常负责反序列化Body内容)
  • ResposneSerializer: 响应序列化器(通常负责序列化响应对象到Body)
  • 内置Jackson, Fastjson, Gson, ProtoBuf序列化反对

Restlight for Spring MVC

基于Restlight Core的Spring MVC注解反对

eg

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-core</artifactId>
    <version>0.1.1</version>
</dependency>
<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-jaxrs-provider</artifactId>
    <version>0.1.1</version>
</dependency>

编写Controller

@RequestMapping("/hello")
public class HelloController {

    @GetMapping(value = "/restlight")
    public String restlight() {
        return "Hello Restlight!";
    }
}

应用Restlight启动Server

Restlight.forServer()
        .daemon(false)
        .deployments()
        .addController(HelloController.class)
        .server()
        .start();

Restlight for JAX-RS

基于Restlight Core的JAX-RS注解反对

eg.

引入依赖

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-core</artifactId>
    <version>0.1.1</version>
</dependency>
<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-jaxrs-provider</artifactId>
    <version>0.1.1</version>
</dependency>

编写Controller

@Path("/hello")
public class HelloController {

    @Path("/restlight")
    @GET
    @Produces(MediaType.TEXT_PLAIN_VALUE)
    public String restlight() {
        return "Hello Restlight!";
    }
}

应用Restlight启动Server

Restlight.forServer()
        .daemon(false)
        .deployments()
        .addController(HelloController.class)
        .server()
        .start();

Restlight for Spring

在Restlight Core根底上反对在Spring场景下通过ApplicationContext容器主动配置各种内容(RestlightOptions, 从容器中主动配置Filter, Controller等)

实用于Spring场景

Restlight Starter

在Restlight for Spring根底上反对在Spring Boot场景的主动配置

实用于Spring Boot场景

Restlight Actuator

在Restlight Starter根底上反对在Spring Boot Actuator原生各种Endpoints反对以及Restlight独有的Endpoints。

实用于Spring Boot Actuator场景

线程模型

Restlight因为是应用Netty作为底层HttpServer的实现,因而图中沿用了局部EventLoop的概念,线程模型由了AcceptorIO EventLoopGroup(IO线程池)以及Biz ThreadPool(业务线程池)组成。

  • Acceptor: 由1个线程组成的线程池, 负责监听本地端口并散发IO 事件。
  • IO EventLoopGroup: 由多个线程组成,负责读写IO数据(对应图中的read()write())以及HTTP协定的编解码和散发到业务线程池的工作。
  • Biz Scheduler:负责执行真正的业务逻辑(大多为Controller中的业务解决,拦截器等)。
  • Custom Scheduler: 自定义线程池

通过第三个线程池Biz Scheduler的退出实现IO操作与理论业务操作的异步(同时可通过Restlight的线程调度性能随便调度)

灵便的线程调度 & 接口隔离

线程调度容许用户依据须要随便制订Controller在IO线程上执行还是在Biz线程上执行还是在自定义线程上运行。

指定在IO线程上运行
@RequestMapping("/hello")
@Scheduled(Schedulers.IO)
public String list() {
    return "Hello";
}
指定在BIZ线程池执行
@RequestMapping("/hello")
@Scheduled(Schedulers.BIZ)
public String list() {
    // ...
    return "Hello";
}
指定在自定义线程池执行
@RequestMapping("/hello")
@Scheduled("foo")
public String list() {
    // ...
    return "Hello";
}

@Bean
public Scheduler scheduler() {
    // 注入自定义线程池
    return Schedulers.fromExecutor("foo", Executors.newCachedThreadPool());
}

通过随便的线程调度,用户能够均衡线程切换及隔离,达到最优的性能或是隔离的成果

ESA Restlight性能优化的亿些细节

Restlight始终将性能放在第一位,甚至有时候到了对性能偏执的水平。

Netty

Restlight基于Netty编写,Netty自带的一些高性能个性天然是高性能的基石,Netty常见个性均在Restlight有所使用

  • Epoll & NIO
  • ByteBuf
  • PooledByteBufAllocator
  • EventLoopGroup
  • Future & Promise
  • FastThreadLocal
  • InternalThreadLocalMap
  • Recycler

除此之外还做了许多其余的工作

HTTP协定编解码优化

说到Netty中的实现Http协定编解码,最常见的用法便是HttpServerCodec + HttpObjectAggregator的组合了(或是HttpRequestDecoder + HttpResponseEncoder + HttpObjectAggregator的组合)。

以Http1.1为例

其实HttpServerCodec曾经实现了Http协定的编解码,可是HttpObjectAggregator存在的作用又是什么呢?

HttpServerCodec会将Http协定解析为HttpMessage(申请则为HttpRequest, 响应则为HttpResponse), HttpContent, LastHttpContent三个局部,别离代表Http协定中的协定头(蕴含申请行/状态行及Header), body数据块,最初一个body数据块(用于标识申请/相应完结,同时蕴含Trailer数据)。

以申请解析为例,通常咱们须要的是残缺的申请,而不是单个的HttpRequest,亦或是一个一个的body音讯体HttpContent。因而HttpObjectAggregator便是将HttpServerCodec解析出的HttpRequestHttpContentLastHttpContent聚合成一个FullHttpRequest, 不便用户应用。

然而HttpObjectAggregator依然有一些问题

  • maxContentLength问题

    HttpObjectAggregator结构器中须要指定一个maxContentLength参数,用于指定聚合申请body过大时抛出TooLongFrameException。问题在于这个参数是int类型的,因而这使得申请Body的大小不能超过int的最大值2^31 – 1,也就是2G。在大文件,大body, chunk等场景事与愿违。

  • 性能

    通常尽管咱们须要一个整合的FullHttpRequest解析后果,然而实际上当咱们将申请对象向后传递的时候咱们又不能间接将Netty原生的对象给到用户,因而大多须要自行进行一次包装(比方相似HttpServletRequest), 这使得本来HttpServerCodec解析出的后果进行了两次的转换,第一次转换成FullHttpRequest, 第二次转换为用户自定义的对象。其实咱们真正须要的是期待整个Http协定的解码实现后将其后果聚合成咱们本人的对象而已。

  • 大body问题

    聚合也就意味着要等到所有的body都收到了之后能力做后续的操作,然而如果是一个Multipart申请,申请中蕴含了大文件,这时候应用HttpObjectAggregator将会把所有的Body数据都保留在内存(甚至还是间接内存)中,直到这个申请的完结。这简直是不可承受的。

    通常这种场景有两种解决方案:1)将收到的body数据转储到本地磁盘,开释内存资源,等须要应用的时候通过流的形式读取磁盘数据。2)每收到一部分body数据都立马生产掉并开释这段内存。

    这两种形式都要求不能间接聚合申请的Body。

  • 响应式body解决

    对于Http协定来说,尽管通常都是这样的步骤:

    client发送残缺申请-> server接管残缺申请-> server发送残缺响应 -> client接管残缺响应

    然而其实咱们能够更加的灵便,解决申请时每当收到一段body都间接交给业务解决

    client发送残缺申请 -> server接管申请头 -> server解决body1 -> server解决body2 -> server解决body3 -> server发送残缺响应

    咱们甚至做到了client与server同时响应式的发送和解决body

因而咱们自行实现了聚合逻辑Http1Handler以及Http2Handler

  • 响应式body解决

    HttpServer.create()
            .handle(req -> {
                req.onData(buf -> {
                    // 每收到一部分的body数据都将调用此逻辑
                    System.out.println(buf.toString(StandardCharsets.UTF_8));
                });
                req.onEnd(p -> {
                    // 写响应
                    req.response()
                            .setStatus(200)
                            .end("Hello ESA Http Server!".getBytes(StandardCharsets.UTF_8));
                    return p.setSuccess(null);
                });
            })
            .listen(8080)
            .awaitUninterruptibly();
  • 获取整个申请

    HttpServer.create()
            .handle(req -> {
                // 设置冀望聚合所有的body体
                req.aggregate(true);
                req.onEnd(p -> {
                    // 获取聚合后的body
                    System.out.println(req.aggregated().body().toString(StandardCharsets.UTF_8));
                    // 写响应
                    req.response()
                            .setStatus(200)
                            .end("Hello ESA Http Server!".getBytes());
                    return p.setSuccess(null);
                });
            })
            .listen(8080)
            .awaitUninterruptibly();
  • 响应式申请body解决及响应body解决

    HttpServer.create()
            .handle(req -> {
                req.onData(buf -> {
                    // 每收到一部分的body数据都将调用此逻辑
                    System.out.println(buf.toString(StandardCharsets.UTF_8));
                });
                req.onEnd(p -> {
                    req.response().setStatus(200);
                    // 写第一段响应body
                    req.response().write("Hello".getBytes(StandardCharsets.UTF_8));
                    // 写第二段响应body
                    req.response().write(" ESA Http Server!".getBytes(StandardCharsets.UTF_8));
                    // 完结申请
                    req.response().end();
                    return p.setSuccess(null);
                });
            })
            .listen(8080)
            .awaitUninterruptibly();

性能体现

测试场景

别离应用ESA HttpServer以及原生Netty(HttpServerCodec, HttpObjectAggregator) 编写两个web服务,实现一个简略的Echo接口(间接返回申请的body内容),别离在申请body为16B, 128B, 512B, 1KB, 4KB, 10KB场景进行测试

测试工具
  • wrk4.1.0
  • OS CPU Mem(G)
    server centos:6.9-1.2.5(docker) 4 8
    client centos:7.6-1.3.0(docker) 16 3
JVM参数
-server -Xms3072m -Xmx3072m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:logs/gc-${appName}-%t.log -XX:NumberOfGCLogFiles=20 -XX:GCLogFileSize=480M -XX:+UseGCLogFileRotation -XX:HeapDumpPath=.
参数配置

IO线程数设置为8

测试后果(RPS)
16B 128B 512B 1KB 4KB 10KB
Netty 133272.34 132818.53 132390.78 127366.28 85408.7 49798.84
ESA HttpServer 142063.99 139608.23 139646.04 140159.5 92767.53 53534.21

应用ESA HttpServer性能甚至比原生Netty性能更高

路由缓存

传统的Spring MVC中, 当咱们的@RequestMapping注解中蕴含了任何的简单匹配逻辑(这里的简单逻辑能够了解为除了一个url对应一个Controller实现,并且url中没有*, ? . {foo}等模式匹配的内容)时方能在路由阶段有绝对较好的成果,反之如通常状况下一个申请的到来到路由到对应的Controller实现这个过程将会是在以后利用中的所有Controller中遍历匹配,值得注意的是通常在微服务提倡RestFul设计的大环境下一个这种遍历简直是无奈防止的, 同时因为匹配的条件自身的复杂性(比如说正则自身为人诟病的就是性能),因而随同而来的则是SpringMVC的路由的损耗十分的大。

缓存设计

  • 二八准则(80%的业务由20%的接口解决)
  • 算法:类LFU(Least Frequently Used)算法

咱们尽管不能扭转路由条件匹配自身的损耗, 然而咱们心愿能做尽量少的匹配次数来达到优化的成果。因而采纳罕用的”缓存”来作为优化的伎俩。
当开启了路由缓存后,默认状况下将应用类LFU(Least Frequently Used)算法的形式缓存非常之的Controller,依据二八准则(80%的业务由20%的接口解决),大部分的申请都将在缓存中匹配胜利并返回(这里框架默认的缓存十分之一,是绝对比拟激进的设置)

算法逻辑

当每次申请匹配胜利时,会进行命中纪录的加1操作,并统计命中纪录最高的20%(可配)的Controller退出缓存, 每次申请的到来都将先从缓存中查找匹配的Controller(大部分的申请都将在此阶段返回), 失败则进入失常匹配的逻辑。

什么时候更新缓存? 咱们不会在每次申请命中的状况下都去更新缓存,因为这波及到一次排序(或者m次遍历, m为须要缓存的Controller的个数,相当于挑选出命中最高的m个Controller)。 取而代之的是咱们会以概率的形式去从新计算并更新缓存, 依据2-8准则通常状况下咱们以后缓存的内存就是咱们须要的内容, 所以没必要每次有申请命中都去从新计算并更新缓存, 因而咱们会在申请命中的肯定概率条件下采取做此操作(默认0.1%, 称之为计算概率), 减小了并发损耗(这段逻辑自身基于CopyOnWrite, 并且为纯无锁并发编程,自身性能损耗就很低),同时此概率可配置能够依据具体的利用理论状况调整配置达到最优的成果。

成果

应用JMH进行微基准测试, 在加缓存与不加缓存操作之间做性能测试比照

别离测试Controller个数为10, 20, 50, 100个时的性能体现

申请遵从泊松散布, 5轮预热,每次测试10次迭代

@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@Threads(Threads.MAX)
@Fork(1)
@State(Scope.Benchmark)
public class CachedRouteRegistryBenchmark {

    private ReadOnlyRouteRegistry cache;
    private ReadOnlyRouteRegistry noCache;

    @Param({"10", "20", "50", "100"})
    private int routes = 100;

    private AsyncRequest[] requests;
    private double lambda;

    @Setup
    public void setUp() {
        RouteRegistry cache = new CachedRouteRegistry(1);
        RouteRegistry noCache = new SimpleRouteRegistry();
        Mapping[] mappings = new Mapping[routes];
        for (int i = 0; i < routes; i++) {
            HttpMethod method = HttpMethod.values()[ThreadLocalRandom.current().nextInt(HttpMethod.values().length)];
            final MappingImpl mapping = Mapping.mapping("/f?o/b*r/**/??x" + i)
                    .method(method)
                    .hasParam("a" + i)
                    .hasParam("b" + i, "1")
                    .hasHeader("c" + i)
                    .hasHeader("d" + i, "1")
                    .consumes(MediaType.APPLICATION_JSON)
                    .produces(MediaType.TEXT_PLAIN);
            mappings[i] = mapping;
        }

        for (Mapping m : mappings) {
            Route route = Route.route(m);
            cache.registerRoute(route);
            noCache.registerRoute(route);
        }

        requests = new AsyncRequest[routes];
        for (int i = 0; i < requests.length; i++) {
            requests[i] = MockAsyncRequest.aMockRequest()
                    .withMethod(mappings[i].method()[0].name())
                    .withUri("/foo/bar/baz/qux" + i)
                    .withParameter("a" + i, "a")
                    .withParameter("b" + i, "1")
                    .withHeader("c" + i, "c")
                    .withHeader("d" + i, "1")
                    .withHeader(HttpHeaderNames.CONTENT_TYPE.toString(), MediaType.APPLICATION_JSON.value())
                    .withHeader(HttpHeaderNames.ACCEPT.toString(), MediaType.TEXT_PLAIN.value())
                    .build();
        }
        this.cache = cache.toReadOnly();
        this.noCache = noCache.toReadOnly();
        this.lambda = (double) routes / 2;
    }

    @Benchmark
    public Route matchByCachedRouteRegistry() {
        return cache.route(getRequest());
    }

    @Benchmark
    public Route matchByDefaultRouteRegistry() {
        return noCache.route(getRequest());
    }

    private AsyncRequest getRequest() {
        return requests[getPossionVariable(lambda, routes - 1)];
    }

    private static int getPossionVariable(double lambda, int max) {
        int x = 0;
        double y = Math.random(), cdf = getPossionProbability(x, lambda);
        while (cdf < y) {
            x++;
            cdf += getPossionProbability(x, lambda);
        }
        return Math.min(x, max);
    }

    private static double getPossionProbability(int k, double lamda) {
        double c = Math.exp(-lamda), sum = 1;
        for (int i = 1; i <= k; i++) {
            sum *= lamda / i;
        }
        return sum * c;
    }
}

测试后果

Benchmark                                                 (routes)   Mode  Cnt     Score    Error   Units
CachedRouteRegistryBenchmark.matchByCachedRouteRegistry         10  thrpt   10  1353.846 ± 26.633  ops/ms
CachedRouteRegistryBenchmark.matchByCachedRouteRegistry         20  thrpt   10   982.295 ± 26.771  ops/ms
CachedRouteRegistryBenchmark.matchByCachedRouteRegistry         50  thrpt   10   639.418 ± 22.458  ops/ms
CachedRouteRegistryBenchmark.matchByCachedRouteRegistry        100  thrpt   10   411.046 ±  5.647  ops/ms
CachedRouteRegistryBenchmark.matchByDefaultRouteRegistry        10  thrpt   10   941.917 ± 33.079  ops/ms
CachedRouteRegistryBenchmark.matchByDefaultRouteRegistry        20  thrpt   10   524.540 ± 18.628  ops/ms
CachedRouteRegistryBenchmark.matchByDefaultRouteRegistry        50  thrpt   10   224.370 ±  9.683  ops/ms
CachedRouteRegistryBenchmark.matchByDefaultRouteRegistry       100  thrpt   10   113.883 ±  5.847  ops/ms

能够看出加了缓存之后性能晋升显著,同时能够看出随着Controller个数增多, 没有缓存的场景性能损失十分重大。

拦截器设计

Spring MVC拦截器性能问题

先前提到Spring MVC中的拦截器因为正则表达式的问题会导致性能问题,Restlight在优化了正则匹配性能的同时引入了不同类型的拦截器

试想一下,在SpringMVC中,是否会有以下场景

场景1

想要拦挡一个Controller,它的Path为/foo, 此时会应用addPathPatterns("/foo")来拦挡

这样的场景比较简单,Spring MVC只须要进行间接的Uri匹配即可,性能耗费不大

场景2

想要拦挡某个Controller Class中的所有Controller,它们具备独特的前缀, 此时可能会应用addPathPatterns("/foo/**")拦挡

这时候就须要对所有申请进行一次正则匹配,性能损耗较大

场景3

想要拦挡多个不同前缀的Controller, 同时排除其中几个,此时可能须要addPathPatterns("/foo/**", "/bar/***")以及excludePathPatterns("/foo/b*", "/bar/q?x")配合应用

此时须要对所有申请进行屡次正则匹配,性能损耗依据正则复杂度不同,影响均比拟大

Restlight中的拦截器设计

拦截器设计的基本目标是让用户可能得心应手的拦挡指标Controller

RouteInterceptor

只绑定到固定Controller/Route的拦截器。这种拦截器容许用户在利用初始化阶段自行决定拦挡哪些Controller,运行时阶段不进行任何匹配的操作,间接绑定到这个Controller上。

同时间接将Controller元数据信息作为参数,用户无需局限于url门路匹配,用户能够依据注解,HttpMethod,Uri,办法签名等等各种信息进行匹配。

在Restlight中一个Controller接口被形象为一个Route

eg.

实现一个拦截器, 拦挡所有GET申请(仅蕴含GET)

@Bean
public RouteInterceptor interceptor() {
    return new RouteInterceptor() {

        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(null);
        }

        @Override
        public boolean match(DeployContext<? extends RestlightOptions> ctx, Route route) {
            HttpMethod[] method = route.mapping().method();
            return method.length == 1 && method[0] == HttpMethod.GET;
        }
    };
}
MappingInterceptor

绑定到所有Controller/Route, 并匹配申请的拦截器。

用户能够依据申请任意的匹配,不必局限于Uri,性能也更高。

eg.

实现一个拦截器, 拦挡所有Header中蕴含X-Foo申请头的申请

@Bean
public MappingInterceptor interceptor() {
    return new MappingInterceptor() {

        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(null);
        }
        
        @Override
        public boolean test(AsyncRequest request) {
            return request.containsHeader("X-Foo");
        }
    };
}

正则相交性优化

下面的拦截器设计是从设计阶段解决正则表达式的性能问题,然而如果用户就是心愿相似Spring MVC拦截器一样的应用形式呢。

因而咱们须要直面拦截器Uri匹配的性能问题

HandlerInterceptor

兼容Spring MVC应用形式的拦截器

  • includes(): 指定拦截器作用范畴的Path, 默认作用于所有申请。
  • excludes(): 指定拦截器排除的Path(优先级高于includes)默认为空。

eg.

实现一个拦截器, 拦挡除/foo/bar意外所有/foo/结尾的申请

@Bean
public HandlerInterceptor interceptor() {
    return new HandlerInterceptor() {

        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(null);
        }

        @Override
        public String[] includes() {
            return new String[] {"/foo/**"};
        }

        @Override
        public String[] excludes() {
            return new String[] {"/foo/bar"};
        }
    };
}

这种拦截器从性能上与Spring MVC其实没有太大的区别,都是通过Uri匹配

正则相交性判断

试想一下,当初写了一个uri为/foo/bar的Controller

  • includes("/foo/**")

对于这个Controller来说,其实这个拦截器100%会匹配到这个拦截器,因为/foo/**这个正则是蕴含了/foo/bar

同样

  • includes("/foo/b?r")
  • includes("/foo/b*")
  • includes("/f?o/b*r")

这一系列匹配规定都是肯定会匹配上的

反之

  • excludes("/foo/**")

则肯定不会匹配上

优化逻辑

  • 拦截器的includes()excludes()规定肯定会匹配到Controller时,则在初始化阶段便间接和Controller绑定,运行时不进行任何匹配操作
  • 拦截器的includes()excludes()规定肯定不会匹配到Controller时,则在初始化阶段便间接疏忽,运行时不进行任何匹配操作
  • 拦截器的includes()excludes()可能会匹配到Controller时,运行时进行匹配

咱们在程序启动阶段去判断拦截器规定与Controller之间的相交性,将匹配的逻辑放到了启动阶段一次性实现,大大晋升了每次申请的性能。

实际上当可能会匹配到Controller时Restlight还会进一步进行优化,这里篇幅无限就不过多赘述…

Restful设计不再放心性能

先前提到在Spring MVC中应用相似/zoos/{id} 模式的Restful格调设计会因为正则带来性能损耗,这个问题在Restlight中将不复存在。

参考PR

Restlight as FaaS Runtime

Faas

FaaS(Functions as a Service), 这是当初云原生的热点词汇,属于Serverless领域。

Serverless的外围

  • 按使用量付费
  • 按需获取
  • 疾速弹性伸缩
  • 事件驱动
  • 状态非本地长久化
  • 资源保护托管

其中对于FaaS场景来说疾速弹性伸缩便是一个辣手的问题。

其中最突出的问题便是冷启动问题,Pod缩容到0之后,新的申请进来时须要尽快的去调度一个新的Pod提供服务。

这个问题在Knative中尤为突出,因为采纳KPA进行扩缩容的调度,冷启动工夫较长,这里暂不探讨。

Fission

Fission是面向FaaS的另一个解决方案。FaaS场景对冷启动工夫十分敏感,Fission则采纳热Pod池技术来解决冷启动的问题。

通过事后启动一组热Pod池,提前将镜像,JVM,Web容器等用户逻辑以下的资源事后启动,扩容时热加载Function代码并提供服务的形式,将冷启动工夫缩短到100ms以内(Knative可能须要10s甚至时30s的工夫)。

只是以Fission为例,Fission计划还不算成熟,咱们外部对其进行深度的批改和加强

框架面临的挑战

在FaaS中最常见的一个场景便是HttpTrigger, 即用户编写一个Http接口(或者说Controller),而后将此段代码依靠于某个Web容器中运行。

有了热Pod池技术之后,冷启动工夫更多则是在特化的过程(加载Function代码,在曾经运行着的Pod中裸露Http服务)。

冷启动

  • 启动速度自身足够的快
  • 利用体积足够小(节俭镜像拉取的工夫)
  • 资源占用少(更少的CPU,内存占用)

规范

用户编写Function时无需关注也不应该去关注理论FaaS底层的Http服务是应用的Spring MVC还是Restlight或是其余的组件,因而不应该要求用户用Spring MVC的形式去编写Http接口, 这时便须要定义一套规范,屏蔽上层根底设置细节,让用户在没有任何其余依赖的状况下进行Function编写。

JAX-RS便是比拟好的抉择(当然也不是惟一的抉择)

监控指标

FaaS要求疾速扩缩容,判断服务是否须要扩缩容的根据最间接的就是Metrics, 因而须要框架外部裸露更加明确的指标,让FaaS进行疾速的扩缩容响应。比方:线程池应用状况,排队,线程池回绝等各类指标。

Restlight

很显著Spring MVC无奈满足这个场景,因为它是面向长时间运行的服务而设计, 同时依赖Spring Boot等泛滥组件,体机大,启动速度同样无奈满足冷启动的要求。

Restlight则可能十分好的符合FaaS的场景。

  • 启动快
  • 小体积:不依赖任何三方依赖
  • 丰盛的指标:IO线程,Biz线程池指标
  • 无环境依赖:纯原生Java便可启动
  • 反对JAX-RS
  • 高性能:单Pod能够承载更多的并发申请,节省成本

当初在我司外部曾经应用Restlight作为FaaS Java Runtime底座构建FaaS能力。

Restlight将来布局

JAX-RS残缺反对

现阶段Restlight只是对JAX-RS注解进行了反对,后续将会对整个JAX-RS标准进行反对。

这是很有意义的,JAX-RS是专门为Rest服务设计的规范,这与一开始Restlight的出发点是统一的。

同时就在去年JAX-RS曾经公布了JAX-RS 3.0, 而当初行业外部还鲜有框架对其进行了反对,Restlight将会率先去对其进行反对。

FaaS Runtime深刻反对

作为FaaS Runtimme底座,Restlight须要更多更底层的能力。

Function目前是独占Pod模式,对于低频拜访的function,保留Pod实例节约,缩减到0又会频繁冷启动。目前只有尽可能放大Pod的规格,调大Pod的闲暇工夫。

现实状态下,咱们心愿Pod同时能反对多个Function的运行,这样能节约更多的老本。然而这对Function隔离要求更高

因而Restlight未来会反对

  • 动静Route:运行时动静批改Web容器中的Route,满足运行时特化需要
  • 协程反对:以更加轻量的形式运行Function,缩小资源间的争抢
  • Route隔离: 满足不同Function之间的隔离要求,防止一个Function影响其余Function
  • 资源计费:不同Function别离应用了多少资源
  • 更加精细化的Metrics:更准确,及时的指标,满足疾速扩缩容需要。

Native Image反对

云原生同样对传统微服务也提出了更多要求,要求服务也须要体积小,启动快。

因而Restlight同样会思考反对Native Image,间接编译为二进制文件,从而晋升启动速度,缩小资源占用。

实测Graal VM后成果不是那么现实,且应用上不太敌对。

结语

Restlight专一于云原生Rest服务开发。

对云原生方向坚韧不拔

对性能有着极致的谋求

对代码有洁癖

它还是一个年老的我的项目,欢送各路技术爱好者们退出,一起探讨学习与提高。

作者简介

Norman OPPO高级后端工程师

专一云原生微服务畛域,云原生框架,ServiceMesh,Serverless等技术。

获取更多精彩内容,欢送关注[OPPO互联网技术]公众号

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理