Vert.x/Jetty/Tomcat/Servle3.x-ASync 性能对比

目的

首先声明:本文的测试方式和数据不能当做标准参考!不能当做标准参考!不能参考!

我发这篇文章的目的只是想亲自尝试一下,那个很基础又很重要的概念,即:Web 应用主要性能瓶颈之 IO 。
那么测试并发跟 IO 有什么关系呢?答:IO 瓶颈会造成响应延迟,但是高并发下 IO 瓶颈会进一步导致系统资源的大量消耗,主要为过多的 IO 阻塞导致的系统线程浪费,Servlet 容器工作线程用尽,请求被排队处理。
如果在同步多线程的 Server 应用上,这个现象非常普遍。所以我其实真正想体验的是同步和异步应用之间的并发差距是否已经大到:在异步架构面前,框架之间封装导致的性能差异已经不值一提。

背景

主要因为前几天我在某处技术讨论时抛出了这个看法,觉得仍然还在纠结 Spring MVC 或是更轻量级的 Web framework 已经意义不大了。它不仅不能带给你明显的性能提升,并且可能会严重干扰你的开发体验和效率以及项目健壮性,这种考虑已经过时了。

因为如果真的不满足于 Web 层面的性能,就应该考虑更适合的「异步」架构。例如 Servlet3 的 asyncSupported 或者干脆抛弃 Servlet 用 Netty 处理 HTTP,例如 Vert.x 。而不应该去纠结 XX 框架/容器 更轻量级带来的那点微小的性能提升。因为这点点的提升,可能多出一个应用进程带来的效果也能平衡,意义非常之小。

测试硬件

  • 单机,组装电脑。
  • CPU: i7 6700k
  • 内存: 16GB

测试软件

  • OS: Linux(Ubuntu-Budgie 16.04)
  • LoadTest: Artillery
  • Java:

    java version "1.8.0_131"
              Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
              Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
              
  • Tomcat: 8.5.14

  • Jetty: 9.4.4.v20170414

模拟环境

  • 仅存在一个 ‘/’ 主页 Mapping
  • 针对每一个请求,执行: Thread.sleep(150) 后响应。(即延时 0.15 秒)
  • 持续 60 秒,每秒的模拟会话数量和请求数量一致。

有效数据

  • 分三次测试,去掉最高和最低,贴出居中数据
  • 如果三次中某次数据差异过大,作废处理
  • RPS(每秒平均请求数) 作主要性能对比依据,其余数据作次要对比。

Servlet 同步容器测试

Servlet 依赖:

compile group: 'javax.servlet', name: 'servlet-api', version: '2.5'
          

Servlet 容器:Tomcat
类型:同步

IndexServlet.java

public class IndexServlet extends HttpServlet {
              @Override
              public void doGet(HttpServletRequest req, HttpServletResponse resp)
                      throws IOException, ServletException {
                  try {
                      Thread.sleep(150);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  resp.setContentType("text/html;charset=UTF-8");
                  PrintWriter out = resp.getWriter();
                  out.println("Hello World!");
                  out.flush();
              }
          }
          

测试结果:

all scenarios completed
          Complete report @ 2017-04-29T20:44:26.656Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 1274.1
            Request latency:
              min: 150.2
              max: 4452.4
              median: 3754.3
              p95: 4348.9
              p99: 4391.9
            Scenario duration:
              min: 29614.7
              max: 188861.1
              median: 180233.8
              p95: 188655.1
              p99: 188739.5
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

第二项测试:

Servlet 容器:Jetty
(其余跟第一项完全相同)

测试结果:

all scenarios completed
          Complete report @ 2017-04-29T20:50:07.995Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 1255.23
            Request latency:
              min: 150.2
              max: 8530.2
              median: 3771.6
              p95: 4459.3
              p99: 4531.6
            Scenario duration:
              min: 27930.2
              max: 191595.4
              median: 183742.4
              p95: 191402.1
              p99: 191508.2
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

Servlet3 异步容器测试

Servlet 依赖:

compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
          

Servlet 容器:Tomcat
类型:异步

IndexServlet.java

@WebServlet(urlPatterns = "/", asyncSupported = true)
          public class IndexServlet extends HttpServlet {
              @Override
              public void doGet(HttpServletRequest req, HttpServletResponse resp)
                      throws IOException, ServletException {
                  resp.setContentType("text/html;charset=UTF-8");
                  AsyncContext ctx = req.startAsync();
                  new Thread(new Executor(ctx)).start();
              }
          }
          
