商品详情页系统的Servlet3异步化实践
在京东工作的这一年多时间里,我在整个商品详情页系统(后端数据源)及商品详情页统一服务系统(页面中异步加载的很多服务,如库存服务、图书相关服务、延保服务等)中使用了Servlet3请求异步化模型,总结了Servlet3请求异步化的一些经验和想法跟大家分享和交流。
我将从如下几点阐述Servlet3异步化之后的好处:
1、为什么实现请求异步化需要使用Servlet3
2、请求异步化后得到的好处是什么
3、如何使用Servlet3异步化
4、一些Servlet3异步化压测数据
首先说下一般http请求处理的流程:
1、容器负责接收并解析请求为HttpServletRequest;
2、然后交给Servlet进行业务处理;
3、最后通过HttpServletResponse写出响应。
在Servlet2.x规范中,所有这些处理都是同步进行的,也就是说必须在一个线程中完成从接收请求、业务处理到响应。
1、为什么实现请求异步化需要使用Servlet3
此处已Tomcat6举例子,Tomcat6没有实现Servlet3规范,它在处理请求时是通过如下方式实现的:
org.apache.catalina.connector.CoyoteAdapter#service
- // Recycle the wrapper request and response
- if (!comet) {
- request.recycle();
- response.recycle();
- } else {
- // Clear converters so that the minimum amount of memory
- // is used by this processor
- request.clearEncoders();
- response.clearEncoders();
- }
在请求结束时会同步进行请求的回收,也就是说请求解析、业务处理和响应必须在一个线程内完成,不能跨越线程界限。
这也就说明了必须使用实现了Servlet规范的容器进行处理,如Tomcat 7.x。
2、请求异步化后得到的好处是什么
2.1、更高的并发能力;
2.2、请求解析和业务处理线程池分离;
2.3、根据业务重要性对业务分级,并分级线程池;
2.4、对业务线程池进行监控、运维、降级等处理。
2.1、更高的并发能力
得益于技术的升级,在JDK7配合Tomcat7压测中获得了不错的性能表现。
2.2、请求解析和业务处理线程池分离
在引入Servlet3之前我们的线程模型是如下样子的:
整个请求解析、业务处理、生成响应都是由Tomcat线程池进行处理,而且都是在一个线程中处理;不能分离线程处理;比如接收到请求后交给其他线程处理,这样不能灵活定义业务处理模型。
引入Servlet3之后,我们的线程模型可以改造为如下样子:
此处可以看到请求解析使用Tomcat单线程;而解析完成后会扔到业务队列中,由业务线程池进行处理;这种处理方式可以得到如下好处:
1、根据业务重要性对业务进行分级,然后根据分级定义线程池;
2、可以拿到业务线程池,可以进行很多的操作,比如监控、降级等。
2.3、根据业务重要性对业务分级,并分级线程池
在一个系统的发展期间,我们一般把很多服务放到一个系统中进行处理,比如库存服务、图书相关服务、延保服务等等;这些服务中我们可以根据其重要性对业务分级别和做一些限制:
1、可以把业务分为核心业务级别和非核心业务级别;
2、为不同级别的业务定义不同的线程池,线程池之间是隔离的;
3、根据业务量定义各级别线程池大小。
此时假设非核心业务因为数据库连接池或者网络问题抖动造成响应时间慢,不会对我们核心业务产生影响。
2.4、对业务线程池进行监控、运维、降级等处理
因为业务线程池从Tomcat中分离出来,可以进行线程池的监控,比如查看当前处理的请求有多少,是否到了负载瓶颈,到了瓶颈后可以进行业务报警等处理。
上图是我们一个简陋的监控图,可实时查看到当前处理情况:正在处理的任务有多少,队列中等待的任务有多少;可以根据这些数据进行监控和预警。
另外我们还可以进行一些简单的运维:
对业务线程池进行扩容,或者业务出问题时立即清空线程池防止容器崩溃等问题;而不需要等待容器重启(容器重启需要耗费数十秒甚至数几十毫秒、而且启动后会有预热问题,而造成业务产生抖动)。
如果发现请求处理不过来,负载比较高,最简单的办法就是直接清空线程池,将老请求拒绝掉,而没有雪崩效应。
因为业务队列和业务线程池都是自己的,可以对这些基础组件做很多处理,比如定制业务队列,按照用户级别对用户请求排序,高级别用户得到更高优先级的业务处理。
3、如何使用Servlet3异步化
对于Servlet3的使用,可以参考我之前的博客:
而在我项目里使用就比较简单:
1、接收请求
- @RequestMapping("/book")
- public void getBook(HttpServletRequest request, @RequestParam(value="skuId") final Long skuId,
- @RequestParam(value="cat1") final Integer cat1, @RequestParam(value="cat2") final Integer cat2) throws Exception {
- oneLevelAsyncContext.submitFuture(request, () -> bookService.getBook(skuId, cat1, cat2));
- }
通过一级业务线程池接收请求,并提交业务处理到该线程池;
2、业务线程池封装
- public void submitFuture(final HttpServletRequest req, final Callable<Object> task) {
- final String uri = req.getRequestURI();
- final Map<String, String[]> params = req.getParameterMap();
- final AsyncContext asyncContext = req.startAsync(); //开启异步上下文
- asyncContext.getRequest().setAttribute("uri", uri);
- asyncContext.getRequest().setAttribute("params", params);
- asyncContext.setTimeout(asyncTimeoutInSeconds * 1000);
- if(asyncListener != null) {
- asyncContext.addListener(asyncListener);
- }
- executor.submit(new CanceledCallable(asyncContext) { //提交任务给业务线程池
- @Override
- public Object call() throws Exception {
- Object o = task.call(); //业务处理调用
- if(o == null) {
- callBack(asyncContext, o, uri, params); //业务完成后,响应处理
- }
- if(o instanceof CompletableFuture) {
- CompletableFuture<Object> future = (CompletableFuture<Object>)o;
- future.thenAccept(resultObject -> callBack(asyncContext, resultObject, uri, params))
- .exceptionally(e -> {
- callBack(asyncContext, "", uri, params);
- return null;
- });
- } else if(o instanceof String) {
- callBack(asyncContext, o, uri, params);
- }
- return null;
- }
- });
- }
- private void callBack(AsyncContext asyncContext, Object result, String uri, Map<String, String[]> params) {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- try {
- if(result instanceof String) {
- write(resp, (String)result);
- } else {
- write(resp, JSONUtils.toJSON(result));
- }
- } catch (Throwable e) {
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); //程序内部错误
- try {
- LOG.error("get info error, uri : {}, params : {}", uri, JSONUtils.toJSON(params), e);
- } catch (Exception ex) {
- }
- } finally {
- asyncContext.complete();
- }
- }
线程池的初始化
- @Override
- public void afterPropertiesSet() throws Exception {
- String[] poolSizes = poolSize.split("-");
- //初始线程池大小
- int corePoolSize = Integer.valueOf(poolSizes[0]);
- //最大线程池大小
- int maximumPoolSize = Integer.valueOf(poolSizes[1]);
- queue = new LinkedBlockingDeque<Runnable>(queueCapacity);
- executor = new ThreadPoolExecutor(
- corePoolSize, maximumPoolSize,
- keepAliveTimeInSeconds, TimeUnit.SECONDS,
- queue);
- executor.allowCoreThreadTimeOut(true);
- executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
- @Override
- public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
- if(r instanceof CanceledCallable) {
- CanceledCallable cc = ((CanceledCallable) r);
- AsyncContext asyncContext = cc.asyncContext;
- if(asyncContext != null) {
- try {
- String uri = (String) asyncContext.getRequest().getAttribute("uri");
- Map params = (Map) asyncContext.getRequest().getAttribute("params");
- LOG.error("async request rejected, uri : {}, params : {}", uri, JSONUtils.toJSON(params));
- } catch (Exception e) {}
- try {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- } finally {
- asyncContext.complete();
- }
- }
- }
- }
- });
- if(asyncListener == null) {
- asyncListener = new AsyncListener() {
- @Override
- public void onComplete(AsyncEvent event) throws IOException {
- }
- @Override
- public void onTimeout(AsyncEvent event) throws IOException {
- AsyncContext asyncContext = event.getAsyncContext();
- try {
- String uri = (String) asyncContext.getRequest().getAttribute("uri");
- Map params = (Map) asyncContext.getRequest().getAttribute("params");
- LOG.error("async request timeout, uri : {}, params : {}", uri, JSONUtils.toJSON(params));
- } catch (Exception e) {}
- try {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- } finally {
- asyncContext.complete();
- }
- }
- @Override
- public void onError(AsyncEvent event) throws IOException {
- AsyncContext asyncContext = event.getAsyncContext();
- try {
- String uri = (String) asyncContext.getRequest().getAttribute("uri");
- Map params = (Map) asyncContext.getRequest().getAttribute("params");
- LOG.error("async request error, uri : {}, params : {}", uri, JSONUtils.toJSON(params));
- } catch (Exception e) {}
- try {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- } finally {
- asyncContext.complete();
- }
- }
- @Override
- public void onStartAsync(AsyncEvent event) throws IOException {
- }
- };
- }
- }
3、业务处理
执行bookService.getBook(skuId, cat1, cat2)进行业务处理。
4、返回响应
在之前封装的异步线程池上下文中直接返回。
5、Tomcat server.xml的配置
- <Connector port="1601" asyncTimeout="10000" acceptCount="10240" maxConnections="10240" acceptorThreadCount="1" minSpareThreads="1" maxThreads="1" redirectPort="8443" processorCache="1024" URIEncoding="UTF-8" protocol="org.apache.coyote.http11.Http11NioProtocol" enableLookups="false"/>
我们升级到了jdk1.8.0_51 +tomcat 8.0.26,在使用Http11Nio2Protocol时遇到一些问题,暂时还是使用的Http11Nio1Protocol。此处可以看到Tomcat线程池我们配置了maxThreads=1,即一个线程进行请求解析。
4、一些Servlet3异步化压测数据
压测机器基本环境:32核CPU、32G内存;jdk1.7.0_71 + tomcat 7.0.57,服务响应时间在20ms+,使用最简单的单个URL压测吞吐量:
1、使用同步方式压测
- siege-3.0.7]# ./src/siege -c100 -t60s -b http://***.item.jd.com/981821
- Transactions: 279187 hits
- Availability: 100.00 %
- Elapsed time: 59.33 secs
- Data transferred: 1669.41 MB
- Response time: 0.02 secs
- Transaction rate: 4705.66 trans/sec
- Throughput: 28.14 MB/sec
- Concurrency: 99.91
- Successful transactions: 279187
- Failed transactions: 0
- Longest transaction: 1.04
- Shortest transaction: 0.00
2.1、 使用Servlet3异步化压测 100并发、60秒:
- siege-3.0.7]# ./src/siege -c100 -t60s -b http://***.item.jd.com/981821 .
- Transactions: 337998 hits
- Availability: 100.00 %
- Elapsed time: 59.09 secs
- Data transferred: 2021.07 MB
- Response time: 0.03 secs
- Transaction rate: 5720.05 trans/sec
- Throughput: 34.20 MB/sec
- Concurrency: 149.79
- Successful transactions: 337998
- Failed transactions: 0
- Longest transaction: 1.07
- Shortest transaction: 0.00
2.2、使用Servlet3异步化压测 600并发、60秒:
- siege-3.0.7]# ./src/siege -c600 -t60s -b http://***.item.jd.com/981821
- Transactions: 370985 hits
- Availability: 100.00 %
- Elapsed time: 59.16 secs
- Data transferred: 2218.32 MB
- Response time: 0.10 secs
- Transaction rate: 6270.88 trans/sec
- Throughput: 37.50 MB/sec
- Concurrency: 598.31
- Successful transactions: 370985
- Failed transactions: 0
- Longest transaction: 1.32
- Shortest transaction: 0.00
可以看出异步化之后吞吐量提升了,但是响应时间长了,也就是异步化并不会提升响应时间,但是会增加吞吐量和增加我们需要的灵活性。
通过异步化我们不会获得更快的响应时间,但是我们获得了整体吞吐量和我们需要的灵活性:请求解析和业务处理线程池分离;根据业务重要性对业务分级,并分级线程池;对业务线程池进行监控、运维、降级等处理。
商品详情页系统的Servlet3异步化实践的更多相关文章
- java亿级流量电商详情页系统的大型高并发与高可用缓存架构实战视频教程
亿级流量电商详情页系统的大型高并发与高可用缓存架构实战 完整高清含源码,需要课程的联系QQ:2608609000 1[免费观看]课程介绍以及高并发高可用复杂系统中的缓存架构有哪些东西2[免费观看]基于 ...
- Freemarker商品详情页静态化服务调用处理
--------------------------------------------------------------------------------------------- [版权申明: ...
- 高并发 Nginx+Lua OpenResty系列(10)——商品详情页
本章以京东商品详情页为例,京东商品详情页虽然仅是单个页面,但是其数据聚合源是非常多的,除了一些实时性要求比较高的如价格.库存.服务支持等通过AJAX异步加载加载之外,其他的数据都是在后端做数据聚合然后 ...
- 自己定义ViewGroup实现仿淘宝的商品详情页
近期公司在新版本号上有一个须要. 要在首页加入一个滑动效果, 详细就是仿照X宝的商品详情页, 拉到页面底部时有一个粘滞效果, 例如以下图 X东的商品详情页,假设用户继续向上拉的话就进入商品图文描写叙述 ...
- 如何用Baas快速在腾讯云上开发小程序-系列4:实现客户侧商品列表、商品详情页程序
版权声明:本文由贺嘉 原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/431172001487671163 来源:腾云阁 h ...
- Ecshop商品详情页显示当前会员等级价格
会员登录状态下,在ECSHOP商品详情页的本店售价中显示当前登录会员对应的等级价格,在未登录状态下,则还默认显示原来的本店售价. 解决方法: 这个需要修改ECSHOP程序代码来实现. 打开文件 /in ...
- ecshop 商品详情页显示同类别下的推荐商品
1.打开goods.php文件找到下面代码 $smarty->assign('goods_rank', get_goods_rank($goods_id)); // 商品的销售排名 在上面的代码 ...
- ecshop 优化_将商品详情页goods.php重命名为shangpin.php
有人说,将商品详情页的文件名 goods.php 改一个名字,对百度收录会有帮助,也许吧,这里不讨论是否有帮助,这里只讲解如何重命名. 例如:我们将 goods.php 改为 shangpin.php ...
- Vue通过id跳转到商品详情页
首页列表: 在这里我用a标签进行跳转,在vue里面使用<router-link></router-link> <router-link :to="{path:' ...
随机推荐
- [转帖]SAP一句话入门:Sales and Distribution
SAP一句话入门:Sales and Distribution http://blog.vsharing.com/MilesForce/A616565.html SD是Sales and Distri ...
- Linux基础操作二
编程语言的作用及与操作系统和硬件的关系 编程语言的作用:用来定义计算机程序的形式,程序员用它来编写程序,进而控制其向计算机发出指令,使计算机完成人类布置的任务. 编程语言的作用及与操作系统和硬件的关系 ...
- 使用Elasticsearch 出现的拒绝连接
pom 文件 spring: elasticsearch: jest: uris: http://192.168.124.142:9201 # data: # elasticsearch: # clu ...
- Java变量类型识别的3种方式
内容导览 反射方式,成员变量的类型判断 isInstance用法 利用泛型识别类型 测试类: package com.cxyapi.oo; import java.util.Date; import ...
- ERP系统的问题
1.业务统计报表导出超时 2.库存统计相关接口查询容易导致慢查询,而且慢查询出现在主库上
- QTP自动化测试-点滴-步骤
1 添加 test 2 设置 整个测试项目的 setting -数据表位置 3 添加 引用 方法文件 4 添加 action 5 添加 action 对应的 repository 控件库 6 录制.整 ...
- Calendar用法随笔
平时在处理时间问题的时候,一般会想到用java.util.Date类型,在使用倒时间的运算的时候,就不是很方便,找找到了java.util.Calendar类,中文意思是“日历”,以下就是自己对这个类 ...
- 安装.Net Standard 2.0, Impressive
此版本的.NET Standard现在支持大约33K的API,与.NET Standard 1.x支持的14K API相比.好的是大部分API来自.NET Framework.这使得生活更容易将代码移 ...
- MobX基础 ----- 类的静态属性和装饰器
当我们使用MobX的时候,首先要声明一个store, 用来保存状态,它的最基本的语法 如下: class Todo { @observable title = ""; @obser ...
- 洛谷P2918 [USACO08NOV]买干草(一道完全背包模板题)
题目链接 很明显的一道完全背包板子题,做法也很简单,就是要注意 这里你可以买比所需多的干草,只要达到数量就行了 状态转移方程:dp[j]=min(dp[j],dp[j-m[i]]+c[i]) 代码如下 ...