异步Servlet在什么样的场景下能发挥作用?

从 Servlet 3.0开始, 异步Servlet成为了标准, 在此之前类似jetty这样的web服务器都已经有了自己的实现. 从2011年3月份 Servlet 3.0 的最终规范出来到现在4年已经过去了, 似乎在实际项目上看到的用异步方式处理HTTP请求的例子并不多. 我想不是因为异步Servlet太复杂, 也不是因为异步Servlet的实现不稳定, 而是多数情况下人们找不到应用他的场景. 我在2年前的一个即时通信工具的web版上采用了异步Servlet, 但是由于没有做完整的性能测试, 也不知道那个选择是否正确.

这几天做了一些小测试, 想验证一下什么样的场景下适合用异步Servlet.
代码地址: https://github.com/zjumty/spring-async-perftest

首先讲一下异步Servlet的基本用法:

从Servlet 3.0开始, HttpServletRequest多了一个startAsync方法, 这个方法返回一个AsyncContext对象. 简单来讲异步Servlet就是用这个东西.

想要你的Servlet支持异步处理,你还需要在web.xml的servlet上加上如下属性:
Xml代码   收藏代码
  1. <async-supported>true</async-supported>  


在你的Servlet的doXXX方法中, 调用request.startAsync()方法, 得到一个AsyncContext对象, 你然后就可以让你的doXXX方法结束了, 这时这个HTTP请求的处理线程就已经完成任务了, 可以继续为后续的请求提供服务器. 再此之前你需要把AsyncContext传递给其他的工作线程(多采用线程池或或异步回调方法), 在那个线程的处理完成后, 把数据通过AsyncContext里的Response对象发送给浏览器, 然后调用AsyncContext中的complete方法完成本次HTTP的流程.

在客户端浏览器来看, 仍然是一个Request, 一个Response, 中间没有什么中断. 在startAsync以后, 当前的请求就会pending在一个队列中而不用持续占用一个线程.

当时实际上, 异步Servlet有很多方法, 处理逻辑也可以很复杂.

在本文的代码中没有采用上面这种原始的方式,而是采用Spring MVC的异步功能, 实现起来更简单.

Java代码   收藏代码
  1. @Controller  
  2. @RequestMapping("/foo")  
  3. public class FooController {  
  4.     @Autowired  
  5.     @Qualifier("workerPool")  
  6.     private ExecutorService workerPool;  
  7.   
  8.     @RequestMapping("/async-100ms")  
  9.     public @ResponseBody DeferredResult<FooBean> async100ms() throws Exception {  
  10.         DeferredResult<FooBean> defer = new DeferredResult<>(120000);  
  11.         workerPool.submit(() -> {  
  12.             try {  
  13.                 Thread.sleep(100);  
  14.             } catch (InterruptedException e) {  
  15.                 e.printStackTrace();  
  16.             }  
  17.             defer.setResult(new FooBean());  
  18.         });  
  19.         return defer;  
  20.     }  
  21. }  


同时@Conntroller, 同样是@RequestMapping, 不同的只有返回值是DeferredResult, 一切就这么简单.

后面Spring其实也是用异步Servlet的那些方法和对象实现的. 上面的代码中我们设置了timeout时间为2分钟. 默认是30秒, 如果处理时间一长, 客户端就收到503错误.

为了有对照这里还增加了一些同步方法.

Java代码   收藏代码
  1. @RequestMapping("/sync-100ms")  
  2. public @ResponseBody FooBean sync100ms() throws Exception {  
  3.     Future<FooBean> future = workerPool.submit(() ->  
  4.     {  
  5.         Thread.sleep(100);  
  6.         return new FooBean();  
  7.     });  
  8.     return future.get();  
  9. }  


还有一个没有延迟的处理:

Java代码   收藏代码
  1. @RequestMapping("/nodelay")  
  2. public @ResponseBody FooBean nodelay() throws Exception {  
  3.     return new FooBean();  
  4. }  


考虑到jetty的工作线程多少也会影响到结果. 所以我分别用16线程和200线程两个环境做了测试.

因为我是用的spring-boot的内嵌jetty模式, 改变工作线程数稍微有点小复杂.

Java代码   收藏代码
  1. @Configuration  
  2. public class JettyConfiguration {  
  3.   
  4.     @Value("${jetty.threadPool.minSize}")  
  5.     private int minSize;  
  6.   
  7.     @Value("${jetty.threadPool.maxSize}")  
  8.     private int maxSize;  
  9.   
  10.     @Bean  
  11.     public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {  
  12.         JettyEmbeddedServletContainerFactory factory = new JettyEmbeddedServletContainerFactory();  
  13.         factory.addServerCustomizers(server -> {  
  14.             QueuedThreadPool threadPool = (QueuedThreadPool) server.getThreadPool();  
  15.             threadPool.setMaxThreads(maxSize);  
  16.             threadPool.setMinThreads(minSize);  
  17.             threadPool.setName("jetty-");  
  18.         });  
  19.   
  20.         return factory;  
  21.     }  
  22. }  


