前言:
去年的时候facebook开发工程师分享了了一个在facebook中实现的bigpipe的实现方案,如果您还不了解bigpipe方案解决的问题,请查看这个链接(http://www.facebook.com/note.php?note_id=389414033919)
当时,facebook的开发工程师主要对这个方案的目标,好处,以及总体的实现方法(没有很具体,只是一些思路)介绍了一下。听完facebook工程师的分享之后,感觉这个方案的确不错。它借鉴CPU上流水线的工作方式,用多线程异步运行的方式,将一个页面能够比原来更快的速度在客户端展现出来,可以让用户感觉不到页面延迟。
以上这幅图是传统的用户感知到页面加载时间延迟时间,主要由三部时间组成:
- 蓝色部分是服务端计算页面中动态内容所要花费的时间,
- 绿色部分是服务器端生成的内容传输到客户端所要花费的时间,
- 红色部分是页面在接收到服务器端传过来的内容之后调用css和javascript渲染页面所花费的时间
我们发现这三部分内容是线性发生的,就像软件设计中的瀑布模型一样,只有等到上游的事件完成了才会发生下游的事件。
如果采用bigpipe来设计页面的话,用户感知到的页面延迟时间就会如下图所示那样了:
一个大的页面可以分割成N个逻辑上没有耦合的paglet,每个paglet可以以自己的方式并行执行,每个paglet的生命周期也是由上面介绍的传统的三部分时间组成的。
bigpipe的使用场景是像facebook这样的个人页面是按照内容分块(pagelet)显示,并且,每块内容之间没有耦合性,并且要求内容显示要求实时性非常高的页面。这样的页面非常适合淘宝的“旺铺”,以及“我的淘宝”这样的页面使用。
下图是在“口碑店铺”中的使用场景:
红线框中是的内容是一个独立的paglet内容
实现:
实现的场景是采用java语言开发,采用tomcat6以下版本,采用j2ee框架协议进行编写,view模板采用JSP。
首先需要考虑的问题是,当服务器端相应用户请求之后需要以最快的速度将用户的框架skeleton 页面frush到用户客户端,这样用户会,这个骨架skeleton 中是不会有任何动态内容的,这样能够保证用户能在最快时间内看见一个框架页面。即使后期服务器处理很慢的话,也能保证用户可以在第一时间内看见一个大致的web页面,从而感觉不到网络延时。
骨架(skeleton )内容如下:
<html>
<head> <script src=“<big pipe js>” />…</head>
<body>…
<div id=“left_column”>
<div id=“pagelet_navigation”>
</div>
</div>
<div id=“middle_column”>
<div id=“big3” ></div>
</div>
</body>
</html>
从上面这段代码可以看到id为pagelet_navigation和big3这两个div,标签内容为空,具体内容会等到稍后服务器异步flush到pagelet的内容来填充的。
每次flush的内容会是以下的方式打印到页面的最末端:
<script>
bigpipeFun({id:'big3',html:‘<h1>hello</h1>’})
</script>
将会调用javascript函数bigpipeFun将id所在的内容替换成 html的属性值。当然在实际应用场景下这个html属性的内容会比较长,且复杂。
写一个测试用的Servlet,写一个doGet方法,在dispatcher.include(req, resp),加载骨架内容,再在response中通过writer要向客户端写点内容,来模拟一个paglet。
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 打印骨架内容
RequestDispatcher dispatcher = this.getServletContext()
.getRequestDispatcher("/skeleton.jsp");
dispatcher.include(req, resp);
// 模拟一个pagelet执行,并且在paglet中打印hello
final Writer writer = resp.getWriter();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Thread.sleep(1000);
writer.write(“hello”);
}
});
thread.start();
Thread.sleep(2000); // 这样可以显示“hello”,但是主线程退出必须要由子线程来控制
}
这个代码在上一部先用include的方式打印一个骨架页面,然后创建一个线程来模拟一个paglet,假设paglet执行需要1秒,当执行完成之后在页面上打印“hello” 作为paglet的内容,你会在方法体的最下面有一个 Thread.sleep(2000);的代码,这个是主线程,如果在这里不等待子线程paglet执行完成的话,子线程的打印的hello内容是不会在页面上打印出来的。因为当子线程开始执行的时候住线程如果不等待的话,主线程就
直接退出了,并且会将主response的输出流关闭。
改成这个样子的了,页面上基本上可以实现bigpipe的效果了。但是这个例子中还是有问题的,在doGET的末尾必须要加上
Thread.sleep(2000);
这行代码,不然的话,pageLet中打印的内容就不能在页面上显示了。 这个说明servlet这个主线程,必须要等待pagelet子线程执行完毕才能退出方法,不然的话当servlet退出doGet方法之后,会将response的writer对象属性关闭向客户端写的功能。所以,在这里主线程必须等待其他线程完成所有工作之后才能退出。
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
final Object lock = new Object();
final Writer writer = resp.getWriter();
Thread thread = new Thread(new Runnable() {
synchronized(lock){
@Override
public void run() {
Thread.sleep(1000);
writer.write(“hello”);
synchronize(lock ){
lock.notify();
}
}
});
thread.start();
lock.wait();
}
}
创建一个lock对象,在paglet中调用lock的notify()方法,在主线程中调用wait方法让主线程等待子线程来唤醒它(notify)。
注意:对lock对象的操作必须放置在synchronize代码块中。这样能够保证先执行wait()方法再执行notify();
这里特别要提一下wait和notify的区别:
- Wait是Object的成员方法,但是sleep是Thread的静态成员方法
- Wait需要notify唤醒,sleep会自己苏醒
- Wait会释放对象锁,而sleep不释放
之前我们讨论的页面都只有一个paglet,现在要考虑多个paglet的情况,所以需要重新设计一下class的设计:
因为,执行一个paglet的频率是非常高的,而且一个paglet的生命周期是非常短的,考虑到java创建一个thread是非常耗费资源的,所以在这里引入线程池的解决方案,来解决频繁创建线程耗费资源的问题。
ExecutorService threadPool = java.util.concurrent
.Executors. newCachedThreadPool();
threadPool.execute(new StorePagelet1(pageletCount , response));
ExecutorService就是一个线程池的实现。执行的时候只需要new一个 Runnable实例对象,调用ExecutorService的execute方法,ExecutorService会负责找到线程池中找闲置的线程对象来执行Runnable的task。Paglet类主要负责打印paglet中的内容,调用者只需要继承Paglet中,实现getContent方法,返回值是paglet需要显示的内容。
接下来的问题是:一个页面一般会由N个paglet构成,也就是一个主线程会由N个子线程构成,只有当这n个子线程都完成之后主线程才能退出。
需要设计一个计数器,来协调各个线程工作,只有等到所有子线程都退出了,主线程才能退出:
class PageletCount {
private int count;
public synchronized PageletCount add() {
count++;
return PageletCount.this;
}
public synchronized int decrease() {
return --count;
}
}
PageletCount的工作方式如下:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
final PageletCount count= new PageletCount ();
final Writer writer = resp.getWriter();
count.Add();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Thread.sleep(1000);
writer.write(“hello”);
synchronize(count){
if (this.pageletCount.decrease() < 1) {
count.notify();
} }
}
});
thread.start();
synchronize(count ){
count.wait();
}
}
至此,facebook bigpie的所有实现细节都讲到了。
下面是bigpipe的示例代码,如有需要的话可以下载:http://dl.iteye.com/topics/download/d4652e8b-4fa4-30d9-923e-32b327277c2e
参照:
- http://www.facebook.com/note.php?note_id=389414033919
- http://codemonkeyism.com/facebook-bigpipe-java/
- http://www.54chen.com/architecture/rose-pipe-http-54chen.html
- 大小: 22.5 KB
- 大小: 14.3 KB
- 大小: 16 KB
- 大小: 85.8 KB
分享到:
相关推荐
Java 实现 BigPipe 技术详解(上) 在 Web 应用开发中,为了提高页面加载速度和用户体验,BigPipe 技术应运而生。它最初由 Facebook 提出,目的是将一个大页面...后续的"Java实现BigPipe(下)"将进一步探讨这些问题。
在本文中,我们将探讨BigPipe的设计原理以及如何使用Java实现这一技术。 BigPipe的核心思想是分块加载(Partial Loading),它将页面分为多个逻辑单元,如头部、导航、主要内容和侧边栏等。每个逻辑单元作为一个...
在Java实现中,BigPipe技术可以支持单线程和多线程加载策略。单线程加载虽然简单,但可能会限制并行处理的能力,导致页面加载速度的上限。而多线程加载则能充分利用现代多核处理器的性能,将不同Pagelet的加载任务...
在Java Web环境中,结合`Struts2`框架,我们可以实现`BigPipe`技术,以提升基于`Struts2`应用的性能。 `BigPipe`的核心思想是将一个大的HTML页面拆分为多个小的部分(通常称为Pagelets),每个部分独立加载和渲染。...
Webx是一个基于Java的开源Web框架,而BigPipe则是Webx框架中的一个特性,用于实现快速、高效的页面分片加载。 在传统的网页加载过程中,浏览器会等待所有资源下载完毕后再一次性渲染页面,这可能导致用户看到空白页...
Struts2 是一个流行的 Java Web 框架,而 BigPipe 技术则是 Facebook 提出的一种优化页面加载速度的方法,它借鉴了服务器端渲染的优势,将大型页面拆分为多个小块,逐个加载,从而显著提高了页面的加载效率。...
"bigpipe前端技术"是Facebook提出的一种优化页面加载的技术,它的主要思想是将一个大的页面拆分成多个小的部分,然后逐步加载,这样可以显著提高用户体验,减少整体页面加载时间。在电子商务网站中,这种技术尤其...
- **Pipe支持:**类似于Facebook的BigPipe技术,优化页面加载速度。 - **上传文件:**提供文件上传功能的支持。 **3.2 DAO层:** - **基本配置与使用:**介绍如何配置和使用DAO层,实现数据访问操作。 - **SQLParm...
- **Portal/pipe**:类似于Facebook BigPipe的机制,支持多线程运行,提高页面加载速度。 - **相关URL**:http://code.google.com/p/paoding-rose/wiki/Rose_SJ ##### 10. Thrift - **功能简介**:Apache Thrift是...