论坛首页 Java企业应用论坛

充分利用多核优势,高效并行渲染页面--改造nutz使其成为支持并行计算的MVC框架

浏览 14028 次
精华帖 (0) :: 良好帖 (2) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-07-04  

首先我们来看一个场景:

 


       此图为sohu 首页的一部分。

这个页面比较典型,其实只要是复杂一些的页面通常都由许多不同模块的数据内容组成。如该页面就包含了新闻、体育、娱乐、视频等内容。每一个模块都需要单独查询,然后将结果填充到页面的各个部分。

如果由你来实现这个页面,你会怎么写呢?

通常,我们的写法是 (struts2.0)

public String index() {
	//查询新闻
	newsList = service.queryNewsList();
	//查询体育
	sportsList = service.querySportsList();
	//查询视频
	videoList = service.queryVideoList();
	return SUCCESS;
}

 这样是可以完成任务的,但是每一个查询都必须等待上一个查询结束后才能开始。假设查询比较耗时(不考虑缓存的因素):新闻查了20秒,体育查了10秒,视频查了5秒,则总页面至少需要 35 秒后才能打开。

 

如果老板对性能要求较高,且 SQL优化已经起不到帮助作用,那么还有没有其它办法可以提高程序的效率呢?

 

对了,最简单的就是在页面中使用 iframe,将页面拆分成若干个子页面并行加载。这样每个部分执行快慢并不影响页面的整体打开速度,从总体上讲页面的加载速度提高了。但是过多的使用iframe,会带来许多负面影响。如session问题、内存占用问题、样式问题等等,而且页面的可维护性也将变差。因此这并不是一种好的解决方案。

或者也可以采用 ajax 动态获取各个部分并填充页面。但这样大大增加了前端的代码量及实现难度。

 

现在我们的服务器往往都是4核、甚至是8核的了,我们能不能充分利用多核优势在后台并行运算结果并统一渲染页面呢?

思路1:当用户请求到 indexAction 的时候,启动n个子线程分别查询不同模块数据。最终当所有线程处理结束后,合并结果,渲染页面。

使用多线程,就必须解决线程同步、加解锁等等问题。需要大大增加代码量,且要求程序员水平较高。如果一个项目中,程序员水平参差不齐,那么最好别采用这种方案,否则不知哪里的出了问题就会影响整个项目的质量。

 

思路2:让框架去干脏活、累活。当用户请求到 indexAction 的时候,我们利用框架的功能,动态将该请求模拟成为n个子action的请求,并在子action结束时自动合并结果,渲染页面。这样我们就通过框架虚拟出了iframe 的效果,同时避免了页面中使用 iframe 带来的问题,且降低了程序的复杂度。

采用这种方案,原有的项目无需大改,程序员也可以用熟悉的方法去开发,无需关注多线程及数据合并等等问题,可谓一举多得。

现在我们来改造 NutzMVC 框架,使其能支持并行计算功能。

Nutz是一个国产的开源框架,融合了MVCIocAOPDao诸多功能,且设计合理,预留了多处扩展点,用户可以很容易的动态给它增、减功能。因此今天就拿它来开刀。

Nutz地址为:http://code.google.com/p/nutz/ 有兴趣的同学可以去看看。下面的内容假设您已经对 Nutz有所了解。

为了在程序中虚拟出对 action 请求,需要用到 UrlMapping 类的实例。这个类在系统启动时会自动创建,但默认为 private 直接获取不到。因此需要自定义一个加载器,将UrlMapping 实例开放出来。

这个类很简单,只需重载下 NutLoading 即可。

 

 

 

/**
 * 缺省加载器的基础上公开 urlMaping 属性,使 MVC 运行时可以并行执行其它 URL
 * @author Gongqin(gongqin@gmail.com)
 */
public class ProLoading extends NutLoading {
	
	private static UrlMapping urlMaping = null;

