- 浏览: 40711 次
文章分类
最新评论
Java Web 高性能开发
http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf1/
http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf2/
图片压缩
减少图片的大小,可以明显的提高性能,而对于已有图片,要想减少图片的大小,只能改变图片的格式,这里推荐的是 PNG8 的格式,它可以在基本保持清晰度的情况下,减少图片的大小。知道这个原理以后,可以用 Windows 的画图工具、以及 PhotoShop 工具逐个的改变。但是这样做的缺点是单张处理,效率太慢。本文推荐一个在线转换工具 Smush.it,可以批量的进行压缩与转换。它的地址是:www.smushit.com/ysmush.it。打开后效果如下图所示。
图 1. Yahoo 提供的在线压缩工具
我们上传了一张大小为 3790K 的图片,待在线程序处理完毕后,点击 Download Smushed Images 下载查看结果。下载界面如下图所示。
图 2. 压缩后的结果
打开下载下来的压缩包,查看结果可以看到,图片从 3790 减少到了 3344,就如下图所示。对于大批量的图片网站,这个方法会帮助快速实现批量图片压缩。
图 3. 压缩后的结果
回页首
图像合并实现 CSS Sprites
CSS Sprites 是一个吸引人的技术,它其实就是把网页中一些背景图片整合到一张图片文件中,再利用 CSS 的“background-image”,“background- repeat”,“background-position”的组合进行背景定位,background-position 可以用数字能精确的定位出背景图片的位置。利用 CSS Sprites 能很好地减少网页的 HTTP 请求,从而大大的提高了页面的性能,这也是 CSS Sprites 最大的优点,也是其被广泛传播和应用的主要原因。CSS Sprites 能减少图片的字节,由于图像合并后基本信息不用重复,那么多张图片合并成 1 张图片的字节往往总是小于这些图片的字节总和。同时 CSS Sprites 解决了网页设计师在图片命名上的困扰,只需对一张集合的图片上命名就可以了,不需要对每一个小元素进行命名,从而提高了网页的制作效率。更换风格方便,只需要在一张或少张图片上修改图片的颜色或样式,整个网页的风格就可以改变。维护起来更加方便。同时,由于将图片合并到一张图片,因此图片的请求数就被缩减到 1 个。其他的请求都可以用到本地缓存,不需要访问服务器。下图是一个合并以后的图片。它将很多小图标都拼到了一起。
图 4. 合并后的图片
这里介绍一个小工具 ---“CSS Sprites 样式生成工具 2.0”,可以从 这里下载。这是一个简单免费的小工具,用该工具打开上面的图片,选中图片中的某块。如下图的“绿色大拇指”部分,工具会计算出这个部分的长、宽、距离左上角的距离。勾选复制类名、复制宽、复制高,再点击“复制当前样式”按钮。这样生成的样式会被复制到剪切板上。
图 5. 小工具的使用
生成的 CSS 代码如清单 1 所示。
清单 1. 小工具生成的 CSS 代码
.div_6148{width:18px;height:20px;background-position:-17px -209px;}
将这段代码运用在网页上,它的代码如下清单所示。
清单 2. 测试 CSS Sprites 代码
<html>
<head>
<style>
.div_6148
{
width:18px;
height:20px;
background-image:url(css-sprites-source.gif);
background-position:-17px -209px;
}
</style>
</head>
<body>
<div class="div_6148"></div>
</body>
</html>
打开测试网页显示结果如下图所示。
图 6. 测试网页效果
可以看到,网页只显示工具选择的“绿色大拇指”部分,这样的代码可以运用在网页的多个部分,而图片只需要下载一次,这就是该技术的最大优势,减少了因为小图片引起的多个请求。
回页首
多域名请求
有时候,图片数据太多,一些公司的解决方法是将图片数据分到多个域名的服务器上,这在一方面是将服务器的请求压力分到多个硬件服务器上。另一方面,是利用了浏览器的特性。一般来说,浏览器对于相同域名的图片,最多用 2-4 个线程并行下载。不同浏览器的并发下载数,都是不同的,并发数如下清单所示。
清单 3. 各浏览器的并发下载数
Browsers HTTP/1.1 HTTP/1.0
IE6,7 2 4
IE8 6 6
FireFox 2 2 8
FireFox 3 6 6
Safari 3,4 4 4
Chrome 1,2 6 ?
Chrome 3 4 4
Opera 9.63,10.00alpha 4 4
而相同域名的其他图片,则要等到其他图片下载完后才会开始下载。 这里我做了一个测试,选择了多个相同域名的图片在同一网页上。代码如清单 5 所示。
清单 4. 单域名的多图片下载
接下来,使用 FireFox 的 Firebug 插件监控网络。结果如下图所示。
图 7. 单域名多图片的监控效果
可以看到,相同域名的多张图片,它们下载的起始点是存在延迟的。它们并不是并行下载。当我们将其中的 3 张图片换成别的域名图片。如清单 6 所示。
清单 5. 多域名多图片下载
再次查看网络监控,可以看到,这些图片是并行下载的。
图 8. 多域名多图片测试结果
多域名的下载固然很好,但是太多域名并不太好,一般在 2-3 个域名下载就差不多。
回页首
图像的 BASE64 编码
不管如何,图片的下载始终都要向服务器发出请求,要是图片的下载不用向服务器发出请求,而可以随着 HTML 的下载同时下载到本地那就太好了。而目前,浏览器已经支持了该特性,我们可以将图片数据编码成 BASE64 的字符串,使用该字符串代替图像地址。假设用 S代表这个 BASE64 字符串,那么就可以使用 <img src="data:image/png;base64,S"> 来显示这个图像。可以看出,图像的数据包含在了 HTML 代码里,无需再次访问服务器。那么图像要如何编码成 BASE64 字符串呢?可以使用 在线的工具---“Base64 Online”,这个工具可以上传图片将图片转换为 BASE64 字符串。当然,如果读者有兴趣,完全可以自己实现一个 BASE64 编码工具,比如使用 Java 开发,它的代码就如清单 7 所示。
清单 6. BASE64 的 Java 代码
本文编码了一个图像,并且将编码获得的 BASE64 字符串,写到了 HTML 之中,如下清单 8 所示。
清单 7. 嵌入 BASE64 的测试 HTML 代码
由于图片数据包含在了 BASE64 字符串中,因此无需向服务器请求图像数据,结果显示如下图所示。
图 9. BASE64 显示图像
然而这种策略并不能滥用,它适用的情况是浏览器连接服务器的时间 > 图片下载时间,也就是发起连接的代价要大于图片下载,那么这个时候将图片编码为 BASE64 字符串,就可以避免连接的建立,提高效率。如果图片较大的话,使用 BASE64 编码虽然可以避免连接建立,但是相对于图像下载,请求的建立只占很小的比例,如果用 BASE64,对于动态网页来说图像缓存就会失效(静态网页可以缓存),而且 BASE64 字符串的总大小要大于纯图片的大小,这样一算就非常不合适了。因此,如果你的页面已经静态化,图像又不是非常大,可以尝试 BASE64 编码,客户端会将网页内容和图片的 BASE64 编码一起缓存;而如果你的页面是动态页面,图像还较大,每次都要下载 BASE64 字符串,那么就不能用 BASE64 编码图像,而正常引用图像,从而使用到浏览器的图像缓存,提高下载速度。从现实我们接触的角度看,如一些在线 HTML 编辑器,里面的小图标,如笑脸等,都使用到了 BASE64 编码,因为它们非常小,数量多,BASE64 可以帮助网页减少图标的请求数,提高效率。
回页首
GZIP 压缩
为了减少传输的数据,压缩是一个不错的选择,而 HTTP 协议支持 GZIP 的压缩格式,服务器响应的报头包含 Content-Encoding: gzip,它告诉浏览器,这个响应的返回数据,已经压缩成 GZIP 格式,浏览器获得数据后要进行解压缩操作。这在一定程度可以减少服务器传输的数据,提高系统性能。那么如何给服务器响应添加 Content-Encoding: gzip 报头,同时压缩响应数据呢?如果你用的是 Tomcat 服务器,打开 $tomcat_home$/conf/server.xml 文件,对 Connector 进行配置,配置如清单 9 所示。
清单 8. TOMCAT 配置清单
<Connector port ="80" maxHttpHeaderSize ="8192"
maxThreads ="150" minSpareThreads ="25" maxSpareThreads ="75"
enableLookups ="false" redirectPort ="8443" acceptCount ="100"
connectionTimeout ="20000" disableUploadTimeout ="true" URIEncoding ="utf-8"
compression="on"
compressionMinSize="2048"
noCompressionUserAgents="gozilla, traviata"
compressableMimeType="text/html,text/xml" />
我们为 Connector 添加了如下几个属性,他们意义分别是:
compression="on" 打开压缩功能
compressionMinSize="2048" 启用压缩的输出内容大小,这里面默认为 2KB
noCompressionUserAgents="gozilla, traviata" 对于以下的浏览器,不启用压缩
compressableMimeType="text/html,text/xml, image/png" 压缩类型
有时候,我们无法配置 server.xml,比如如果我们只是租用了别人的空间,但是它并没有启用 GZIP,那么我们就要使用程序启用 GZIP 功能。我们将需要压缩的文件,放到指定的文件夹,使用一个过滤器,过滤对这个文件夹里文件的请求。
清单 9. 自定义 Filter 压缩 GZIP
该程序的主体思想,是在响应流写回之前,对响应的字节数据进行 GZIP 压缩,因为并不是所有的浏览器都支持 GZIP 解压缩,如果浏览器支持 GZIP 解压缩,会在请求报头的 Accept-Encoding 里包含 gzip。这是告诉服务器浏览器支持 GZIP 解压缩,因此如果用程序控制压缩,为了保险起见,还需要判断浏览器是否发送 accept-encoding: gzip 报头,如果包含了该报头,才执行压缩。为了验证压缩前后的情况,使用 Firebug 监控请求和响应报头。
清单 10. 压缩前请求
GET /testProject/gzipCategory/test.html HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
Host: localhost:9090
Connection: Keep-Alive
清单 11. 不压缩的响应
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: W/"5060-1242444154000"
Last-Modified: Sat, 16 May 2009 03:22:34 GMT
Content-Type: text/html
Content-Length: 5060
Date: Mon, 18 May 2009 12:29:49 GMT
清单 12. 压缩后的响应
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: W/"5060-1242444154000"
Last-Modified: Sat, 16 May 2009 03:22:34 GMT
Content-Encoding: gzip
Content-Type: text/html
Content-Length: 837
Date: Mon, 18 May 2009 12:27:33 GMT
可以看到,压缩后的数据比压缩前数据小了很多。压缩后的响应报头包含 Content-Encoding: gzip。同时 Content-Length 包含了返回数据的大小。GZIP 压缩是一个重要的功能,前面提到的是对单一服务器的压缩优化,在高并发的情况,多个 Tomcat 服务器之前,需要采用反向代理的技术,提高并发度,而目前比较火的反向代理是 Nginx(这在后续的文章会进行详细的介绍)。对 Nginx 的 HTTP 配置部分里增加如下配置。
清单 13. Nginx 的 GZIP 配置
gzip on;
gzip_min_length 1000;
gzip_buffers 4 8k;
gzip_types text/plain application/x-javascript text/css text/html application/xml;
由于 Nginx 具有更高的性能,利用该配置可以更好的提高性能。在高性能服务器上该配置将非常有用。
回页首
懒加载与预加载
预加载和懒加载,是一种改善用户体验的策略,它实际上并不能提高程序性能,但是却可以明显改善用户体验或减轻服务器压力。
预加载原理是在用户查看一张图片时,就将下一张图片先下载到本地,而当用户真正访问下一张图片时,由于本地缓存的原因,无需从服务器端下载,从而达到提高用户体验的目的。为了实现预加载,我们可以实现如下的一个函数。
清单 14. 预加载函数
上面的代码,首先定义了 Image 对象,并且声明了需要预加载的图像数组,然后逐一的开始加载(.src=images[i])。如果已经在缓存里,则不做其他处理;如果不在缓存,监听 onload 事件,它会在图片加载完毕时调用。
而懒加载则是在用户需要的时候再加载。当一个网页中可能同时有上百张图片,而大部分情况下,用户只看其中的一部分,如果同时显示上百张,则浪费了大量带宽资源,因此可以当用户往下拉动滚动条时,才去请求下载被查看的图像,这个原理与 word 的显示策略非常类似。
在 JavaScript 中,它的基本原理是首先要有一个容器对象,容器里面是 img 元素集合。用隐藏或替换等方法,停止 img 的加载,也就是停止它去下载图像。然后历遍 img 元素,当元素在加载范围内,再进行加载(也就是显示或插入 img 标签)。加载范围一般是容器的视框范围,即浏览者的视觉范围内。当容器滚动或大小改变时,再重新历遍元素判断。如此重复,直到所有元素都加载后就完成。当然对于开发来讲,选择已有的成熟组件,并不失为一个上策,Lazy Load Plugin for jQuery 是基于 JQuery 的懒加载组件,它有自己的 官方网站。这是一个不错的免费插件。可以帮助程序员快速的开发懒加载应用。
Flush 机制的使用
实际上在 Web 技术中,Flush 机制并不新鲜,它的思想是无需等到网页内容全部加载完毕,一次性写回客户端,而是可以部分逐次的返回。如果网页很大的话,一次性写回全部内容显然是个不明智的选择,因为这会造成网页的长时间空白。Flush 机制允许开发人员将网页的内容按文档流顺序逐步返回给客户端,这样可以使得用户知道我们的系统正在工作,只是等待的时间稍长而已,这样用户也会“心甘情愿”的等下去。Flush 机制是一个经典的提高用户体验的方法,至今也一直在用。如果网页很大,这个机制也是建议使用的。在 Java Web 技术中,实现 Flush 非常简单,只要调用 HttpServletResponse.getWriter 输出流的 flush 方法,就可以将已经完成加载的内容写回给客户端。
但是是否每个网页都要使用该技术呢?笔者当然不这么建议。将网页内容加载完毕后再一次性返回客户端也有它的好处。我们知道网络传输也有最大的传输单元,内容加载完毕后一次性输出就可以最大程度的利用传输的带宽,减少分块,减少传输次数,也就是说实际上 Flush 机制会增加用户等待时间、增加浏览器渲染时间,但是对于大网页来说,降低这点效率来增强用户体验,是值得的。
回页首
动静分离
所谓的动静分离,就是将 Web 应用程序中静态和动态的内容分别放在不同的 Web 服务器上,有针对性的处理动态和静态内容,从而达到性能的提升。本文基于 Java Web 来讲解 Web 优化,而 Java Web 的主流服务器软件是 Tomcat。让人遗憾的是,Tomcat 在并发和静态资源处理的能力上较弱,这也是 Tomcat 为人诟病的地方。但是瑕不掩瑜,既然我们选择了 Java Web,那么就应该发挥我们程序员的头脑去想方设法的提高性能。而动静分离就是其中一种方法,既然 Tomcat 处理静态资源的能力较弱,那就将静态资源的处理任务交给适合的软件,而让 Tomcat 专注于处理 JSP/Servlet 的请求。
对于静态资源处理的服务器软件,我们可以选择 Nginx,它是一款俄罗斯人开发的软件,似乎比 Apache 更加优秀。它支持高并发,对静态资源处理的能力较强,这正是我们想要的不是吗?事实上,动静分离的方案很多,有人采用 Apache+Tomcat 的组合;也有人使用 Tomcat+Tomcat 的组合,不过两个 Tomcat 分别被放置于不同的主机,不同的域名。其中 Apache+Tomcat 的方案与 Nginx 的方案原理上是一样的,它们都是基于反向代理,相对于使用 Nginx 配置动静分离,Apache 的配置就显得略微复杂一些。在 Apache 里,mod_proxy 模块负责反向代理的实现。其中核心配置内容如清单 1 所示,该配置属于本人参与某项目的其中一部分。
清单 1. 动静分离的 Apache 核心配置
从 Apache 官方对 mod_proxy 模块的介绍,我们可以知道 ProxyPass 属性可以将一个远端服务器映射到本地服务器的 URL 空间中,也就是说这是一个地址映射功能。在清单 1 的配置中,当访问的路径不在 /static/ 下时(!表示非),就转发给后端的服务器(也就是 Tomcat);否则如果是 /static/ 路径就访问本机。例如,当访问 www.xuanli365.com/static/css/index.css 时,实际处理请求的是 Apache 服务器,而访问 www.xuanli365.com/index.jsp,那么 Apache 会将请求转发到后端的 Tomcat 服务器,实际访问的页面是 http:// 192.168.1.178( 或 145):8080/index.jsp,这就实现了动静分离。在清单 1 的配置中实际也包含了简单的负载均衡(loadfactor 因子)。
事实上,我们可以随便打开一个大型门户网站来看一下,我打开的是腾讯网站,任意查看其中两张图片的地址,我发现一个是:http://mat1.gtimg.com/www/iskin960/qqcomlogo.png,而另一个则是:http://img1.gtimg.com/v/pics/hv1/95/225/832/54158270.jpg。可见该网站存放图片资源使用了多个的域名,我们再用 Linux 的 host 命令查看两个域名的 IP 地址,结果如图 1 所示。
图 1. 某网站的动静分离
可以看到,通过查看 IP 地址,我们发现这些图片很可能存放在不同的主机上(为什么是很可能?因为一个主机可以拥有多个 IP),而图片内容和网页的动态内容并不在同一 IP 下,也很可能是动静分离。多个域名在前面也已经提到,可以增加浏览器的并发下载数,提高下载效率。
本文采用另一种策略对动静分离进行演示,它的大致结构如图 2 所示。
图 2. 本文设计的动静分离结构
在本文中,我们将静态资源放在 A 主机的一个目录上,将动态程序放在 B 主机上,同时在 A 上安装 Nginx 并且在 B 上安装 Tomcat。配置 Nginx,当请求的是 html、jpg 等静态资源时,就访问 A 主机上的静态资源目录;当用户提出动态资源的请求时,则将请求转发到后端的 B 服务器上,交由 Tomcat 处理,再由 Nginx 将结果返回给请求端。
提到这,可能有您会有疑问,动态请求要先访问 A,A 转发访问 B,再由 B 返回结果给 A,A 最后又将结果返回给客户端,这是不是有点多余。初看的确多余,但是这样做至少有 2 点好处。第一,为负载均衡做准备,因为随着系统的发展壮大,只用一台 B 来处理动态请求显然是是不够的,要有 B1,B2 等等才行。那么基于图 2 的结构,就可以直接扩展 B1,B2,再修改 Nginx 的配置就可以实现 B1 和 B2 的负载均衡。第二,对于程序开发而言,这种结构的程序撰写和单台主机没有区别。我们假设只用一台 Tomcat 作为服务器,那么凡是静态资源,如图片、CSS 代码,就需要编写类似这样的访问代码:<img src=”{address of A}/a.jpg”>,当静态资源过多,需要扩展出其他的服务器来安放静态资源时,访问这些资源就可能要编写这样的代码:<img src=”{address of C}/a.jpg”>、<img src=”{address of D}/a.jpg”>。可以看到,当服务器进行变更或扩展时,代码也要随之做出修改,对于程序开发和维护来说非常困难。而基于上面的结构,程序都只要 <img src=”a.jpg”>,无需关心具体放置资源的服务器地址,因为具体的地址 Nginx 为帮您绑定和选择。
按照图 2 所示的架构图,安装好需要的软件 Nginx 和 Tomcat。按照设想,对 Nginx 的配置文件 nginx.conf 进行配置,其中与本文该部分相关的配置如清单 2 所示。
清单 2. 动静分离的 Nginx 配置
清单 2 十分简洁,其目的和我们预期的一样,动态的请求(以 .jsp 结尾)发到 B(192.168.1.117:8080,即 tomcat_server)上,而静态的请求(gif|jpg 等)则直接访问定义的 root(/home/wq243221863/Desktop/ROOT)目录。这个 root 目录我直接将其放到 Linux 的桌面 ROOT 文件夹。
接下来在 Tomcat 中新建 Web 项目,很简单,我们只为其添加一个 test.jsp 文件,目录结构如图 3 所示。
图 3. B 上的测试项目结构
而我们定义了一张测试用的静态图片,放置在 A 的桌面 ROOT/seperate 目录下。结构如图 4 所示
图 4. A 上的静态资源文件夹结构
注意:这里的 separate 目录名是与 B 的项目文件夹同名的。
再查看图 3 中的 test.jsp 的源码。如清单 3 所示。
清单 3. test.jsp 源码
清单 3 是一个非常简单的 JSP 页面,主要是使用 img 标签来访问 jquery.gif,我们知道 test.jsp 在 B 服务器上,而 jquery.gif 在 A 服务器上。用于访问 jquery.gif 的代码里不需要指定 A 的地址,而是直接使用相对路径即可,就好像该图片也在 B 上一样,这就是本结构的一个优点了。我们在 A 上访问 test.jsp 文件。结果如图 5 所示。
图 5. test.jsp 的结果
非常顺利,完全按照我们的想法实现了动静分离!
我们初步完成了动静分离的配置,但是究竟动静分离如何提高我们的程序性能我们还不得而知,我们将 Tomcat 服务器也迁移到 A 服务器上,同时将 jquery.gif 拷贝一份到 separate 项目目录下,图 3 的结构变为图 6 所示。
图 6. 拷贝 jquery.gif 的 separate 项目
我们将 Tomcat 的端口设置为 8080,Nginx 的端口依然是 9090。现在访问 http://localhost:9090/separate/test.jsp(未使用动静分离)和访问 http://localhost:8080/separate/test.jsp(使用了动静分离)的效果是一样的了。只是 8080 端口的静态资源由 Tomcat 处理,而 9090 则是由 Nginx 处理。我们使用 Apache 的 AB 压力测试工具,对 http://localhost:8080/seperate/jquery.gif、http://localhost:9090/seperate/jquery.gif、http://localhost:8080/seperate/test.jsp、http://localhost:9090/seperate/test.jsp 分别进行压力和吞吐率测试。
首先,对静态资源(jquery.gif)的处理结果如清单 4 所示。
清单 4. 静态资源的 AB 测试
清单 4 的测试脚本代表同时处理 100 个请求并下载 1000 次 jquery.gif 文件,您可以只关注清单 4 的粗体部分(Requests per second 代表吞吐率),从内容上就可以看出 Nginx 实现动静分离的优势了,动静分离每秒可以处理 2267 个请求,而不使用则只可以处理 535 个请求,由此可见动静分离后效率的提升是显著的。
您还会关心,动态请求的转发,会导致动态脚本的处理效率降低吗?降低的话又降低多少呢?因此我再用 AB 工具对 test.jsp 进行测试,结果如清单 5 所示。
清单 5. 动态脚本的 AB 测试
经过笔者的多次测试,得出了清单 5 的较为稳定的测试结果,可以看到在使用 Nginx 实现动静分离以后,的确会造成吞吐率的下降,然而对于网站整体性能来说,静态资源的高吞吐率,以及未来可以实现的负载均衡、可扩展、高可用性等,该牺牲我想也应该是值得的。
我想任何技术都是有利有弊,动静分离也是一样,选择了动静分离,就选择了更为复杂的系统架构,维护起来在一定程度会更为复杂和困难,但是动静分离也的确带来了很大程度的性能提升,这也是很多系统架构师会选择的一种解决方案。
回页首
HTTP 持久连接
持久连接(Keep-Alive)也叫做长连接,它是一种 TCP 的连接方式,连接会被浏览器和服务器所缓存,在下次连接同一服务器时,缓存的连接被重新使用。由于 HTTP 的无状态性,人们也一直很清楚“一次性”的 HTTP 通信。持久连接则减少了创建连接的开销,提高了性能。HTTP/1.1 已经支持长连接,大部分浏览器和服务器也提供了长连接的支持。
可以想象,要想发起长连接,服务器和浏览器必须共同合作才可以。一方面浏览器要保持连接,另一方面服务器也不会断开连接。也就是说要想建立长连接,服务器和浏览器需要进行协商,而如何协商就要靠伟大的 HTTP 协议了。它们协商的结构图如图 7 所示。
图 7. 长连接协商
浏览器在请求的头部添加 Connection:Keep-Alive,以此告诉服务器“我支持长连接,你支持的话就和我建立长连接吧”,而倘若服务器的确支持长连接,那么就在响应头部添加“Connection:Keep-Alive”,从而告诉浏览器“我的确也支持,那我们建立长连接吧”。服务器还可以通过 Keep-Alive:timeout=10, max=100 的头部告诉浏览器“我希望 10 秒算超时时间,最长不能超过 100 秒”。
在 Tomcat 里是允许配置长连接的,配置 conf/server.xml 文件,配置 Connector 节点,该节点负责控制浏览器与 Tomcat 的连接,其中与长连接直接相关的有两个属性,它们分别是:keepAliveTimeout,它表示在 Connector 关闭连接前,Connector 为另外一个请求 Keep Alive 所等待的微妙数,默认值和 connectionTimeout 一样;另一个是 maxKeepAliveRequests,它表示 HTTP/1.0 Keep Alive 和 HTTP/1.1 Keep Alive / Pipeline 的最大请求数目,如果设置为 1,将会禁用掉 Keep Alive 和 Pipeline,如果设置为小于 0 的数,Keep Alive 的最大请求数将没有限制。也就是说在 Tomcat 里,默认长连接是打开的,当我们想关闭长连接时,只要将 maxKeepAliveRequests 设置为 1 就可以。
毫不犹豫,首先将 maxKeepAliveRequests 设置为 20,keepAliveTimeout 为 10000,通过 Firefox 查看请求头部(这里我们访问上面提到的 test.jsp)。结果如图 8 所示。
图 8. 服务器打开长连接
接下来,我们将 maxKeepAliveRequests 设置为 1,并且重启服务器,再次请求网页后查看的结果如图 9 所示。
图 9. 服务器关闭长连接
对比可以发现,Tomcat 关闭长连接后,在服务器的请求响应中,明确标识了:Connection close, 它告诉浏览器服务器并不支持长连接。那么长连接究竟可以带来怎么样的性能提升,我们用数据说话。我们依然使用 AB 工具,它可以使用一个 -k 的参数,模拟浏览器使用 HTTP 的 Keep-Alive 特性。我们对 http://localhost:8080/seperate/jquery.gif 进行测试。测试结果如清单 6 所示。
清单 6. AB 测试长连接
结果一定会让您大为惊讶,使用长连接和不使用长连接的性能对比,对于 Tomcat 配置的 maxKeepAliveRequests 为 50 来说,竟然提升了将近 5 倍。可见服务器默认打开长连接是有原因的。
回页首
HTTP 协议的合理使用
很多程序员都将精力专注在了技术实现上,他们认为性能的高低完全取决于代码的实现,却忽略了已经成型的某些规范、协议、工具。最典型的就是在 Web 开发上,部分开发人员没有意识到 HTTP 协议的重要性,以及 HTTP 协议可以提供程序员另一条性能优化之路。通过简单的在 JSP 的 request 对象中添加响应头部,往往可以迅速提升程序性能,一切实现代码仿佛都成浮云。本系列文章的宗旨也在于让程序员编最少的代码,提升最大的性能。
本文提出一个这样的需求,在文章前面部分提到的 test.jsp 中,它的一部分功能是显示服务器的当前时间。现在我们希望这个动态网页允许被浏览器缓存,这似乎有点不合理,但是在很多时候,虽然是动态网页,但是却只执行一次(比如有些人喜欢将网页的主菜单存入数据库,那么他肯定不希望每次加载菜单都去读数据库)。浏览器缓存带来的性能提升已经众人皆知了,而很多人却并不知道浏览器的缓存过期时间、缓存删除、什么页面可以缓存等,都可以由我们程序员来控制,只要您熟悉 HTTP 协议,就可以轻松的控制浏览器。
我们访问上面提及的 test.jsp。用 Firebug 查看请求情况,发现每次请求都会重新到服务器下载内容,这不难理解,因此 test.jsp 是动态内容,每次服务器必须都执行后才可以返回结果 , 图 10 是访问当前的 test.jsp 的头部情况。现在我们往 test.jsp 添加清单 7 的内容。
清单 7. 在 test.jsp 的首部添加的代码
上述代码的意图是:服务器获得浏览器请求头部中的 If-Modified-Since 时间,这个时间是浏览器询问服务器,它所请求的资源是否过期,如果没过期就返回 304 状态码,告诉浏览器直接使用本地的缓存就可以,
图 10. 修改 test.jsp 前的访问头部情况
修改完 test.jsp 代码后,使用鼠标激活浏览器地址栏,按下回车刷新页面。这次的结果如图 11 所示。
图 11. 修改 test.jsp 后的首次访问
可以看到图 11 和图 10 的请求报头没有区别,而在服务器的响应中,图 11 增加了 Last-Modified 头部,这个头部告诉浏览器可以将此页面缓存。
按下 F5(必须是 F5 刷新),F5 会强制 Firefox 加载服务器内容,并且发出 If-Modified-Since 头部。得到的报头结果如图 12 所示 .
图 12. 修改 test.jsp 后的再次访问
可以看到,图 12 的底部已经提示所有内容都来自缓存。浏览器的请求头部多出了 If-Modified-Since,以此询问服务器从缓存时间起,服务器是否对资源进行了修改。服务器判断后发现没有对此资源(test.jsp)修改,就返回 304 状态码,告诉浏览器可以使用缓存。
我们在上面的实验中,用到了 HTTP 协议的相关知识,其中涉及了 If-Modified-Since、Last-Modified、304 状态码等,事实上与缓存相关的 HTTP 头部还有许多,诸如过期设置的头部等。熟悉了 HTTP 头部,就如同学会了如何与用户的浏览器交谈,也可以利用协议提升您的程序性能。这也是本文为何一直强调 HTTP 协议的重要性。那么对于 test.jsp 这个小网页来说,基于缓存的方案提升了多少性能呢?我们用 AB 给您答案。
AB 是个很强大的工具,他提供了 -H 参数,允许测试人员手动添加 HTTP 请求头部,因此测试结果如清单 8 所示。
清单 8. AB 测试 HTTP 缓存
测试脚本:ab -c 1000 – n 10000 – H ‘ If-Modified-Since: Sun, 05 Jun 3910 00:00:00 GMT ’ http://localhost:8080/seperate/test.jsp
分别对比 Document Length、Requests per second 以及 Transfer rate 这三个指标。可以发现没使用缓存的 Document Length(下载内容的长度)是 362 字节,而使用了缓存的长度为 0。在吞吐率方面,使用缓存是不使用缓存的 3 倍左右。同时在传输率方面,缓存的传输率比没缓存的小。这些都是用到了客户端缓存的缘故。
回页首
CDN 的使用
CDN 也是笔者最近才了解和接触到的东西,耳中也是多次听到 CDN 这个词了,在淘宝的前端技术报告上、在一个好朋友的创新工场创业之路上,我都听到了这个词,因此我想至少有必要对此技术了解一下。所谓的 CDN,就是一种内容分发网络,它采用智能路由和流量管理技术,及时发现能够给访问者提供最快响应的加速节点,并将访问者的请求导向到该加速节点,由该加速节点提供内容服务。利用内容分发与复制机制,CDN 客户不需要改动原来的网站结构,只需修改少量的 DNS 配置,就可以加速网络的响应速度。当用户访问了使用 CDN 服务的网站时,DNS 域名服务器通过 CNAME 方式将最终域名请求重定向到 CDN 系统中的智能 DNS 负载均衡系统。智能 DNS 负载均衡系统通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的节点地址提供给用户,使用户可以得到快速的服务。同时,它还与分布在不同地点的所有 CDN 节点保持通信,搜集各节点的健康状态,确保不将用户的请求分配到任何一个已经不可用的节点上。而我们的 CDN 还具有在网络拥塞和失效情况下,能拥有自适应调整路由的能力。
由于笔者对 CDN 没有亲身实践,不便多加讲解,但是各大网站都在一定程度使用到了 CDN,淘宝的前端技术演讲中就提及了 CDN,可见 CDN 的威力不一般。
图 12. 淘宝的 CDN 前端优化
因此 CDN 也是不得不提的一项技术,国内有免费提供 CDN 服务的网站:http://www.webluker.com/,它需要您有备案的域名,感兴趣的您可以去试试。
回页首
小结
本文总结了 HTTP 长连接、动静分离、HTTP 协议等等,在您需要的时候,可以查看本文的内容,相信按照本文的方法,可以辅助您进行前端的高性能优化。笔者将继续写后续的部分,包括数据库的优化、负载均衡、反向代理等。由于笔者水平有限,如有错误,请联系我批评指正。
接下来在第三部分文章中,我将介绍服务器端缓存、静态化与伪静态化、分布式缓存等,并且将它们应用到 Java Web 的开发中。使用这些技术可以帮助提高 Java Web 应用程序的性能。
http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf2/
图片压缩
减少图片的大小,可以明显的提高性能,而对于已有图片,要想减少图片的大小,只能改变图片的格式,这里推荐的是 PNG8 的格式,它可以在基本保持清晰度的情况下,减少图片的大小。知道这个原理以后,可以用 Windows 的画图工具、以及 PhotoShop 工具逐个的改变。但是这样做的缺点是单张处理,效率太慢。本文推荐一个在线转换工具 Smush.it,可以批量的进行压缩与转换。它的地址是:www.smushit.com/ysmush.it。打开后效果如下图所示。
图 1. Yahoo 提供的在线压缩工具
我们上传了一张大小为 3790K 的图片,待在线程序处理完毕后,点击 Download Smushed Images 下载查看结果。下载界面如下图所示。
图 2. 压缩后的结果
打开下载下来的压缩包,查看结果可以看到,图片从 3790 减少到了 3344,就如下图所示。对于大批量的图片网站,这个方法会帮助快速实现批量图片压缩。
图 3. 压缩后的结果
回页首
图像合并实现 CSS Sprites
CSS Sprites 是一个吸引人的技术,它其实就是把网页中一些背景图片整合到一张图片文件中,再利用 CSS 的“background-image”,“background- repeat”,“background-position”的组合进行背景定位,background-position 可以用数字能精确的定位出背景图片的位置。利用 CSS Sprites 能很好地减少网页的 HTTP 请求,从而大大的提高了页面的性能,这也是 CSS Sprites 最大的优点,也是其被广泛传播和应用的主要原因。CSS Sprites 能减少图片的字节,由于图像合并后基本信息不用重复,那么多张图片合并成 1 张图片的字节往往总是小于这些图片的字节总和。同时 CSS Sprites 解决了网页设计师在图片命名上的困扰,只需对一张集合的图片上命名就可以了,不需要对每一个小元素进行命名,从而提高了网页的制作效率。更换风格方便,只需要在一张或少张图片上修改图片的颜色或样式,整个网页的风格就可以改变。维护起来更加方便。同时,由于将图片合并到一张图片,因此图片的请求数就被缩减到 1 个。其他的请求都可以用到本地缓存,不需要访问服务器。下图是一个合并以后的图片。它将很多小图标都拼到了一起。
图 4. 合并后的图片
这里介绍一个小工具 ---“CSS Sprites 样式生成工具 2.0”,可以从 这里下载。这是一个简单免费的小工具,用该工具打开上面的图片,选中图片中的某块。如下图的“绿色大拇指”部分,工具会计算出这个部分的长、宽、距离左上角的距离。勾选复制类名、复制宽、复制高,再点击“复制当前样式”按钮。这样生成的样式会被复制到剪切板上。
图 5. 小工具的使用
生成的 CSS 代码如清单 1 所示。
清单 1. 小工具生成的 CSS 代码
.div_6148{width:18px;height:20px;background-position:-17px -209px;}
将这段代码运用在网页上,它的代码如下清单所示。
清单 2. 测试 CSS Sprites 代码
<html>
<head>
<style>
.div_6148
{
width:18px;
height:20px;
background-image:url(css-sprites-source.gif);
background-position:-17px -209px;
}
</style>
</head>
<body>
<div class="div_6148"></div>
</body>
</html>
打开测试网页显示结果如下图所示。
图 6. 测试网页效果
可以看到,网页只显示工具选择的“绿色大拇指”部分,这样的代码可以运用在网页的多个部分,而图片只需要下载一次,这就是该技术的最大优势,减少了因为小图片引起的多个请求。
回页首
多域名请求
有时候,图片数据太多,一些公司的解决方法是将图片数据分到多个域名的服务器上,这在一方面是将服务器的请求压力分到多个硬件服务器上。另一方面,是利用了浏览器的特性。一般来说,浏览器对于相同域名的图片,最多用 2-4 个线程并行下载。不同浏览器的并发下载数,都是不同的,并发数如下清单所示。
清单 3. 各浏览器的并发下载数
Browsers HTTP/1.1 HTTP/1.0
IE6,7 2 4
IE8 6 6
FireFox 2 2 8
FireFox 3 6 6
Safari 3,4 4 4
Chrome 1,2 6 ?
Chrome 3 4 4
Opera 9.63,10.00alpha 4 4
而相同域名的其他图片,则要等到其他图片下载完后才会开始下载。 这里我做了一个测试,选择了多个相同域名的图片在同一网页上。代码如清单 5 所示。
清单 4. 单域名的多图片下载
接下来,使用 FireFox 的 Firebug 插件监控网络。结果如下图所示。
图 7. 单域名多图片的监控效果
可以看到,相同域名的多张图片,它们下载的起始点是存在延迟的。它们并不是并行下载。当我们将其中的 3 张图片换成别的域名图片。如清单 6 所示。
清单 5. 多域名多图片下载
再次查看网络监控,可以看到,这些图片是并行下载的。
图 8. 多域名多图片测试结果
多域名的下载固然很好,但是太多域名并不太好,一般在 2-3 个域名下载就差不多。
回页首
图像的 BASE64 编码
不管如何,图片的下载始终都要向服务器发出请求,要是图片的下载不用向服务器发出请求,而可以随着 HTML 的下载同时下载到本地那就太好了。而目前,浏览器已经支持了该特性,我们可以将图片数据编码成 BASE64 的字符串,使用该字符串代替图像地址。假设用 S代表这个 BASE64 字符串,那么就可以使用 <img src="data:image/png;base64,S"> 来显示这个图像。可以看出,图像的数据包含在了 HTML 代码里,无需再次访问服务器。那么图像要如何编码成 BASE64 字符串呢?可以使用 在线的工具---“Base64 Online”,这个工具可以上传图片将图片转换为 BASE64 字符串。当然,如果读者有兴趣,完全可以自己实现一个 BASE64 编码工具,比如使用 Java 开发,它的代码就如清单 7 所示。
清单 6. BASE64 的 Java 代码
本文编码了一个图像,并且将编码获得的 BASE64 字符串,写到了 HTML 之中,如下清单 8 所示。
清单 7. 嵌入 BASE64 的测试 HTML 代码
由于图片数据包含在了 BASE64 字符串中,因此无需向服务器请求图像数据,结果显示如下图所示。
图 9. BASE64 显示图像
然而这种策略并不能滥用,它适用的情况是浏览器连接服务器的时间 > 图片下载时间,也就是发起连接的代价要大于图片下载,那么这个时候将图片编码为 BASE64 字符串,就可以避免连接的建立,提高效率。如果图片较大的话,使用 BASE64 编码虽然可以避免连接建立,但是相对于图像下载,请求的建立只占很小的比例,如果用 BASE64,对于动态网页来说图像缓存就会失效(静态网页可以缓存),而且 BASE64 字符串的总大小要大于纯图片的大小,这样一算就非常不合适了。因此,如果你的页面已经静态化,图像又不是非常大,可以尝试 BASE64 编码,客户端会将网页内容和图片的 BASE64 编码一起缓存;而如果你的页面是动态页面,图像还较大,每次都要下载 BASE64 字符串,那么就不能用 BASE64 编码图像,而正常引用图像,从而使用到浏览器的图像缓存,提高下载速度。从现实我们接触的角度看,如一些在线 HTML 编辑器,里面的小图标,如笑脸等,都使用到了 BASE64 编码,因为它们非常小,数量多,BASE64 可以帮助网页减少图标的请求数,提高效率。
回页首
GZIP 压缩
为了减少传输的数据,压缩是一个不错的选择,而 HTTP 协议支持 GZIP 的压缩格式,服务器响应的报头包含 Content-Encoding: gzip,它告诉浏览器,这个响应的返回数据,已经压缩成 GZIP 格式,浏览器获得数据后要进行解压缩操作。这在一定程度可以减少服务器传输的数据,提高系统性能。那么如何给服务器响应添加 Content-Encoding: gzip 报头,同时压缩响应数据呢?如果你用的是 Tomcat 服务器,打开 $tomcat_home$/conf/server.xml 文件,对 Connector 进行配置,配置如清单 9 所示。
清单 8. TOMCAT 配置清单
<Connector port ="80" maxHttpHeaderSize ="8192"
maxThreads ="150" minSpareThreads ="25" maxSpareThreads ="75"
enableLookups ="false" redirectPort ="8443" acceptCount ="100"
connectionTimeout ="20000" disableUploadTimeout ="true" URIEncoding ="utf-8"
compression="on"
compressionMinSize="2048"
noCompressionUserAgents="gozilla, traviata"
compressableMimeType="text/html,text/xml" />
我们为 Connector 添加了如下几个属性,他们意义分别是:
compression="on" 打开压缩功能
compressionMinSize="2048" 启用压缩的输出内容大小,这里面默认为 2KB
noCompressionUserAgents="gozilla, traviata" 对于以下的浏览器,不启用压缩
compressableMimeType="text/html,text/xml, image/png" 压缩类型
有时候,我们无法配置 server.xml,比如如果我们只是租用了别人的空间,但是它并没有启用 GZIP,那么我们就要使用程序启用 GZIP 功能。我们将需要压缩的文件,放到指定的文件夹,使用一个过滤器,过滤对这个文件夹里文件的请求。
清单 9. 自定义 Filter 压缩 GZIP
该程序的主体思想,是在响应流写回之前,对响应的字节数据进行 GZIP 压缩,因为并不是所有的浏览器都支持 GZIP 解压缩,如果浏览器支持 GZIP 解压缩,会在请求报头的 Accept-Encoding 里包含 gzip。这是告诉服务器浏览器支持 GZIP 解压缩,因此如果用程序控制压缩,为了保险起见,还需要判断浏览器是否发送 accept-encoding: gzip 报头,如果包含了该报头,才执行压缩。为了验证压缩前后的情况,使用 Firebug 监控请求和响应报头。
清单 10. 压缩前请求
GET /testProject/gzipCategory/test.html HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
Host: localhost:9090
Connection: Keep-Alive
清单 11. 不压缩的响应
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: W/"5060-1242444154000"
Last-Modified: Sat, 16 May 2009 03:22:34 GMT
Content-Type: text/html
Content-Length: 5060
Date: Mon, 18 May 2009 12:29:49 GMT
清单 12. 压缩后的响应
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: W/"5060-1242444154000"
Last-Modified: Sat, 16 May 2009 03:22:34 GMT
Content-Encoding: gzip
Content-Type: text/html
Content-Length: 837
Date: Mon, 18 May 2009 12:27:33 GMT
可以看到,压缩后的数据比压缩前数据小了很多。压缩后的响应报头包含 Content-Encoding: gzip。同时 Content-Length 包含了返回数据的大小。GZIP 压缩是一个重要的功能,前面提到的是对单一服务器的压缩优化,在高并发的情况,多个 Tomcat 服务器之前,需要采用反向代理的技术,提高并发度,而目前比较火的反向代理是 Nginx(这在后续的文章会进行详细的介绍)。对 Nginx 的 HTTP 配置部分里增加如下配置。
清单 13. Nginx 的 GZIP 配置
gzip on;
gzip_min_length 1000;
gzip_buffers 4 8k;
gzip_types text/plain application/x-javascript text/css text/html application/xml;
由于 Nginx 具有更高的性能,利用该配置可以更好的提高性能。在高性能服务器上该配置将非常有用。
回页首
懒加载与预加载
预加载和懒加载,是一种改善用户体验的策略,它实际上并不能提高程序性能,但是却可以明显改善用户体验或减轻服务器压力。
预加载原理是在用户查看一张图片时,就将下一张图片先下载到本地,而当用户真正访问下一张图片时,由于本地缓存的原因,无需从服务器端下载,从而达到提高用户体验的目的。为了实现预加载,我们可以实现如下的一个函数。
清单 14. 预加载函数
上面的代码,首先定义了 Image 对象,并且声明了需要预加载的图像数组,然后逐一的开始加载(.src=images[i])。如果已经在缓存里,则不做其他处理;如果不在缓存,监听 onload 事件,它会在图片加载完毕时调用。
而懒加载则是在用户需要的时候再加载。当一个网页中可能同时有上百张图片,而大部分情况下,用户只看其中的一部分,如果同时显示上百张,则浪费了大量带宽资源,因此可以当用户往下拉动滚动条时,才去请求下载被查看的图像,这个原理与 word 的显示策略非常类似。
在 JavaScript 中,它的基本原理是首先要有一个容器对象,容器里面是 img 元素集合。用隐藏或替换等方法,停止 img 的加载,也就是停止它去下载图像。然后历遍 img 元素,当元素在加载范围内,再进行加载(也就是显示或插入 img 标签)。加载范围一般是容器的视框范围,即浏览者的视觉范围内。当容器滚动或大小改变时,再重新历遍元素判断。如此重复,直到所有元素都加载后就完成。当然对于开发来讲,选择已有的成熟组件,并不失为一个上策,Lazy Load Plugin for jQuery 是基于 JQuery 的懒加载组件,它有自己的 官方网站。这是一个不错的免费插件。可以帮助程序员快速的开发懒加载应用。
Flush 机制的使用
实际上在 Web 技术中,Flush 机制并不新鲜,它的思想是无需等到网页内容全部加载完毕,一次性写回客户端,而是可以部分逐次的返回。如果网页很大的话,一次性写回全部内容显然是个不明智的选择,因为这会造成网页的长时间空白。Flush 机制允许开发人员将网页的内容按文档流顺序逐步返回给客户端,这样可以使得用户知道我们的系统正在工作,只是等待的时间稍长而已,这样用户也会“心甘情愿”的等下去。Flush 机制是一个经典的提高用户体验的方法,至今也一直在用。如果网页很大,这个机制也是建议使用的。在 Java Web 技术中,实现 Flush 非常简单,只要调用 HttpServletResponse.getWriter 输出流的 flush 方法,就可以将已经完成加载的内容写回给客户端。
但是是否每个网页都要使用该技术呢?笔者当然不这么建议。将网页内容加载完毕后再一次性返回客户端也有它的好处。我们知道网络传输也有最大的传输单元,内容加载完毕后一次性输出就可以最大程度的利用传输的带宽,减少分块,减少传输次数,也就是说实际上 Flush 机制会增加用户等待时间、增加浏览器渲染时间,但是对于大网页来说,降低这点效率来增强用户体验,是值得的。
回页首
动静分离
所谓的动静分离,就是将 Web 应用程序中静态和动态的内容分别放在不同的 Web 服务器上,有针对性的处理动态和静态内容,从而达到性能的提升。本文基于 Java Web 来讲解 Web 优化,而 Java Web 的主流服务器软件是 Tomcat。让人遗憾的是,Tomcat 在并发和静态资源处理的能力上较弱,这也是 Tomcat 为人诟病的地方。但是瑕不掩瑜,既然我们选择了 Java Web,那么就应该发挥我们程序员的头脑去想方设法的提高性能。而动静分离就是其中一种方法,既然 Tomcat 处理静态资源的能力较弱,那就将静态资源的处理任务交给适合的软件,而让 Tomcat 专注于处理 JSP/Servlet 的请求。
对于静态资源处理的服务器软件,我们可以选择 Nginx,它是一款俄罗斯人开发的软件,似乎比 Apache 更加优秀。它支持高并发,对静态资源处理的能力较强,这正是我们想要的不是吗?事实上,动静分离的方案很多,有人采用 Apache+Tomcat 的组合;也有人使用 Tomcat+Tomcat 的组合,不过两个 Tomcat 分别被放置于不同的主机,不同的域名。其中 Apache+Tomcat 的方案与 Nginx 的方案原理上是一样的,它们都是基于反向代理,相对于使用 Nginx 配置动静分离,Apache 的配置就显得略微复杂一些。在 Apache 里,mod_proxy 模块负责反向代理的实现。其中核心配置内容如清单 1 所示,该配置属于本人参与某项目的其中一部分。
清单 1. 动静分离的 Apache 核心配置
从 Apache 官方对 mod_proxy 模块的介绍,我们可以知道 ProxyPass 属性可以将一个远端服务器映射到本地服务器的 URL 空间中,也就是说这是一个地址映射功能。在清单 1 的配置中,当访问的路径不在 /static/ 下时(!表示非),就转发给后端的服务器(也就是 Tomcat);否则如果是 /static/ 路径就访问本机。例如,当访问 www.xuanli365.com/static/css/index.css 时,实际处理请求的是 Apache 服务器,而访问 www.xuanli365.com/index.jsp,那么 Apache 会将请求转发到后端的 Tomcat 服务器,实际访问的页面是 http:// 192.168.1.178( 或 145):8080/index.jsp,这就实现了动静分离。在清单 1 的配置中实际也包含了简单的负载均衡(loadfactor 因子)。
事实上,我们可以随便打开一个大型门户网站来看一下,我打开的是腾讯网站,任意查看其中两张图片的地址,我发现一个是:http://mat1.gtimg.com/www/iskin960/qqcomlogo.png,而另一个则是:http://img1.gtimg.com/v/pics/hv1/95/225/832/54158270.jpg。可见该网站存放图片资源使用了多个的域名,我们再用 Linux 的 host 命令查看两个域名的 IP 地址,结果如图 1 所示。
图 1. 某网站的动静分离
可以看到,通过查看 IP 地址,我们发现这些图片很可能存放在不同的主机上(为什么是很可能?因为一个主机可以拥有多个 IP),而图片内容和网页的动态内容并不在同一 IP 下,也很可能是动静分离。多个域名在前面也已经提到,可以增加浏览器的并发下载数,提高下载效率。
本文采用另一种策略对动静分离进行演示,它的大致结构如图 2 所示。
图 2. 本文设计的动静分离结构
在本文中,我们将静态资源放在 A 主机的一个目录上,将动态程序放在 B 主机上,同时在 A 上安装 Nginx 并且在 B 上安装 Tomcat。配置 Nginx,当请求的是 html、jpg 等静态资源时,就访问 A 主机上的静态资源目录;当用户提出动态资源的请求时,则将请求转发到后端的 B 服务器上,交由 Tomcat 处理,再由 Nginx 将结果返回给请求端。
提到这,可能有您会有疑问,动态请求要先访问 A,A 转发访问 B,再由 B 返回结果给 A,A 最后又将结果返回给客户端,这是不是有点多余。初看的确多余,但是这样做至少有 2 点好处。第一,为负载均衡做准备,因为随着系统的发展壮大,只用一台 B 来处理动态请求显然是是不够的,要有 B1,B2 等等才行。那么基于图 2 的结构,就可以直接扩展 B1,B2,再修改 Nginx 的配置就可以实现 B1 和 B2 的负载均衡。第二,对于程序开发而言,这种结构的程序撰写和单台主机没有区别。我们假设只用一台 Tomcat 作为服务器,那么凡是静态资源,如图片、CSS 代码,就需要编写类似这样的访问代码:<img src=”{address of A}/a.jpg”>,当静态资源过多,需要扩展出其他的服务器来安放静态资源时,访问这些资源就可能要编写这样的代码:<img src=”{address of C}/a.jpg”>、<img src=”{address of D}/a.jpg”>。可以看到,当服务器进行变更或扩展时,代码也要随之做出修改,对于程序开发和维护来说非常困难。而基于上面的结构,程序都只要 <img src=”a.jpg”>,无需关心具体放置资源的服务器地址,因为具体的地址 Nginx 为帮您绑定和选择。
按照图 2 所示的架构图,安装好需要的软件 Nginx 和 Tomcat。按照设想,对 Nginx 的配置文件 nginx.conf 进行配置,其中与本文该部分相关的配置如清单 2 所示。
清单 2. 动静分离的 Nginx 配置
清单 2 十分简洁,其目的和我们预期的一样,动态的请求(以 .jsp 结尾)发到 B(192.168.1.117:8080,即 tomcat_server)上,而静态的请求(gif|jpg 等)则直接访问定义的 root(/home/wq243221863/Desktop/ROOT)目录。这个 root 目录我直接将其放到 Linux 的桌面 ROOT 文件夹。
接下来在 Tomcat 中新建 Web 项目,很简单,我们只为其添加一个 test.jsp 文件,目录结构如图 3 所示。
图 3. B 上的测试项目结构
而我们定义了一张测试用的静态图片,放置在 A 的桌面 ROOT/seperate 目录下。结构如图 4 所示
图 4. A 上的静态资源文件夹结构
注意:这里的 separate 目录名是与 B 的项目文件夹同名的。
再查看图 3 中的 test.jsp 的源码。如清单 3 所示。
清单 3. test.jsp 源码
清单 3 是一个非常简单的 JSP 页面,主要是使用 img 标签来访问 jquery.gif,我们知道 test.jsp 在 B 服务器上,而 jquery.gif 在 A 服务器上。用于访问 jquery.gif 的代码里不需要指定 A 的地址,而是直接使用相对路径即可,就好像该图片也在 B 上一样,这就是本结构的一个优点了。我们在 A 上访问 test.jsp 文件。结果如图 5 所示。
图 5. test.jsp 的结果
非常顺利,完全按照我们的想法实现了动静分离!
我们初步完成了动静分离的配置,但是究竟动静分离如何提高我们的程序性能我们还不得而知,我们将 Tomcat 服务器也迁移到 A 服务器上,同时将 jquery.gif 拷贝一份到 separate 项目目录下,图 3 的结构变为图 6 所示。
图 6. 拷贝 jquery.gif 的 separate 项目
我们将 Tomcat 的端口设置为 8080,Nginx 的端口依然是 9090。现在访问 http://localhost:9090/separate/test.jsp(未使用动静分离)和访问 http://localhost:8080/separate/test.jsp(使用了动静分离)的效果是一样的了。只是 8080 端口的静态资源由 Tomcat 处理,而 9090 则是由 Nginx 处理。我们使用 Apache 的 AB 压力测试工具,对 http://localhost:8080/seperate/jquery.gif、http://localhost:9090/seperate/jquery.gif、http://localhost:8080/seperate/test.jsp、http://localhost:9090/seperate/test.jsp 分别进行压力和吞吐率测试。
首先,对静态资源(jquery.gif)的处理结果如清单 4 所示。
清单 4. 静态资源的 AB 测试
清单 4 的测试脚本代表同时处理 100 个请求并下载 1000 次 jquery.gif 文件,您可以只关注清单 4 的粗体部分(Requests per second 代表吞吐率),从内容上就可以看出 Nginx 实现动静分离的优势了,动静分离每秒可以处理 2267 个请求,而不使用则只可以处理 535 个请求,由此可见动静分离后效率的提升是显著的。
您还会关心,动态请求的转发,会导致动态脚本的处理效率降低吗?降低的话又降低多少呢?因此我再用 AB 工具对 test.jsp 进行测试,结果如清单 5 所示。
清单 5. 动态脚本的 AB 测试
经过笔者的多次测试,得出了清单 5 的较为稳定的测试结果,可以看到在使用 Nginx 实现动静分离以后,的确会造成吞吐率的下降,然而对于网站整体性能来说,静态资源的高吞吐率,以及未来可以实现的负载均衡、可扩展、高可用性等,该牺牲我想也应该是值得的。
我想任何技术都是有利有弊,动静分离也是一样,选择了动静分离,就选择了更为复杂的系统架构,维护起来在一定程度会更为复杂和困难,但是动静分离也的确带来了很大程度的性能提升,这也是很多系统架构师会选择的一种解决方案。
回页首
HTTP 持久连接
持久连接(Keep-Alive)也叫做长连接,它是一种 TCP 的连接方式,连接会被浏览器和服务器所缓存,在下次连接同一服务器时,缓存的连接被重新使用。由于 HTTP 的无状态性,人们也一直很清楚“一次性”的 HTTP 通信。持久连接则减少了创建连接的开销,提高了性能。HTTP/1.1 已经支持长连接,大部分浏览器和服务器也提供了长连接的支持。
可以想象,要想发起长连接,服务器和浏览器必须共同合作才可以。一方面浏览器要保持连接,另一方面服务器也不会断开连接。也就是说要想建立长连接,服务器和浏览器需要进行协商,而如何协商就要靠伟大的 HTTP 协议了。它们协商的结构图如图 7 所示。
图 7. 长连接协商
浏览器在请求的头部添加 Connection:Keep-Alive,以此告诉服务器“我支持长连接,你支持的话就和我建立长连接吧”,而倘若服务器的确支持长连接,那么就在响应头部添加“Connection:Keep-Alive”,从而告诉浏览器“我的确也支持,那我们建立长连接吧”。服务器还可以通过 Keep-Alive:timeout=10, max=100 的头部告诉浏览器“我希望 10 秒算超时时间,最长不能超过 100 秒”。
在 Tomcat 里是允许配置长连接的,配置 conf/server.xml 文件,配置 Connector 节点,该节点负责控制浏览器与 Tomcat 的连接,其中与长连接直接相关的有两个属性,它们分别是:keepAliveTimeout,它表示在 Connector 关闭连接前,Connector 为另外一个请求 Keep Alive 所等待的微妙数,默认值和 connectionTimeout 一样;另一个是 maxKeepAliveRequests,它表示 HTTP/1.0 Keep Alive 和 HTTP/1.1 Keep Alive / Pipeline 的最大请求数目,如果设置为 1,将会禁用掉 Keep Alive 和 Pipeline,如果设置为小于 0 的数,Keep Alive 的最大请求数将没有限制。也就是说在 Tomcat 里,默认长连接是打开的,当我们想关闭长连接时,只要将 maxKeepAliveRequests 设置为 1 就可以。
毫不犹豫,首先将 maxKeepAliveRequests 设置为 20,keepAliveTimeout 为 10000,通过 Firefox 查看请求头部(这里我们访问上面提到的 test.jsp)。结果如图 8 所示。
图 8. 服务器打开长连接
接下来,我们将 maxKeepAliveRequests 设置为 1,并且重启服务器,再次请求网页后查看的结果如图 9 所示。
图 9. 服务器关闭长连接
对比可以发现,Tomcat 关闭长连接后,在服务器的请求响应中,明确标识了:Connection close, 它告诉浏览器服务器并不支持长连接。那么长连接究竟可以带来怎么样的性能提升,我们用数据说话。我们依然使用 AB 工具,它可以使用一个 -k 的参数,模拟浏览器使用 HTTP 的 Keep-Alive 特性。我们对 http://localhost:8080/seperate/jquery.gif 进行测试。测试结果如清单 6 所示。
清单 6. AB 测试长连接
结果一定会让您大为惊讶,使用长连接和不使用长连接的性能对比,对于 Tomcat 配置的 maxKeepAliveRequests 为 50 来说,竟然提升了将近 5 倍。可见服务器默认打开长连接是有原因的。
回页首
HTTP 协议的合理使用
很多程序员都将精力专注在了技术实现上,他们认为性能的高低完全取决于代码的实现,却忽略了已经成型的某些规范、协议、工具。最典型的就是在 Web 开发上,部分开发人员没有意识到 HTTP 协议的重要性,以及 HTTP 协议可以提供程序员另一条性能优化之路。通过简单的在 JSP 的 request 对象中添加响应头部,往往可以迅速提升程序性能,一切实现代码仿佛都成浮云。本系列文章的宗旨也在于让程序员编最少的代码,提升最大的性能。
本文提出一个这样的需求,在文章前面部分提到的 test.jsp 中,它的一部分功能是显示服务器的当前时间。现在我们希望这个动态网页允许被浏览器缓存,这似乎有点不合理,但是在很多时候,虽然是动态网页,但是却只执行一次(比如有些人喜欢将网页的主菜单存入数据库,那么他肯定不希望每次加载菜单都去读数据库)。浏览器缓存带来的性能提升已经众人皆知了,而很多人却并不知道浏览器的缓存过期时间、缓存删除、什么页面可以缓存等,都可以由我们程序员来控制,只要您熟悉 HTTP 协议,就可以轻松的控制浏览器。
我们访问上面提及的 test.jsp。用 Firebug 查看请求情况,发现每次请求都会重新到服务器下载内容,这不难理解,因此 test.jsp 是动态内容,每次服务器必须都执行后才可以返回结果 , 图 10 是访问当前的 test.jsp 的头部情况。现在我们往 test.jsp 添加清单 7 的内容。
清单 7. 在 test.jsp 的首部添加的代码
上述代码的意图是:服务器获得浏览器请求头部中的 If-Modified-Since 时间,这个时间是浏览器询问服务器,它所请求的资源是否过期,如果没过期就返回 304 状态码,告诉浏览器直接使用本地的缓存就可以,
图 10. 修改 test.jsp 前的访问头部情况
修改完 test.jsp 代码后,使用鼠标激活浏览器地址栏,按下回车刷新页面。这次的结果如图 11 所示。
图 11. 修改 test.jsp 后的首次访问
可以看到图 11 和图 10 的请求报头没有区别,而在服务器的响应中,图 11 增加了 Last-Modified 头部,这个头部告诉浏览器可以将此页面缓存。
按下 F5(必须是 F5 刷新),F5 会强制 Firefox 加载服务器内容,并且发出 If-Modified-Since 头部。得到的报头结果如图 12 所示 .
图 12. 修改 test.jsp 后的再次访问
可以看到,图 12 的底部已经提示所有内容都来自缓存。浏览器的请求头部多出了 If-Modified-Since,以此询问服务器从缓存时间起,服务器是否对资源进行了修改。服务器判断后发现没有对此资源(test.jsp)修改,就返回 304 状态码,告诉浏览器可以使用缓存。
我们在上面的实验中,用到了 HTTP 协议的相关知识,其中涉及了 If-Modified-Since、Last-Modified、304 状态码等,事实上与缓存相关的 HTTP 头部还有许多,诸如过期设置的头部等。熟悉了 HTTP 头部,就如同学会了如何与用户的浏览器交谈,也可以利用协议提升您的程序性能。这也是本文为何一直强调 HTTP 协议的重要性。那么对于 test.jsp 这个小网页来说,基于缓存的方案提升了多少性能呢?我们用 AB 给您答案。
AB 是个很强大的工具,他提供了 -H 参数,允许测试人员手动添加 HTTP 请求头部,因此测试结果如清单 8 所示。
清单 8. AB 测试 HTTP 缓存
测试脚本:ab -c 1000 – n 10000 – H ‘ If-Modified-Since: Sun, 05 Jun 3910 00:00:00 GMT ’ http://localhost:8080/seperate/test.jsp
分别对比 Document Length、Requests per second 以及 Transfer rate 这三个指标。可以发现没使用缓存的 Document Length(下载内容的长度)是 362 字节,而使用了缓存的长度为 0。在吞吐率方面,使用缓存是不使用缓存的 3 倍左右。同时在传输率方面,缓存的传输率比没缓存的小。这些都是用到了客户端缓存的缘故。
回页首
CDN 的使用
CDN 也是笔者最近才了解和接触到的东西,耳中也是多次听到 CDN 这个词了,在淘宝的前端技术报告上、在一个好朋友的创新工场创业之路上,我都听到了这个词,因此我想至少有必要对此技术了解一下。所谓的 CDN,就是一种内容分发网络,它采用智能路由和流量管理技术,及时发现能够给访问者提供最快响应的加速节点,并将访问者的请求导向到该加速节点,由该加速节点提供内容服务。利用内容分发与复制机制,CDN 客户不需要改动原来的网站结构,只需修改少量的 DNS 配置,就可以加速网络的响应速度。当用户访问了使用 CDN 服务的网站时,DNS 域名服务器通过 CNAME 方式将最终域名请求重定向到 CDN 系统中的智能 DNS 负载均衡系统。智能 DNS 负载均衡系统通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的节点地址提供给用户,使用户可以得到快速的服务。同时,它还与分布在不同地点的所有 CDN 节点保持通信,搜集各节点的健康状态,确保不将用户的请求分配到任何一个已经不可用的节点上。而我们的 CDN 还具有在网络拥塞和失效情况下,能拥有自适应调整路由的能力。
由于笔者对 CDN 没有亲身实践,不便多加讲解,但是各大网站都在一定程度使用到了 CDN,淘宝的前端技术演讲中就提及了 CDN,可见 CDN 的威力不一般。
图 12. 淘宝的 CDN 前端优化
因此 CDN 也是不得不提的一项技术,国内有免费提供 CDN 服务的网站:http://www.webluker.com/,它需要您有备案的域名,感兴趣的您可以去试试。
回页首
小结
本文总结了 HTTP 长连接、动静分离、HTTP 协议等等,在您需要的时候,可以查看本文的内容,相信按照本文的方法,可以辅助您进行前端的高性能优化。笔者将继续写后续的部分,包括数据库的优化、负载均衡、反向代理等。由于笔者水平有限,如有错误,请联系我批评指正。
接下来在第三部分文章中,我将介绍服务器端缓存、静态化与伪静态化、分布式缓存等,并且将它们应用到 Java Web 的开发中。使用这些技术可以帮助提高 Java Web 应用程序的性能。
相关推荐
"Java高性能、高并发Web应用开发技巧" Java开发高性能、高并发Web应用是当前Web应用发展的主要需求。为满足这种需求,需要了解Java开发高性能、高并发Web应用的技巧。在本文中,我们将讨论如何用Java开发高性能、...
Web MVC 框架、网站性能优化与安全策略、设计模式与架构、网站设计与网页配色、Java Web典型项目开发案例等。配书光盘附带了实例的源程序。 《Java Web开发实例大全(提高卷)》既适合Java Web程序员参考和查阅,也...
Java_Web高性能开发_第2部分:前端的高性能.doc ,供学习参考
《Java Web整合开发王者归来》这本书的标题和描述都强调了Java Web开发的整合过程,并表示这是一本关于Java Web开发的完整指南。从这些信息中,我们可以推断出书中可能涉及的一些关键知识点: 1. Java基础:作为...
在Java开发领域,构建高性能、高并发的Web应用是一项核心任务。这涉及到多个技术层面的综合运用,包括但不限于系统架构设计、线程管理、数据访问优化、缓存策略、负载均衡以及性能监控等。以下是一些关键的知识点,...
Java Web开发性能监测工具是开发者用来优化应用程序性能的关键工具,它们可以帮助我们识别并解决系统中的瓶颈,提升用户体验,以及确保服务的稳定性和高效性。在Java Web开发领域,有许多这样的工具,它们各自拥有...
Java开发高性能、高并发Web应用 Java开发高性能、高并发Web应用
随着Java Web开发的发展,越来越多的高级技术和框架被引入进来,以简化开发过程、提高应用性能和安全性。常见的高级技术包括: 1. **MVC模式**:Model-View-Controller模式是一种设计模式,用于分离数据模型、用户...
《深入体验Java_Web开发内幕—核心基础》是一本针对Java Web开发的深度解析书籍,旨在帮助读者全面理解和掌握Java Web开发的核心技术。这本书涵盖了从基础到高级的多个主题,旨在提供一个完整的Java Web开发学习路径...
Web MVC 框架、网站性能优化与安全策略、设计模式与架构、网站设计与网页配色、Java Web典型项目开发案例等。配书光盘附带了实例的源程序。 《Java Web开发实例大全(提高卷)》既适合Java Web程序员参考和查阅,也...
Java Web开发实践教程是一门深度探索Java在Web领域应用的课程,旨在帮助学习者掌握从基础到高级的Java Web开发技术。本课程涵盖了广泛的主题,包括Servlet、JSP、MVC框架(如Spring MVC)、数据库交互、过滤器、监听...
Java Web开发技术是互联网应用程序构建的核心领域,涵盖了多种技术和工具,用于创建动态、交互式的Web应用。本资源“Java Web开发技术大全”旨在提供全面的学习材料,帮助开发者深入理解和掌握这一领域的关键概念。 ...
Java Web开发技术是构建互联网应用程序的关键领域,涵盖了服务器端编程、网页交互以及数据库管理等多个方面。本书《Java Web开发技术大全》无疑是深入学习这一领域的宝贵资源。以下将详细阐述Java Web开发中的核心...
《Java Web应用开发项目教程》是一本以实践为导向的教程,旨在帮助读者深入理解并掌握Java Web开发技术。教程通过完整的案例,采用模块化的教学方式,将复杂的Web应用开发过程分解为可操作的步骤,逐步引导学习者...
《Tomcat与Java Web开发技术详解(第2版)》是一本深入探讨Java Web应用程序部署和运行环境的权威指南。本书主要围绕Apache Tomcat服务器展开,涵盖了从基础概念到高级特性的全方位讲解,旨在帮助读者掌握Java Web...
Java Web Service性能监视是针对基于Java的Web服务应用程序的性能监控...通过这些工具,可以有效地监控Java Web Service的性能,从而提高整体服务质量,减少响应时间,避免资源浪费,并确保系统的高可用性和稳定性。