          class Executor implements Runnable {
              private AsyncContext ctx;
              Executor(AsyncContext ctx) {
                  this.ctx = ctx;
              }
              public void run() {
                  try {
                      Thread.sleep(150);
                      PrintWriter out = ctx.getResponse().getWriter();
                      out.println("Hello World!");
                      out.flush();
                      ctx.complete();
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }
          }
          

测试结果:

all scenarios completed
          Complete report @ 2017-04-29T17:46:45.825Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 2797.99
            Request latency:
              min: 150.3
              max: 845.3
              median: 244.4
              p95: 370.9
              p99: 445.7
            Scenario duration:
              min: 9427.6
              max: 27992.9
              median: 17424.7
              p95: 26784.6
              p99: 27175.7
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

第二项测试(Jettty):

all scenarios completed
          Complete report @ 2017-04-29T17:42:50.983Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 2838.49
            Request latency:
              min: 150.2
              max: 746.4
              median: 229.1
              p95: 321.4
              p99: 385.6
            Scenario duration:
              min: 8977.9
              max: 22505.3
              median: 17095.1
              p95: 21894.5
              p99: 22229.2
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

Vert.x 性能测试

Vert.x 依赖:

compile group: 'io.vertx', name: 'vertx-web', version: '3.4.1'
          

Core code:

@Override
          public void start(Future<Void> startFuture) throws Exception {
              HttpServer server = vertx.createHttpServer();
              Router router = Router.router(vertx);
              router.route("/").handler(routingContext -> {
                  HttpServerResponse response = routingContext.response();
                  new Thread(() -> {
                      try {
                          Thread.sleep(150);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      response.putHeader("content-type", "text/html;charset=UTF-8");
                      response.end("Hello World!");
                  }).start();
              });
              server.requestHandler(router::accept).listen(8080);
          }
          

测试结果:

all scenarios completed
          Complete report @ 2017-04-29T18:57:59.848Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 2930.83
            Request latency:
              min: 150.2
              max: 996.4
              median: 264.8
              p95: 411.4
              p99: 549.9
            Scenario duration:
              min: 9043.1
              max: 32210.9
              median: 19721
              p95: 31235.8
              p99: 31507.3
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

结果总结

并发数从高到底依次是:Vert.x > Jetty(ASync) > Tomcat(ASync) 。它们之间的差距非常小,不超过 150 个并发。
然后是:Tomcat(Sync) > Jetty(Sync) 。20 个并发差距左右。

它们都分别经历了三次维持 60 秒的每秒 5000 个并发请求的测试,并且我们甚至可以认为,在这次测试中:同步和异步两个类型分别的性能对比大致上等同。

关于其它框架的测试

我主要测试了两个不同类型的「两个方面」:

  • Servlet 和 NoServlet 对比
  • 同步和异步的对比

无论什么框架,如果是基于 Servlet 或者 Netty 的。它们的性能差异肯定不大(至少绝不会想测试中的同/异步差距那么大),例如 Spring MVC 也在很早就支持了 Servlet3 的 AsyncContext,如果继续测试 Spring MVC 的同步和异步只是重蹈 Servlet 的同步异步测试的覆辙而已。并且由于 Spring MVC 的封装,它的测试结果肯定是 < Servlet 的。

所以,没有继续测试其它 Web 框架的必要。

数据中的差异

基本上同步 Tomcat 和 同步 Jetty 的差异非常小,异步 Tomcat&Jetty 同样。如果硬要说性能,差异并不大。即使说 Jetty 或者 Tomcat 某一方勉强性能更强一点点,性能偏弱的 Servlet 中间件多用异步的话,仍然能吊打性能强的一方,并且有相当高的差距(接近 2.5 倍)。

抛开 Servlet 中间件,以 Servlet 和 JAX-RS 规范为基础的 Spring Boot、RestEasy、Jersey、CXF 等 Web 框架(特意举例的流行的 RESTful 框架)差异又能有多少呢?

单纯比较出性能高低意义非常小,一方面是本身它们的差距就不会太大,第二方面是 IO 阻塞才是最大的性能问题之一,合理利用异步比换个性能稍强点的框架代价要小得多,且效果显著得多。

测试中的不当之处

不当之处还是很多的,例如任何一个中间件和框架都没有优化设置,全部是默认配置。但是调优下来测试又非常麻烦,本身调优这东西就只有最适合场景的方案,没有万能的最好方案。所以默认配置测试也没什么… 除非某个东西的默认配置非常不适合这个测试场景,至少本文出现的内容中并没有这种现象。

Web 应用性能不止只有吞吐性能,虽然它是最直观最直接表现出来的。但是系统资源消耗:CPU 利用率、内存/缓存占用等指标也有很大的意义。如果能以更低的代价发挥出同样的性能,相比之下这本身就是一种更高性能的表现。

(请不要提那个白痴的暂停线程模拟 IO 阻塞)

我会怎么选

如果让我选择用 Tomcat 还是 Jetty,至少性能绝不会是我的第一指标。Jetty 瞄准的是轻量级、模块化、嵌入式容器的方向,而 Tomcat 能应对更通用的场景,它们之间只有更适合没有谁最好。
例如,你如果用 Spring Boot 那么你就通常会用 Tomcat。如果你用 Dropwizard 那么你肯定用的是 Jetty。框架是一种场景,业务类型也是。场景的不同,这个太重要了。

关于 Web 框架,性能当然也不会是我看重的第一指标。如果现有项目是 Spring MVC,但是有人抱怨 Spring MVC 过重,已经出现性能问题,那么我首先肯定不会相信性能问题已经达到要换框架的地步了。因为往常,任意一个横向扩展方向(例如集群、分布式)足矣解决框架封装过重导致的性能开销问题,更不要提异步让性能优化在内部展现了。
一个框架(特别是知名框架)能考量的地方很多:例如文档完善程度、项目活跃度、是否采用更新的技术、成功案例数量、哪些公司在采用等都是很好的考量标准。框架用了怎样的技术往往也决定了它的上限,例如一个仅支持 Servlet2 阶段的框架,它的性能上限永远不可能超过采用异步的框架,并且要低得多。文档、活跃程度则更容易让人全方面的上手,毕竟尽可能的不要去自己踩坑。

最后

事实证明我的观点还是很正确的,如果仍然还在想怎样换一种框架提高性能,而不是换一种解决问题的架构模式,视野未免过于狭窄。
NodeJS 几年前就火过了,天生的事件驱动异步模型导致它的超高并发性能直接吊打 Java 这种传统的多线程同步 Server 的应用。当然,那时候 Java 就已经诞生了类似的技术,只是人们往往不愿意抛弃自己掌握和用熟的东西而已。
就例如 Vert.x 因为非 JavaEE标准(Servlet)所以不能与众多传统常用的框架相配合,导致很多人不知道这种新应用该怎么去写,因为他们仍然想着用传统的做法去使用一个崭新的框架。

附加一些其它语言框架测试:

NodeJs&Koa(天生的事件驱动异步模型)

依赖版本:

"dependencies": {
            "koa": "^2.2.0",
            "koa-route": "^3.2.0",
          }
          

Core code:

app.use(_.get('/', (ctx, next) => {
              return new Promise(resolve => {
                  setTimeout(resolve, 150);
              })
                  .then(() => {
                      return next().then(() => {
                          ctx.body = 'Hello World!';
                      });
                  });
          }));
          

测试结果:

all scenarios completed
          Complete report @ 2017-05-01T05:09:14.465Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 2925.97
            Request latency:
              min: 149.4
              max: 965.5
              median: 248.9
              p95: 363.1
              p99: 422.6
            Scenario duration:
              min: 8657.5
              max: 27171.1
              median: 18091.1
              p95: 26396.1
              p99: 26547.6
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

Python&Sanic(基于 uvloop 的异步框架)

依赖版本:

sanic==0.5.2
          asyncio==3.4.3
          

Core code:

#!/usr/bin/env python3
          import asyncio
          
          from sanic import Sanic
          from sanic.response import text
          
          
          app = Sanic()
          
          
          @app.route("/")
          async def hello(request):
              await asyncio.sleep(0.15)
              return text("Hello World!")
          
          if __name__ == "__main__":
              app.run(host="0.0.0.0", port=8080)
          

测试结果:

all scenarios completed
          Complete report @ 2017-05-01T06:40:18.406Z
            Scenarios launched:  6000
            Scenarios completed: 6000
            Requests completed:  300000
            RPS sent: 2977.08
            Request latency:
              min: 149.3
              max: 913.1
              median: 251.2
              p95: 400.7
              p99: 482
            Scenario duration:
              min: 8546.9
              max: 31919.4
              median: 17875
              p95: 30743.9
              p99: 31134.2
            Scenario counts:
              0: 6000 (100%)
            Codes:
              200: 300000
          

可以发现,在其它语言上异步的框架并发性能也是超高的。Python、Ruby 等动态语言的性能相比 Java 低很多,它们若使用同步 Web 框架(例如 Sinatra、Flask)的并发性能也比 Servlet 同步性能低很多(某些 Ruby 框架甚至低到不忍直视)。但是若都使用异步模型,可以发现它们的差距变得几乎没有了。

最后再提一下,我这份测试可能根本不能代表任何事情。在 TechemPower 上有大部分流行 web 框架对比,这里是最新的结果。可以看到,Netty 以及 Vert.x 遥遥领先于 Koa 。