	@Override
	public UrlMapping load(NutConfig config) {
		urlMaping = super.load(config);
		return urlMaping;
	}

	/**
	 * 返回 urlMaping 实例
	 * @return
	 */
	public static UrlMapping getUrlMaping() {
		return urlMaping;
	}
}
 

 

 

新增加一个注解,用于标识需要异步请求的子 action 地址

 

/**
 * 声明一个需要异步请求的 url 地址<br/>
 * 例如: 
 * <code>
 * @Asyn({"retb:/b", "retc:/c"})
 * </code> 
 * 对 /b 和 /c 请求的返回值会分别存入 req 的 retb 和 retc 的属性中。
 * 
 * @author Gongqin(gongqin@gmail.com)
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface Asyn {
	
	/**
	 * 需要异步请求的 url,支持同时进行多个请求<p/> 
	 * 每行的格式为 key:url 。运行完成后会将 key 做为主键存入 req 中去。如果没有写 key,则认为您已经自己把返回值做了处理
	 */
	String[] value();
	
	/**
	 * 异步执行的子action最长处理时间,默认为 30 秒
	 * @return
	 */
	int timeout() default 30;
	
}

 

 

 这里到了今天的核心:定制一个动作链处理器,用于解析注解,并异步执行子 action ,最后合并处理结果。

 

/**
 * 异步执行的动作链处理器
 * @author Gongqin(gongqin@gmail.com)
 */
public class AsynProcessor extends AbstractProcessor {

	/**
	 * 解析 Asyn 注解并异步执行子 action,最后合并处理结果
	 */
	@Override
	public void process(final ActionContext ac) throws Throwable {
		Asyn asyn = ac.getMethod().getAnnotation(Asyn.class);
		// 如果没有异步任务,则处理完毕后直接返回
		if (null == asyn || null == asyn.value() || asyn.value().length == 0) {
			doNext(ac);
			return;
		}

		// 开始处理异步任务
		ExecutorService executor = Executors.newCachedThreadPool();
		List<Future<Pair<Object>>> tasks = new ArrayList<Future<Pair<Object>>>(asyn.value().length);
		for (final String url : asyn.value()) {
			//增加一个异步请求
			tasks.add(executor.submit(new Callable<Pair<Object>>() {
				@Override
				public Pair<Object> call() throws Exception {
					if (url == null)
						return null;
					String key = null;
					String tourl = url;
					int pos = url == null ? 0 : url.indexOf(":");
					if (pos > 0) {
						key = url.substring(0, pos);
						tourl = url.substring(pos + 1);
					}
					ActionContext subAc = new ActionContext();
					subAc.setRequest(new PathInfoRequest(tourl, ac.getRequest())).setResponse(ac.getResponse());

					ActionInvoker ai = ProLoading.getUrlMaping().get(subAc);
					ai.invoke(subAc);
					return new Pair<Object>(key, subAc.getMethodReturn());
				}

			}));
		}

		doNext(ac);

		// 获取其它 action 的返回值,供页面渲染
		for (Future<Pair<Object>> future : tasks) {
			try {
				Pair<Object> ret = future.get(asyn.timeout(), TimeUnit.SECONDS);
				if (ret != null && !Strings.isBlank(ret.getName())) {
					ac.getRequest().setAttribute(ret.getName(), ret.getValue());
				}
			}
			catch (TimeoutException e) {
				//忽略处理超时的任务
			}
		}
	}
}

 

 发起虚拟请求时,要修改req的请求路径。默认的 HttpServletRequest 无法修改其 pathInfo 属性。为了虚拟出多个请求,因此对HttpServletRequest 进行了薄封装,使其允许修改 pathInfo

 

public class PathInfoRequest implements HttpServletRequest {
	//其它方法省略
	private String url;
	public void setPathInfo(String url) {
			this.url = url;
		}
		@Override
		public String getPathInfo() {
			return url;
	}
}

 

 

 