其实也没复杂到哪里去 . 我在application.yml文件里预置了16和200线程两种.通过启动参数可以选择:

Java代码   收藏代码
  1. java -jar -Xmx2048m async-0.0.1.jar --server.port=9000 --spring.profiles.active=dev  


当然也可以通过-Djetty.threadPool.maxSize=n来任意指定.

测试环境: 这次小奢侈一把, 动用了公司的测试服务器:Intel(R) Xeon(R) CPU E5-2643 v2 @ 3.50GHz 6*4 (反正闲着也是闲着嘛    )

首先看看单场景异步模式和同步模式的区别:

引用

./wrk -c 100 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/async-100ms

Running 1m test @ http://192.168.200.23:9000/foo/async-100ms
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   609.43ms   32.22ms 724.79ms   98.11%
    Req/Sec    25.65     16.62    90.00     74.23%
  9360 requests in 1.00m, 2.41MB read
Requests/sec:    155.83
Transfer/sec:     41.09KB

./wrk -c 100 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/sync-100ms

Running 1m test @ http://192.168.200.23:9000/foo/sync-100ms
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   605.78ms   43.94ms 706.07ms   96.43%
    Req/Sec    28.31     23.22    90.00     73.35%
  9408 requests in 1.00m, 2.42MB read
Requests/sec:    156.64
Transfer/sec:     41.30KB


哦! wrk是啥? 看这里:
Java代码   收藏代码
  1. https://github.com/wg/wrk  


上面的结果可以看出, 基本没有区别.无论你用那种模式由于延迟的存在, CPU的利用率都不高. 所有即便同步模式下有频繁的线程切换, 只是CPU的利用率稍稍搞了一点. 吞吐量是一样的.

在来看看服务器线程数对性能的影响

Java代码   收藏代码
  1. ./wrk -c 5000 -t 16 -d 60 --timeout=120 http://192.168.200.23:9000/foo/nodelay  


200 Threads
Java代码   收藏代码
  1. Running 1m test @ http://192.168.200.23:9000/foo/nodelay  
  2.   16 threads and 5000 connections  
  3.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  4.     Latency   104.23ms   31.37ms   2.00s    93.20%  
  5.     Req/Sec     2.98k   276.67     6.04k    87.11%  
  6.   2844140 requests in 1.00m, 732.35MB read  
  7. Requests/sec:  47324.93  
  8. Transfer/sec:     12.19MB  


16 Threads
Java代码   收藏代码
  1. Running 1m test @ http://192.168.200.23:9000/foo/nodelay  
  2.   16 threads and 5000 connections  
  3.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  4.     Latency    91.36ms   76.54ms   3.97s    98.46%  
  5.     Req/Sec     3.49k   473.18    11.44k    86.49%  
  6.   3341535 requests in 1.00m, 860.42MB read  
  7. Requests/sec:  55628.92  
  8. Transfer/sec:     14.32MB  


线程少时吞吐量更好. 意料之中, 频繁线程切换回消耗一定的资源.

再来看看异步模式下的16线程和200线程

Java代码   收藏代码
  1. ./wrk -c 5000 -t 16 -d 60 --timeout=120 http://192.168.200.23:9000/foo/async-100ms  


200 Threads
Java代码   收藏代码
  1. Running 1m test @ http://192.168.200.23:9000/foo/async-100ms  
  2.   16 threads and 500 connections  
  3.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  4.     Latency     3.05s   502.13ms   4.64s    93.47%  
  5.     Req/Sec    20.05     21.61   121.00     87.01%  
  6.   9373 requests in 1.00m, 2.41MB read  
  7. Requests/sec:    155.99  
  8. Transfer/sec:     41.13KB  


16 Threads
Java代码   收藏代码
  1. Running 1m test @ http://192.168.200.23:9000/foo/async-100ms  
  2.   16 threads and 500 connections  
  3.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  4.     Latency     3.05s   448.49ms   3.29s    94.05%  
  5.     Req/Sec    25.65     31.66   161.00     84.48%  
  6.   9408 requests in 1.00m, 2.42MB read  
  7. Requests/sec:    156.62  
  8. Transfer/sec:     41.30KB  


基本没有区别.原因跟第一个场景一样.

混合场景: 延迟和非延迟 1:9的请求, 也就是说有少量的高延迟访问, 大量的低延迟访问

高延迟采用异步模式:

Java代码   收藏代码
  1. ./wrk -c 900 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/nodelay  
  2. Running 1m test @ http://192.168.200.23:9000/foo/nodelay  
  3.   8 threads and 900 connections  
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  5.     Latency    16.13ms   24.54ms 863.32ms   97.81%  
  6.     Req/Sec     8.36k   689.01    11.18k    87.42%  
  7.   3993216 requests in 1.00m, 1.00GB read  
  8. Requests/sec:  66512.06  
  9. Transfer/sec:     17.13MB  
  10.   
  11. ./wrk -c 100 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/async-100ms  
  12. Running 1m test @ http://192.168.200.23:9000/foo/async-100ms  
  13.   8 threads and 100 connections  
  14.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  15.     Latency   610.82ms   35.10ms 816.95ms   97.39%  
  16.     Req/Sec    24.43     23.64   111.00     88.33%  
  17.   9392 requests in 1.00m, 2.42MB read  
  18. Requests/sec:    156.39  
  19. Transfer/sec:     41.23KB  


