HTTP性能的问题与方案 一个最终用户访问一个网页,从浏览器发出请求,到接受请求,时间大体上消耗在了以下几个部分: 建立tcp/ip握手连接。 浏览器向服务器传送请求数据。 服务器处理数据。
HTTP性能的问题与方案
一个最终用户访问一个网页,从浏览器发出请求,到接受请求,时间大体上消耗在了以下几个部分:
建立tcp/ip握手连接。
浏览器向服务器传送请求数据。
服务器处理数据。
服务器返回数据。
如果用户请求的资源很少改变,像js,css,图片之类的静态文件,如果每次用户的请求都需要占用服务器资源去处理,再如果一个用户和服务器位于太平洋两岸,那么,时间就被浪费在了网络传输和服务器处理步骤上了。在这种情况下,应该使用cache。
像上图一样,在离用户最近的地方,增加一个缓存服务器,将不常修改的静态文件缓存起来,用户的请求就可以直接由缓存服务器来处理,而不需要再劳烦网站服务器了。
一般缓存服务器,代理服务器和逆向代理服务器在一起,即,一个Apache Httpd上,同时开启了缓存功能与代理功能。只是这台服务器从不同的功能角度,被称为不同的名字,有时叫缓存服务器,有时叫代理服务器。
Http缓存机制
http缓存是遵循http协议实现的。控制缓存行为的字段均在http header中。这些字段分别在几个方面控制着缓存:
有效期机制
重验证机制
共享机制
有效期机制
控制缓存有效期的有下面几个header,我必须将它们列出来。
计算一个缓存是否有效,只需要计算缓存的使用期(cache age)和新鲜生存期(freshness lifetime)。缓存有效的判别式为:
isValid = cache_age < freshness_lifetime;
新鲜生存期比较好计算,应该为Cache-Control:max-age的值。如果没有Cache-Control:max-age的话,则使用Expires,如果都没有,则使用试探时算法(略)。计算式如下:
if (cache-control_max_age_set) fressness_lifetime = Cache-Control:max-age;else if (Expires_set) freshness_lifetime = Expires - Date;else 试探算法。
为何Expires比max-age给替代了?因为Expires有可能是各级代理服务器写入的,但Date是由原始服务器写入的,各个服务器的时钟可能存在差异,其准确度无法得到保证。
缓存使用期则是比较难计算的。它不能简单的用当前时间去减原始服务器写入的Date,原因是各个服务器的时钟差异,有的可能差几天。所以,它的计算依靠每一级代理服务器返回缓存时所带的age头部。对于离服务器最近的一级代理服务器,age头部的算法应该为:
age = (response_arrive_time - request_sent_time) + (now - response_arrived_time) - 修正值; //此计算并不精确,但误差在秒级。
那下一级代理的age算法应该为:
age = age_of_cache_returen_from_last_proxy + cache_time_saved_in_this_proxy;
递归下去,可以计算出每一级代理的age头部。
重验证机制
下面列出了几个重要的头部,还有一些没有列在其中。
以上的几个头部,组成了cache的重验证机制。前3个头部是对cache强制认证的约束,后面4个则是实体认证时所需要检测的属性。
需要注意no-cache和must-revalidate的区别。no-cache表明缓存在被返回之前,必须向服务器冲验证是否被修改过。有时候,代理允许使用过期的cache,比如,服务器死机了,代理的cache虽然已经过期,但还是可以被使用。must-revalidate并不是要求cache一定要重验证,未过期的cache不会因它的存在而去服务器重新验证。must-revalidate是为了禁止这种行为。它要求过期的缓存一定要重验证通过后才能使用,如果服务器死机了,那无法进行重验证,过期的cache也就不能再被使用,用户收到504 gateway timout。 must-revalidate对有效期内的cache并无作用。
共享机制
缓存应该存在什么地方,则由下面的三个头部组成。
Cache的流程图
因为整个过程中存在很多的条件节点,时序图并不好表示出因果关系,所以还是用流程图更好一些。我想了很久,才想出如何用一种更直观的方法来画出cache工作的流程图。
黄色线代表request的流程,蓝色线代表response的流程,红色线代表原始服务器处理流程。
Apache Httpd搭建Cache Server
软件需要
Tomcat 8
Apache Httpd 2.2
服务器开发
我制作一个图片展示页面。页面上展示4张图片,每张图片的响应都包含不同的cache header。这四张图片如下:
为此,我可以专门写一个filter,来管理这些header。Last-Modified和ETag头会由tomcat的default servlet自动添加。我就不自己去处理了。此filter在doFilter方法上加了synchronized修饰,为了让请求按顺序执行,这样我们就可以看清楚每个请求和返回的header都有什么。我把header都打印出来了。
@WebFilter(urlPatterns={"*.jpg"}) public class CacheFilter implements Filter { private int count = 0; public void init(FilterConfig filterConfig) throws ServletException {} public void destroy() {} public synchronized void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String uri = req.getRequestURI(); System.out.println("["+count+++"]Accessing "+uri); printRequestHeaders(req); HttpServletResponse resp = (HttpServletResponse) response; if (uri.endsWith("public.jpg")) { resp.addHeader("Cache-Control", "max-age=600,public"); } if (uri.endsWith("private.jpg")) { resp.addHeader("Cache-Control", "max-age=600,private"); } if (uri.endsWith("noStore.jpg")) { resp.addHeader("Cache-Control", "no-store"); } if (uri.endsWith("noCache.jpg")) { resp.addHeader("Cache-Control", "no-cache"); } chain.doFilter(request, response); printResponseHeaders(resp); } private void printRequestHeaders(HttpServletRequest request) { System.out.println("Request headers:"); Enumeration<String> names = request.getHeaderNames(); while (names.hasMoreElements()) { String name = names.nextElement(); Enumeration<String> values = request.getHeaders(name); while (values.hasMoreElements()) { System.out.println(name + " : " + values.nextElement()); } } System.out.println(); } private void printResponseHeaders(HttpServletResponse response) { System.out.println("Response headers:"); Collection<String> names = response.getHeaderNames(); for (String name : names) { Collection<String> values = response.getHeaders(name); for (String value : values) { System.out.println(name + " : " + value); } } System.out.println("**********************************************"); }}
创建一个web app, 名叫cache。将4张图片复制到webapp/img/目录下。创建上面的filter。
最后在index.html上面展示:
<html><body><div><img src="/cache/img/public.jpg"/></div><div><img src="/cache/img/private.jpg"/></div><div><img src="/cache/img/noStore.jpg"/></div><div><img src="/cache/img/noCache.jpg"/></div></body></html>
打包,并部署到tomcat服务器。
缓存服务器开发
自从Apache 2.2开始,cache_module已经成为非常稳定模块。它完全遵循HTTP协议中的cache规定。所以我们可以使用cache_module来做实现Apache Httpd的缓存管理。
cache_module是由两个实现模块mem_cache_module和disk_cache_module所支持的。其中mem_cache_module是缓存于内存中,disk_cache_module则存于硬盘上。各种详细的命令可以参照Apache手册。本实验,只使用cache_module和disk_cache_module来管理HTTP缓存。只涉及到了部分命令。
修改Apache下的httpd.cnf
LoadModule cache_module modules/mod_cache.soLoadModule disk_cache_module modules/mod_disk_cache.soLoadModule proxy_module modules/mod_proxy.soLoadModule proxy_ajp_module modules/mod_proxy_ajp.so
然后修改Apache下的httpd-vhosts.conf。
NameVirtualHost *:80<VirtualHost *:80> ServerAdmin joey ServerName www.test.com ErrorLog "logs/cache.log" CustomLog "logs/accessCache.log" common # 开启debug LogLevel debug ProxyRequests Off ProxyPreserveHost On ProxyPass / ajp://localhost:9009/ ProxyPassReverse / ajp://localhost:9009/ # 如果header中没有max-age或者expires,则添加默认过期时间,默认1小时. CacheDefaultExpire 3600 # cache存放的目录 CacheRoot D:/share/tmp # 对pattern为/cache的url开启磁盘缓存。 CacheEnable disk /cache CacheDirLevels 5 CacheDirLength 3 UseCanonicalName On</VirtualHost>
在apache2.2中,cache的log还不完善。如果使用2.4版本,可以开启关于cache命中决策的log。2.2中,只能通过在ErrorLog中搜索mod_cache.c关键字,查看cache的处理日志。
检测结果
使用检测工具为Fiddler2, 一个可以捕获所有浏览器对外交流的软件。还可以使用Firebug, chrome debug tool。
在执行下面步骤之前,必须确保明白一件重要的事情,下面所有的步骤都是在浏览器地址栏输入地址后,按enter键产生的。不可以使用刷新键。
(ie)用ie首次访问http://www.test.com/cache
在代理服务器端查看cache.log,或cache文件夹。在浏览器端,可以使用Fiddler和web developer tool等工具。可以发现对4张图片请求的处理:
(ie,10分钟内)在浏览器缓存和代理缓存均未过期,chrome访问http://www.test.com/cache
(chrome,10分钟内)在代理缓存新鲜期内,换一个浏览器,chrome访问http://www.test.com/cache
(chrome,10分钟后)在10分钟后,代理的缓存和浏览器的缓存都过期了。chrome访问http://www.test.com/cache
(ie,在第4步后,立刻)这时候,ie的缓存已经过期,但proxy端的缓存是新的。
定期清理没用的cache磁盘
使用apache的bin目录下的htcacheclean命令,定期清理cache磁盘。htcacheclean可以手工运行,也可以做成守护线程,周期性运行。运行手册http://httpd.apache.org/docs/2.2/programs/htcacheclean.html