`
agapple
  • 浏览: 1584057 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

(业务层)异步并行加载技术分析和设计

阅读更多

背景

   前段时间在做应用的性能优化时,分析了下整体请求,profile看到90%的时间更多的是一些外部服务的I/O等待,cpu利用率其实不高,在10%以下。 单次请求的响应时间在50ms左右,所以tps也不会太高,测试环境压力测试过程,受限于环境因素撑死只能到200tps,20并发下。

 

I/O

目前一般的I/O的访问速度: L1 > L2 > memory -> disk or network
 
常见的IO: 
  1. nas上文件 (共享文件存储)
  2. output/xxx (磁盘文件)
  3. memcache client /  cat client  (cache服务)
  4. database (oracle , mysql)  (数据库)
  5. dubbo client  (外部服务)
  6. search client (搜索引擎)

思路

 

正因为考虑到I/O阻塞,长的外部环境单个请求处理基本都是在几十ms,刚开始的第一个思路是页面做ajax处理。 

 

使用ajax的几个缺陷:

 

  1. 功能代码需进行重构,按照页面需求进行分块处理。 一次ajax请求返回一块的页面数据
  2. 数据重复请求。因为代码是分块,两次ajax中获取的member对象等,可能就没法共用,会造成重复请求。
  3. ajax加载对seo不优化,公司还是比较注重seo,因为这会给客户带来流量价值,而且是免费的流量。
  4. ajax技术本身存在一些磕磕碰碰的点: 跨域问题,返回数据问题,超时处理等。
  5. 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()
}

 

解释下依赖关系,帖子中很多人理解上有一些偏差。

 

   B服务依赖A,指B服务的请求发起或者请求参数,取决于A的返回的结果。 那假定这个A返回结果是一个pojo bean,那依赖结果的概念是指依赖bean具体的属性。因为你直接依赖A,那只能是if(modelA == null)这样的一种关系, 一般更多情况是说我要看下if(modelA.level = 'PM') 

 

 

一种解决思路:

 

    半自动化处理。 任何异步并行加载的时机点,全都取决于代码编写的顺序。 如果有依赖关系的存在,比如例子中的B依赖A的结果,则B会阻塞等待至A的结果返回,最后A和B的处理就又回归到一个有顺序序的请求处理。

 

 

 

 

最后从技术实现上看: 
  从技术实现上说,在调用serviceA获取结果的时,我会直接返回一个假的LazyLoad产生的mock对象。此时对应的属性值全为null,在你具体依赖到该modelA的属性数据时,就会有一个阻塞等待,转为串行的过程。 

 

例子:

 

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请求总的响应时间就等于最长的依赖关系请求链的相应时间。 

 

(业务层)异步并行机制的优点:

  1. 继承了ajax异步加载的优点
  2. 增加了并行加载的特性

相比于ajax的其他优势:

  1. 同时不会对页面seo有任何的影响,页面输出时都是一次性输出html页面
  2. 减少了ajax异步发起的http请求
  3. 两块代码的资源不会存在重复请求,允许进行资源共享
从目前来看,异步并行机制还是有比较大的应用场景。具体是否能做到对一线开发者透明,以及对应业务的开发成本,那就得看一下具体的代码实现

实现

根据以上分析,分析核心模型:


 说明:
  1. 原本服务service。 这个不用多解释,就是原本存在的一些需要被代理的对象,比如DAO,rpc调用客户端等。
  2. 代理参数设置。 比如设置一些超时时间等
  3. 并行执行容器。 一个多线程处理的容器,执行并行加载
  4. 代理服务。  对服务service的一个包装过后的代理对象
  5. 代理服务Model 。  代理对象根据客户端的一些请求返回对应的代理Model,用于代理控制。
具体的类图设计:


 
说明: 
  1. AsyncLoadProxy就是模型中对应的代理服务
  2. AsyncLoadConfig就是模型中对应的代理参数设置
  3. 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();
 

一些扩展

   因为目前大家都比较喜欢于spring的ioc,aop等一些配置方式,类似于编程式事务和声明式事务。为方便以后使用,做了下扩展。
 

扩展一: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>
 
思考: 后续可考虑,基于通配符拦截对应的目标service。
 

扩展二: 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());

 

 

思考

 

  1. 基于cglib的技术局限,存在一些限制。比如final类,java原始类型等不支持异步并行。一点技术局限性
  2. 并行加载机制不适合于cpu密集性的应用,针对I/O密集型的应用效果会比较明显,设置好对应的并行加载容器。具体参数需要细细斟酌,进行相关的压力测试和分析。
  3. 对开发的一个嵌入性,需要考虑对应的Timeout机制,比如异常处理等。同样,我们也可以设置没有timeout(个人不太建议)

最后

具体的代码可以访问:  https://github.com/agapple/asyncload

 

 

 

ps : 大家有兴趣或者有更好的一些想法,可以一起讨论下。 至于其他语言的异步并行加载方案也可以一并讨论下,小成本大收益,何乐而不为呢!

 

 

 

说明一下:(2月24号新增的内容)

 

设计的初衷是追求尽量少的嵌入性,尽可能的对业务开发透明。所以会采用字节码增强的方式进行处理。

 

最理想的情况:

1. 普通开发人员完成业务编码开发,按照正常的方式,不需要关心异步加载这一套。

2. 最后项目的技术经理或者架构师,配置一份xml,决定哪些service的哪几个方法要进行异步并行加载。

3. 项目进入提测,回归测试等。

  • 大小: 7.9 KB
  • 大小: 3.5 KB
  • 大小: 12.9 KB
  • 大小: 79.9 KB
分享到:
评论
52 楼 mojunbin 2014-08-12  
不错,最近也在思考这一块问题,希望用在页面加速,不过感觉不好实现。

独立业务可以异步加载,但是数据载入页面,通过单通过服务端技术不好实现,需要配合js动态替换内容。

这种方式,还是将页面静态化,然后动态并行加载某个页面块比较好实现,利用ssi。
51 楼 agapple 2013-11-29  
pastore123 写道
有个疑问,假如页面有两个DIV A和B,页面发起Ajax请求时,是同步的?先请求A然后再B吗?


不同浏览器对于同各域名的异步加载有线程数限制,IE默认为2个
50 楼 pastore123 2013-11-29  
有个疑问,假如页面有两个DIV A和B,页面发起Ajax请求时,是同步的?先请求A然后再B吗?
49 楼 agapple 2012-07-16  
sodarfish 写道
一般业务逻辑中,调用外部服务都要知道是否调用成功了,然后再往下走。
如果是异步的方式,这种情况怎么处理呢


不知道你说的是否是这样的一种关系:
ModelA modela= a.method();
if(modela.isSuccess()) {
  ModelB modelb = b.method();
}


这个就是文中提到的服务依赖了,这时候程序需要变为一个串行操作。(modela.isSucess会是一个阻塞等待A执行完成,并返回结果。然后才会进行b服务的调用) 其实原理蛮简单 
48 楼 sodarfish 2012-07-13  
一般业务逻辑中,调用外部服务都要知道是否调用成功了,然后再往下走。
如果是异步的方式,这种情况怎么处理呢
47 楼 agapple 2011-11-08  
xiaoZ5919 写道
和spring的asynch异步任务原理相似。通过proxy实施拦截
Future result = this.asyncExecutor.submit(new Callable<Object>() {
public Object call() throws Exception {
try {
Object result = invocation.proceed();
if (result instanceof Future) {
return ((Future) result).get();
}
}
catch (Throwable ex) {
ReflectionUtils.rethrowException(ex);
}
return null;
}
});
我有一点concern,向LZ咨询。
threadpool会不会成为瓶颈?例如设置poolSize为10,当并发为100时,肯定会有一些请求在队列中排队。如加大poolsize,也会带来线程切换的cost?基于以上疑问能不能找到一个平衡点?


原理其实是相通的,就是想通过异步线程进行处理。
相比spring,AsyncLoad中多了一些融合,减少使用异步带来对于业务代码的嵌入性。用了一些字节码处理,扩展了一些spring,jdk的内容
46 楼 agapple 2011-11-08  
xiaoZ5919 写道
和spring的asynch异步任务原理相似。通过proxy实施拦截
Future result = this.asyncExecutor.submit(new Callable<Object>() {
public Object call() throws Exception {
try {
Object result = invocation.proceed();
if (result instanceof Future) {
return ((Future) result).get();
}
}
catch (Throwable ex) {
ReflectionUtils.rethrowException(ex);
}
return null;
}
});
我有一点concern,向LZ咨询。
threadpool会不会成为瓶颈?例如设置poolSize为10,当并发为100时,肯定会有一些请求在队列中排队。如加大poolsize,也会带来线程切换的cost?基于以上疑问能不能找到一个平衡点?


这个我的建议是构建一个2n+1的线程模型。你可以看下我的AsyncLoadExecutor。
2n是指BlockingQueue的size为pool size的两倍
1是指当BlockingQueue队列满了之后切换回串行执行的模式(这样就不会有线程切换的cost)
45 楼 xiaoZ5919 2011-11-08  
和spring的asynch异步任务原理相似。通过proxy实施拦截
Future result = this.asyncExecutor.submit(new Callable<Object>() {
public Object call() throws Exception {
try {
Object result = invocation.proceed();
if (result instanceof Future) {
return ((Future) result).get();
}
}
catch (Throwable ex) {
ReflectionUtils.rethrowException(ex);
}
return null;
}
});
我有一点concern,向LZ咨询。
threadpool会不会成为瓶颈?例如设置poolSize为10,当并发为100时,肯定会有一些请求在队列中排队。如加大poolsize,也会带来线程切换的cost?基于以上疑问能不能找到一个平衡点?
44 楼 agapple 2011-07-18  
waterdh 写道
不错的思路,想得很深入了。
我是显式编写并行加载的代码,不够优美和灵活。


不能做到完全透明,还是有些使用场景的局限。
至于在配置上,基本上可以和spring融合下了挺多功夫,有兴趣可以一起交流。
43 楼 waterdh 2011-07-18  
不错的思路,想得很深入了。
我是显式编写并行加载的代码,不够优美和灵活。
42 楼 agapple 2011-06-14  
Reset 写道
没大看明白,是不是AOP某个方法,使用Future 异步计算结果
然后代理这个方法的返回值,然后使用返回值时如果结果还没出来就阻塞...

整个设计是有点AIO的感觉,几句话概括就是这样的一种情况。
就是将原先同步的调用转成异步调用,减少系统的响应时间

从实际应用测试来看,还是比较靠谱的,提升20%左右,这个也取决于你原先的I/O瓶颈所占的百分比。
在公司内部应用场景也会比较多,逐步的在实施,googlecode上稳定版马上就可以出来了
41 楼 Reset 2011-06-14  
没大看明白,是不是AOP某个方法,使用Future 异步计算结果
然后代理这个方法的返回值,然后使用返回值时如果结果还没出来就阻塞...
40 楼 agapple 2011-05-04  
xgj1988 写道
tps 是什么东西


tps是Transactions per second 的简写,http://en.wikipedia.org/wiki/Transactions_per_second

主要是用于评价一个系统的性能,除此之外,还有并发数,响应时间,吞突量(throughput)等一些列指标,通过一个二维图表绘制这些数据,可以找到一个系统的负载瓶颈点。
39 楼 xgj1988 2011-05-03  
tps 是什么东西
38 楼 agapple 2011-02-25  
winit 写道
呵呵,谢谢及时回复,那我就不明白了,既然modelB的加载(bbb方法的执行)依赖于ModelA执行的结果,A没有返回之前,B是加载不了的,实质上还是串行吧?
result = modelA.xxx();
modelB.bbb(result);


可能表述的依赖没讲清楚。

A服务依赖B,指A服务的请求发起或者请求参数,取决于B的返回的结果。

那假定这个B是一个pojo bean,那依赖结果的概念是指依赖bean具体的属性。

从技术实现上说,在调用serviceA获取结果的时,我会直接返回一个假的LazyLoad产生的mock对象。此时对应的属性值全为null,在你具体依赖到该modelA的属性数据时,就会有一个阻塞等待,转为串行的过程。

不知道说明白了没? 貌似很多人对这块有一定的理解偏差。

最后再看你的modeB.bbb(result)。 你这时的代码应该还不产生一种依赖关系,所以这里还是不会有阻塞,直接过去
37 楼 winit 2011-02-25  
呵呵,谢谢及时回复,那我就不明白了,既然modelB的加载(bbb方法的执行)依赖于ModelA执行的结果,A没有返回之前,B是加载不了的,实质上还是串行吧?
result = modelA.xxx();
modelB.bbb(result);
36 楼 agapple 2011-02-25  
winit 写道
对于楼主说的这种类型的:
if(modelA.isOk()){//先依赖modelA的请求 
    modelB.getXXX() 

确实可以异步加载,但更常见的是如下依赖类型的:
result = modelA.xxx();
modelB.xxxxx(result);
对于这种异步没用吧?


因目前技术实现的局限性,如果服务接口返回的是java的原始类型,暂不支持异步并行加载,final对象后续可考虑使用jdk proxy处理,目前代码没支持。

modelB.xxxx(result); 这里如果你的result是个对象类型时Integer,String等也可以支持。
35 楼 winit 2011-02-25  
对于楼主说的这种类型的:
if(modelA.isOk()){//先依赖modelA的请求 
    modelB.getXXX() 

确实可以异步加载,但更常见的是如下依赖类型的:
result = modelA.xxx();
modelB.xxxxx(result);
对于这种异步没用吧?
34 楼 agapple 2011-02-24  
donglee 写道
大任务包含分支任务分解,可以参考下fork/join并行分解模型


根fork/join是两码事,fork/join主要用于处理一些主任务拆分成对等子任务。 而异步加载需要解决的是未知服务的并行加载,两个服务之间可能还存在数据上的依赖关系。
33 楼 agapple 2011-02-24  
cljhyjs 写道
agapple 写道
cljhyjs 写道
LZ的想法很有创意,但是考虑到如果把这块功能抽象,采用类似拦截机制的方法在业务服务层做封装,可能需要考虑的问题会很多。 毕竟抽象层无法判断各个方法之间的依赖关系,即使采用显式的等待方式,可能也会增加程序的复杂度,或者实现封装代码块的复杂度。
   建议采用半自动化方式,由开发人员自己决定是否将方法投入到异步并行的容器中。就像spring中的ECcache、jbosscache配置使用一样。


这个你可以看看主文中的代码例子。

因底层技术是采用字节码增强方式,包括service和model都会织入一定的代码,具体开发者是感觉不到的。就和正常的写代码是一样的方式,至于依赖关系的串行处理,也是植入代码内部的。

原先设计的理念就是尽量做到无嵌入,对开发透明。

理想的情况就是普通开发人员完成业务编码开发后,由项目的技术经理或者架构师,配置一份xml,决定哪些service的哪几个方法要进行异步并行加载。


这也是一种方法,何必不让开发者自己在applicationContext.xml中配置,决定哪些service以及方法需要异步并行加载呢?



支持的阿,你看下首页中关于 扩展一:AsyncLoadFactoryBean 描述这一块。

基于spring FactoryBean接口,配置方式就是纯spring ioc那一套,对哪个service的哪些方法进行异步加载。

相关推荐

Global site tag (gtag.js) - Google Analytics