OK,这样就全部搞定了。不复杂吧。下面我们来测试一下。

重新定义一个处理器链的配置文件 default-chains.js,把异步执行的动作链处理器声明进去。

 

 

{
	"default" : {
		"ps" : [
		      "org.nutz.mvc.impl.processor.UpdateRequestAttributesProcessor",
		      "org.nutz.mvc.impl.processor.EncodingProcessor",
		      "org.nutz.mvc.impl.processor.ModuleProcessor",
		      "com.nutz.mvc.AsynProcessor", //自行实现的处理器
		      "org.nutz.mvc.impl.processor.ActionFiltersProcessor",
		      "org.nutz.mvc.impl.processor.AdaptorProcessor",
		      "org.nutz.mvc.impl.processor.MethodInvokeProcessor",
		      "org.nutz.mvc.impl.processor.ViewProcessor"
		      ],
		"error" : 'org.nutz.mvc.impl.processor.FailProcessor'
	}
}

 

 

 写个主模块,把自定义的加载器和处理器链声明进去。

 

/**
 * @author Gongqin(gongqin@gmail.com)
 */
//使用自定义的加载器,暴露 urlMaping
@LoadingBy(ProLoading.class)
@ChainBy(args={"com/nutz/mvc/default-chains.js"})
public class MainModule {
	
	/**
	 * 主 action,请求时自动产生两个子action的请求
	 */
	@At("/a")
	@Asyn({"retb:/b", "retc:/c"})
	@Ok("jsp:a")
	public String a(@Param("a") String str, HttpServletRequest req, HttpServletResponse resp) {
		System.out.println("接收参数a=" + str);
		//模拟耗时操作
		try {
			TimeUnit.SECONDS.sleep(1);
		}catch (InterruptedException e) {}
		return "请求到了 a";
	}
	
	@At("/b")
	@Ok("void")
	public String b(@Param("b") String str) {
		//子 action 也一样可以获取到表单提交的参数
		System.out.println("接收参数b=" + str);
		System.out.println("b处理完成");
		return "请求到了 b";
	}
	
	@At("/c")
	@Ok("void")
	public String c(@Param("c") String str) {
		System.out.println("接收参数c=" + str);
		System.out.println("c处理完成");
		return "请求到了 c";
	}
}

 

 

 

开始进行测试:

 

/**
 * @author Gongqin(gongqin@gmail.com)
 */
public class MvcTest extends AbstractMvcTest {

	@Override
	protected void initServletConfig() {
		servletConfig.addInitParameter("modules", "com.nutz.test.MainModule");
	}

	@Test
	public void test1() throws Throwable {
		//模拟一个 http 请求
		request.setPathInfo("/a");
		request.setParameter("a", "this_is_a");
		request.setParameter("b", "this_is_b");
		request.setParameter("c", "this_is_c");
		request.setMethod("GET");
		servlet.service(request, response);
		
		//验证返回值 
		Assert.assertEquals("obj属性返回不正确",  "请求到了 a", request.getAttribute("obj"));
		Assert.assertEquals("retb属性返回不正确", "请求到了 b", request.getAttribute("retb"));
		Assert.assertEquals("retc属性返回不正确", "请求到了 c", request.getAttribute("retc"));
	}
}

 

 

 

执行后通过日志可以看出,虽然我们只请求了 /a ,但框架自动将此请求分解到了 /b 和 /c 的 action 上,并且 /b 和 /c 比 /a 处理完成时间还早,说明它们是真正的并行执行了。

今后,您只需对action 声明一个注解,即可灵活的并行执行多个子action,最终统一渲染页面啦。

拿开头举的场景的例子来说,则总页面只需20 秒即可打开。

注意,虽然子action还可以再嵌套子 action ,但一定要避免循环嵌套。如果出现循环嵌套的情况,则只能等待处理超时了。默认的超时时间为 30 秒。

 

 

 

 

