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

24次阅读

共计 22371 个字符,预计需要花费 56 分钟才能阅读完成。

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 互联网技术] 公众号

正文完
 0