高延迟采用同步模式

Java代码   收藏代码
  1. ./wrk -c 900 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/nodelay  
  2. Running 1m test @ http://192.168.200.23:9000/foo/nodelay  
  3.   8 threads and 900 connections  
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  5.     Latency   213.21ms  214.30ms 863.02ms   75.67%  
  6.     Req/Sec   640.82      1.75k   11.60k    94.33%  
  7.   306108 requests in 1.00m, 78.82MB read  
  8. Requests/sec:   5098.09  
  9. Transfer/sec:      1.31MB  
  10.   
  11. ./wrk -c 100 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/sync-100ms  
  12. Running 1m test @ http://192.168.200.23:9000/foo/sync-100ms  
  13.   8 threads and 100 connections  
  14.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  15.     Latency   610.71ms   30.23ms 831.14ms   98.95%  
  16.     Req/Sec    30.99     30.55   111.00     82.36%  
  17.   9392 requests in 1.00m, 2.42MB read  
  18. Requests/sec:    156.38  
  19. Transfer/sec:     41.23KB  


Oh,Yeah! 终于看到区别了, 差了10倍多!

在同步模式下由于Http线程被高延迟处理霸占, 没有其他线程处理低延迟请求. 而异步模式下由于高延迟处理不霸占HTTP线程, 所以低延迟的请求基本上没有受到影响.

上面的测试是在服务器设置了16线程的情况下执行的. 如果高延迟处理占用了线程, 那我们把服务器线程设置为200再试试.

同步模式:

Java代码   收藏代码
  1. ./wrk -c 900 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/nodelay  
  2. Running 1m test @ http://192.168.200.23:9000/foo/nodelay  
  3.   8 threads and 900 connections  
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  5.     Latency    25.36ms   99.18ms   3.33s    99.21%  
  6.     Req/Sec     5.90k     0.87k    8.96k    92.58%  
  7.   2819464 requests in 1.00m, 725.99MB read  
  8. Requests/sec:  46961.89  
  9. Transfer/sec:     12.09MB  
  10.   
  11. ./wrk -c 100 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/sync-100ms  
  12. Running 1m test @ http://192.168.200.23:9000/foo/sync-100ms  
  13.   8 threads and 100 connections  
  14.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  15.     Latency   610.49ms   37.34ms 689.02ms   97.20%  
  16.     Req/Sec    25.67     21.57   101.00     72.72%  
  17.   9359 requests in 1.00m, 2.41MB read  
  18. Requests/sec:    155.80  
  19. Transfer/sec:     41.08KB  


可以看到同步模式比之前的结果好多了, 也就是高延迟处理不会对低延迟有很大影响, 但是任然比异步模式差.

再看看200线程的异步模式的结果:
Java代码   收藏代码
  1. ./wrk -c 900 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/nodelay  
  2. Running 1m test @ http://192.168.200.23:9000/foo/nodelay  
  3.   8 threads and 900 connections  
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  5.     Latency    20.05ms   18.33ms 890.99ms   92.99%  
  6.     Req/Sec     6.01k   561.72    13.69k    90.15%  
  7.   2874649 requests in 1.00m, 740.20MB read  
  8. Requests/sec:  47831.09  
  9. Transfer/sec:     12.32MB  
  10.   
  11. ./wrk -c 100 -t 8 -d 60 --timeout=120 http://192.168.200.23:9000/foo/async-100ms  
  12. Running 1m test @ http://192.168.200.23:9000/foo/async-100ms  
  13.   8 threads and 100 connections  
  14.   Thread Stats   Avg      Stdev     Max   +/- Stdev  
  15.     Latency   613.31ms   27.23ms 721.98ms   96.03%  
  16.     Req/Sec    24.51     18.56    90.00     70.81%  
  17.   9349 requests in 1.00m, 2.41MB read  
  18. Requests/sec:    155.68  
  19. Transfer/sec:     41.05KB  


可以看到在200线程的异步模式只比同步模式好一点点.

所以综合上面的测试结果, 可以得到如下结论: 异步Servlet模式在单场景和同步模式下几乎没有差别. 由于开发上更复杂所以没什么可取之处. 建议直接用同步模式. 在混合场景中如果是大量低延迟处理+少量高延迟处理的情况下, 由于异步模式不占用服务器线程, 可以有效减少服务器上的线程切换的资源消耗, 并且充分利用系统资源, 是有可取之处的.

上面测试的最佳结果的配置:

jetty服务器线程池16线程, 低延迟处理立刻返回, 高延迟处理延迟100毫秒. 低延迟和高延迟请求比9:1.

最后想说的是, 如果想让异步处理在系统的整体性能方面起作用, 最好把整个系统都设计成异步的, 也就是Reactive模式的. 否则有一个同步的地方卡在那里, 整体上就不会看到异步带来哦好处.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值