思路3BigPipe

BigPipe 是 Facebook 提出的前端性能优化方案。2010 年初的时候,Facebook 的前端性能研究小组开始了他们的优化项目,经过了六个月的努力,成功的将个人空间主页面加载耗时由原来的秒减少为现在的2.5 秒。关于它的详细介绍大家可以baidu一下。

它的核心思路是先给浏览器输出页面的主体框架,之后服务器端并行处理不同的pagelet 的内容,一个pagelet 内容生成好了,立刻将其flush 给浏览器。以此来加速页面。这是一种很棒的思路。

在方案2的基础上,我们只需要再做简单的修改即可实现。唯一的区别就是,我们不需要AsynProcessor处理器了,而是要把官方的 org.nutz.mvc.impl.processor.ViewProcessor 重新定制下即可。

具体的实现方法,留待下一讲再行发布。感兴趣的同学可以自行实现下。

  • 大小: 197.1 KB
   发表时间:2012-07-04  
nutz框架没用过,大致思路看明白些

有几个问题求教下:

这和ajax实现有什么不同吗?
一个是前台异步,
一个是后台分发,

意义在哪里?

和多核又有什么关系?
0 请登录后投票
   发表时间:2012-07-04  
Ajax 是通过用户的浏览器发起异步请求获取内容,也可以实现加快页面打开速度的目的。但是使用 ajax 技术存在以下几个方面的问题:
1、发起了多次 http 请求,增加了网络及服务器的压力。
2、增加了前端编写的难度。
3、ajax不适合传输大数据量的数据。因此你看sohu、sina等门户首页、新闻页等没有一个是这样干的。

而使用后台并行处理的好处:
1、充分利用框架优势,同普通程序一样编写,不会增加编程难度及工作量。
2、使用 JDK5 以后提供的线程池技术,将多个任务分布到多个子线程上去,可以充分利用服务器多核优势,提高性能。理论上改成 JDK7 的 ForkJoinPool 性能将会更高。
3、只需一次 http 请求,降低网络等压力。
0 请登录后投票
   发表时间:2012-07-04  
渲染页面的时候,其他核没事情做或者说是空闲的么
0 请登录后投票
   发表时间:2012-07-05  
資訊類網站應用IO操作是時間消耗大頭。除非請求處理需要大規模數值計算,不然對單一請求作並行處理意義不大,反爾會增加額外的工作,尤其是請求量很大的網站。
0 请登录后投票
   发表时间:2012-07-05  
楼主知道 freemarker吗, 这些不同的版块是定时生成的静态 html文件,而不是实时查询数据库
0 请登录后投票
   发表时间:2012-07-05  
多线程在多CPU或IO密集的情况下确实是提高性能的手段,但是服务器处理中很少有人这样做,其实是有原因的。
服务器的重要测量点是并发请求的处理能力,而不是单个请求,在对单个请求使用多线程的同时,其实占据了其它请求的cpu时间。单线程看似没有好好很好地利用多核,但是当请求并发时就不存在这个问题了。
0 请登录后投票
   发表时间:2012-07-05  
ExecutorService executor = Executors.newCachedThreadPool();

改成全局静态属性, 并使用Fixed的线程池, 效果会更好
0 请登录后投票
   发表时间:2012-07-05  
CPU的核心数量是有限的,当并发数上去后,把数据库操作放到多个线程,只是白白增加jvm线程调度的负担,增加了在线程池上排队的数量,况且更多的线程抢夺jdbc连接池资源,连接池锁的竞争激烈。当并发请求数超过cpu数量后,拆成多线程,并不会提高多少对浏览器的响应时间,而且还会下降。不信你可以测试看看。

要想降低响应时间,还是采用思路3比较靠谱。
0 请登录后投票
   发表时间:2012-07-05  
过多的异步会导致过多的线程的使用,过多的线程就会为使CPU过高,过度异步不见得会使最终效果好。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics