充分利用多核优势,高效并行渲染页面--改造nutz使其成为支持并行计算的MVC框架 企业应用并行计算MVCBigPipe
首先我们来看一个场景:
此图为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是一个国产的开源框架,融合了MVC、Ioc、AOP、Dao诸多功能,且设计合理,预留了多处扩展点,用户可以很容易的动态给它增、减功能。因此今天就拿它来开刀。
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 秒。
思路3:BigPipe!
BigPipe 是 Facebook 提出的前端性能优化方案。2010 年初的时候,Facebook 的前端性能研究小组开始了他们的优化项目,经过了六个月的努力,成功的将个人空间主页面加载耗时由原来的5 秒减少为现在的2.5 秒。关于它的详细介绍大家可以baidu一下。
它的核心思路是先给浏览器输出页面的主体框架,之后服务器端再并行处理不同的pagelet 的内容,一个pagelet 内容生成好了,立刻将其flush 给浏览器。以此来加速页面。这是一种很棒的思路。
在方案2的基础上,我们只需要再做简单的修改即可实现。唯一的区别就是,我们不需要AsynProcessor处理器了,而是要把官方的 org.nutz.mvc.impl.processor.ViewProcessor 重新定制下即可。
具体的实现方法,留待下一讲再行发布。感兴趣的同学可以自行实现下。
上一篇: Mysql系列(九) 单引号与反引号
下一篇: 抓取的网页怎么存入mysql数据库