主题:(业务层)异步并行加载技术分析和设计
背景
前段时间在做应用的性能优化时,分析了下整体请求,profile看到90%的时间更多的是一些外部服务的I/O等待,cpu利用率其实不高,在10%以下。 单次请求的响应时间在50ms左右,所以tps也不会太高,测试环境压力测试过程,受限于环境因素撑死只能到200tps,20并发下。
I/O
- nas上文件 (共享文件存储)
- output/xxx (磁盘文件)
- memcache client / cat client (cache服务)
- database (oracle , mysql) (数据库)
- dubbo client (外部服务)
- search client (搜索引擎)
思路
正因为考虑到I/O阻塞,长的外部环境单个请求处理基本都是在几十ms,刚开始的第一个思路是页面做ajax处理。
使用ajax的几个缺陷:
- 功能代码需进行重构,按照页面需求进行分块处理。 一次ajax请求返回一块的页面数据
- 数据重复请求。因为代码是分块,两次ajax中获取的member对象等,可能就没法共用,会造成重复请求。
- ajax加载对seo不优化,公司还是比较注重seo,因为这会给客户带来流量价值,而且是免费的流量。
- ajax技术本身存在一些磕磕碰碰的点: 跨域问题,返回数据问题,超时处理等。
- ajax处理需要有嵌入性,每个开发都需要按照ajax特有的规范或者机制进行编码,有一定的约束
顺着ajax的思路,是否有一种方式可以很好的解决I/O阻塞,并且又尽量的透明化,也不存在ajax如上的一些问题。
所以就有了本文的异步并行加载机制的研究。原理其实和ajax的有点类似:
一般ajax的请求:
- request就代表html页面的一次渲染过程
- 首先给页面的一块区域渲染一块空的div id=A内容和一块div id=B的内容
- 浏览器继续渲染页面的其他内容
- 在页面底部执行具体的js时,发起div id=A的请求,等A返回后填充对应的div内容,发起div id=B的请求,返回后同样填充。
说明:不同浏览器有不同的机制,默认执行js都是串行处理。
看一下异步并行机制的设计时序图:
说明: 结合ajax的思路,异步并行加载机制在原理设计上有点不同,就是针对ajax的请求发起都是并行的。
引入的问题:
但同样,引入并行加载的设计后,需要考虑的一个点就是如果A和B的数据之间是有一定的依赖关系时怎么处理。
例子
- if(modelA.isOk()){//先依赖modelA的请求
- modelB.getXXX()
- }
if(modelA.isOk()){//先依赖modelA的请求 modelB.getXXX() }
一种解决方案: 半自动化处理。 任何异步并行加载的时机点,全都取决于代码编写的顺序。 如果有依赖关系的存在,比如例子中的B依赖A的结果,则B会阻塞等待至A的结果返回,最后A和B的处理就又回归到一个有顺序序的请求处理。
例子:
- ModelA modelA = serviceA.getModel(); //1. 异步发起请求
- ModelB modelB = serviceB.getModel(); //2. 异步发起请求
- // 3. 此时serviceA和serviceB都在各自并行的加载model
- if(modelA.isOk()){//4. 此时依赖了modelA的结果,阻塞等待modeA的异步请求的返回
- ModelC modelC = servicec.getModel(); //5. 异步发起请求
- }
- // 6. 此时serviceB和serviceC都在各自并行的加载model
- ......
- modelB.xxxx() //7. 数据处理,modelB已经异步加载完成,此时不会阻塞等结果了
- modelC.xxxx() //8. 数据处理,modelB已经异步加载完成,此时不会阻塞等结果了
ModelA modelA = serviceA.getModel(); //1. 异步发起请求 ModelB modelB = serviceB.getModel(); //2. 异步发起请求 // 3. 此时serviceA和serviceB都在各自并行的加载model if(modelA.isOk()){//4. 此时依赖了modelA的结果,阻塞等待modeA的异步请求的返回 ModelC modelC = servicec.getModel(); //5. 异步发起请求 } // 6. 此时serviceB和serviceC都在各自并行的加载model ...... modelB.xxxx() //7. 数据处理,modelB已经异步加载完成,此时不会阻塞等结果了 modelC.xxxx() //8. 数据处理,modelB已经异步加载完成,此时不会阻塞等结果了
来看个对比图:
很明显,一次request请求总的响应时间就等于最长的依赖关系请求链的相应时间。
(业务层)异步并行机制的优点:
- 继承了ajax异步加载的优点
- 增加了并行加载的特性
相比于ajax的其他优势:
- 同时不会对页面seo有任何的影响,页面输出时都是一次性输出html页面
- 减少了ajax异步发起的http请求
- 两块代码的资源不会存在重复请求,允许进行资源共享
实现
![](http://dl.javaeye.com/upload/attachment/423120/9709610e-2f91-3ee5-858f-eaf280b35455.png)
说明:
- 原本服务service。 这个不用多解释,就是原本存在的一些需要被代理的对象,比如DAO,rpc调用客户端等。
- 代理参数设置。 比如设置一些超时时间等
- 并行执行容器。 一个多线程处理的容器,执行并行加载
- 代理服务。 对服务service的一个包装过后的代理对象
- 代理服务Model 。 代理对象根据客户端的一些请求返回对应的代理Model,用于代理控制。
![点击查看原始大小图片](http://dl.javaeye.com/upload/attachment/423136/260f4fae-30d5-3aec-bd8a-25dff6ea1292.png)
- AsyncLoadProxy就是模型中对应的代理服务
- AsyncLoadConfig就是模型中对应的代理参数设置
- AsyncLoadExecutor就是模型中对应的并行执行容器。
- AsyncLoadEnhanceProxy是目前代理服务的一种技术实现,基于cglib的动态代理。后续可以研究下javassist技术,据说性能上比cglib要高。
- AsyncLoadMethodMatch是针对参数设置的一个细化,类似于spring的aop的切面点(PointCut)的概念,在具体的切面上执行异步并行加载机制。
- AsyncLoadExecutor目前是采用了jdk1.5中cocurrent包的pool池技术。支持两个队列设置:running队列,就绪队列。 针对就绪队列满了后,提供REJECT(拒绝后续请求)/BLOCK(阻塞等待队列有空位置)两种处理模式。
- // 初始化config
- AsyncLoadConfig config = new AsyncLoadConfig(3 * 1000l);
- // 初始化executor
- AsyncLoadExecutor executor = new AsyncLoadExecutor(10, 100);
- executor.initital();
- // 初始化proxy
- AsyncLoadEnhanceProxy<AsyncLoadTestService> proxy = new AsyncLoadEnhanceProxy<AsyncLoadTestService>();
- proxy.setService(asyncLoadTestService);
- proxy.setConfig(config);
- proxy.setExecutor(executor);
- // 执行测试
- AsyncLoadTestService service = proxy.getProxy();
- AsyncLoadTestModel model1 = service.getRemoteModel("first", 1000); // 每个请求sleep 1000ms
- AsyncLoadTestModel model2 = service.getRemoteModel("two", 1000); // 每个请求sleep 1000ms
- AsyncLoadTestModel model3 = service.getRemoteModel("three", 1000); // 每个请求sleep 1000ms
- long start = 0, end = 0;
- start = System.currentTimeMillis();
- System.out.println(model1.getDetail());
- end = System.currentTimeMillis();
- want.number(end - start).greaterThan(500l); // 第一次会阻塞, 响应时间会在1000ms左右
- start = System.currentTimeMillis();
- System.out.println(model2.getDetail());
- end = System.currentTimeMillis();
- want.number(end - start).lessThan(500l); // 第二次不会阻塞,因为第一个已经阻塞了1000ms,并行加载已经完成
- start = System.currentTimeMillis();
- System.out.println(model3.getDetail());
- end = System.currentTimeMillis();
- want.number(end - start).lessThan(500l); // 第三次也不会阻塞,因为第一个已经阻塞了1000ms,并行加载已经完成
- // 销毁executor
- executor.destory();
// 初始化config AsyncLoadConfig config = new AsyncLoadConfig(3 * 1000l); // 初始化executor AsyncLoadExecutor executor = new AsyncLoadExecutor(10, 100); executor.initital(); // 初始化proxy AsyncLoadEnhanceProxy<AsyncLoadTestService> proxy = new AsyncLoadEnhanceProxy<AsyncLoadTestService>(); proxy.setService(asyncLoadTestService); proxy.setConfig(config); proxy.setExecutor(executor); // 执行测试 AsyncLoadTestService service = proxy.getProxy(); AsyncLoadTestModel model1 = service.getRemoteModel("first", 1000); // 每个请求sleep 1000ms AsyncLoadTestModel model2 = service.getRemoteModel("two", 1000); // 每个请求sleep 1000ms AsyncLoadTestModel model3 = service.getRemoteModel("three", 1000); // 每个请求sleep 1000ms long start = 0, end = 0; start = System.currentTimeMillis(); System.out.println(model1.getDetail()); end = System.currentTimeMillis(); want.number(end - start).greaterThan(500l); // 第一次会阻塞, 响应时间会在1000ms左右 start = System.currentTimeMillis(); System.out.println(model2.getDetail()); end = System.currentTimeMillis(); want.number(end - start).lessThan(500l); // 第二次不会阻塞,因为第一个已经阻塞了1000ms,并行加载已经完成 start = System.currentTimeMillis(); System.out.println(model3.getDetail()); end = System.currentTimeMillis(); want.number(end - start).lessThan(500l); // 第三次也不会阻塞,因为第一个已经阻塞了1000ms,并行加载已经完成 // 销毁executor executor.destory();
一些扩展
扩展一:AsyncLoadFactoryBean
类似于spring的ProxyFactoryBean的概念,基于spring FactoryBean接口实现。
配置事例:
- <!-- 并行加载容器-->
- <bean id="asyncLoadExecutor" class="com.alibaba.tpolps.common.asyncload.AsyncLoadExecutor" init-method="initital" destroy-method="destory">
- <property name="poolSize" value="10" /> <!-- 并行线程数 -->
- <property name="acceptCount" value="100" /> <!-- 就绪队列长度 -->
- <property name="mode" value="REJECT" /> <!-- 就绪队列满了以后的处理模式 -->
- </bean>
- <bean id="asyncLoadMethodMatch" class="com.alibaba.tpolps.common.asyncload.impl.AsyncLoadPerl5RegexpMethodMatcher" >
- <property name="patterns">
- <list>
- <value>(.*)RemoteModel(.*)</value>
- </list>
- </property>
- <property name="excludedPatterns"> <!-- 排除匹配方法 -->
- <list>
- <value>(.*)listRemoteModel(.*)</value>
- </list>
- </property>
- <property name="excludeOveride" value="false" />
- </bean>
- <bean id="asyncLoadConfig" class="com.alibaba.tpolps.common.asyncload.AsyncLoadConfig">
- <property name="defaultTimeout" value="3000" />
- <property name="matches">
- <map>
- <entry key-ref="asyncLoadMethodMatch" value="2000" /> <!-- 针对每个match设置超时时间 -->
- </map>
- </property>
- </bean>
- <!-- 异步加载模FactoryBean -->
- <bean id="asyncLoadTestFactoryBean" class="com.alibaba.tpolps.common.asyncload.impl.spring.AsyncLoadFactoryBean">
- <property name="target">
- <ref bean="asyncLoadTestService" /> <!-- 指定具体的服务 -->
- </property>
- <property name="executor" ref="asyncLoadExecutor" />
- <property name="config" ref="asyncLoadConfig" />
- </bean>
<!-- 并行加载容器--> <bean id="asyncLoadExecutor" class="com.alibaba.tpolps.common.asyncload.AsyncLoadExecutor" init-method="initital" destroy-method="destory"> <property name="poolSize" value="10" /> <!-- 并行线程数 --> <property name="acceptCount" value="100" /> <!-- 就绪队列长度 --> <property name="mode" value="REJECT" /> <!-- 就绪队列满了以后的处理模式 --> </bean> <bean id="asyncLoadMethodMatch" class="com.alibaba.tpolps.common.asyncload.impl.AsyncLoadPerl5RegexpMethodMatcher" > <property name="patterns"> <list> <value>(.*)RemoteModel(.*)</value> </list> </property> <property name="excludedPatterns"> <!-- 排除匹配方法 --> <list> <value>(.*)listRemoteModel(.*)</value> </list> </property> <property name="excludeOveride" value="false" /> </bean> <bean id="asyncLoadConfig" class="com.alibaba.tpolps.common.asyncload.AsyncLoadConfig"> <property name="defaultTimeout" value="3000" /> <property name="matches"> <map> <entry key-ref="asyncLoadMethodMatch" value="2000" /> <!-- 针对每个match设置超时时间 --> </map> </property> </bean> <!-- 异步加载模FactoryBean --> <bean id="asyncLoadTestFactoryBean" class="com.alibaba.tpolps.common.asyncload.impl.spring.AsyncLoadFactoryBean"> <property name="target"> <ref bean="asyncLoadTestService" /> <!-- 指定具体的服务 --> </property> <property name="executor" ref="asyncLoadExecutor" /> <property name="config" ref="asyncLoadConfig" /> </bean>
扩展二: AsyncLoadTemplate
基于模板模式,提供异步并行机制。可以编程方式指定进行异步并行加载的执行单元。 比如针对好几个service的调用合并为一次并行加载。
事例代码:
- AsyncLoadTestModel model2 = asyncLoadTemplate.execute(new AsyncLoadCallback<AsyncLoadTestModel>() {
- @Override
- public AsyncLoadTestModel doAsyncLoad() {
- // 总共sleep 2000ms
- return asyncLoadTestService.getRemoteModel("ljhtest", 1000);
- }
- });
- System.out.println(model2.getDetail());
AsyncLoadTestModel model2 = asyncLoadTemplate.execute(new AsyncLoadCallback<AsyncLoadTestModel>() { @Override public AsyncLoadTestModel doAsyncLoad() { // 总共sleep 2000ms return asyncLoadTestService.getRemoteModel("ljhtest", 1000); } }); System.out.println(model2.getDetail());
思考
- 基于cglib的技术局限,存在一些限制。比如final类,java原始类型等不支持异步并行。一点技术局限性
- 并行加载机制不适合于cpu密集性的应用,针对I/O密集型的应用效果会比较明显,设置好对应的并行加载容器。具体参数需要细细斟酌,进行相关的压力测试和分析。
- 对开发的一个嵌入性,需要考虑对应的Timeout机制,比如异常处理等。同样,我们也可以设置没有timeout(个人不太建议)
最后
具体的代码可以访问: http://code.google.com/p/asyncload/
几个单元测试例子:
AsyncLoadExecutorTest.java |
AsyncLoadFactoryBeanTest.java |
AsyncLoadMethodMatchTest.java |
AsyncLoadProxyTest.java |
AsyncLoadReturnClassTest.java |
AsyncLoadTemplateTest.java |
ps : 大家有兴趣或者有更好的一些想法,可以一起讨论下,站内PM我。 至于其他语言的异步并行加载方案也可以一并讨论下,小成本大收益,何乐而不为呢!