一,Cache,王道也
我觉得系统架构不应该仅仅是搭建一个强硬的能承受巨大并发压力的后台,前端页面也是需要架构的而且同等重要,不理解前台的的后台工程师是不合格的。中国
人讲究钢柔相济,后台强硬只能说你内功深厚,前端用的巧,那叫四两拨千斤。
一般后台工程师很少关心前端如何使用自己的资源,而前端工程师,不知道自己的一个简单的用法会对后端造成多大影响。我会给出一些数据,来震撼下你的眼
球。
二,Cache 基本原理介绍(参考Caching Tutorial)
为什么使用Cache?
1,减少延迟,让你的网站更快,提高用户体验。
2,避免网络拥塞,减少请求量,减少输出带宽。
补充一个cache的原则:不更新的资源就不应该让它再次产生HTTP请求,如果强制产生了请求,那么就看看能否返回304。
Cache的种类?
浏览器Cache,代理Cache,网关Cache。
后端还有 disk cache ,server cache,php cache,不过不属于我们今天讨论范围。
Cache如何工作的?
1,如果响应头告诉cache别缓存它,cache不对它做缓存;
2,如果请求需要验证的或者是需要安全性的,它将不被缓存;
3,如果响应头里没有ETag或Last-Modifed header这类元素,而且也没有任何显式的信息告诉如何对数据保鲜,则它被认为不可缓
存。
4,在下面情况下,一个缓存项被认为是新鲜的(即,不需到原server上检查就可直接发送给client):
它设置了一个过期时间或age-controlling响应头,而且现在仍未过期。
如果浏览器cache里有某个数据项,并且被被设置为每个会话(session)过程中只检查一次;
如果一个代理cache里能找个某个数据项,并且它是在相对较长时间之前更新过的。
以上情况会认为数据是新鲜的,就直接走cache,不再查询源server。
5,如果有一项过期了,它将会让原server去更新它,或者告诉cache这个拷贝是否还是可用的。
怎么控制你的Cache?
Meta tags :在html页面中指定,这个方法只被少数浏览器支持,Proxy一般不会读你html的具体内容然后再做cache决策的。
Pragma: no-cache : 一般被大家误用在http响应头中,这不会产生任何效果。而实际它仅仅应该用在请求头中。不过google的
Server: GFE/1.3 响应中却这样用,难道人家也误用了呢。
Date: 当前主机GMT时间。
Last-Modified : 文件更新GMT时间,我在响应头中带上这个元素的时候,通常浏览器在cache时间内再发请求都会稍带上If-
Modified-Since,让我们判断需要重新传输文件内容,还是仅仅返回个304告诉浏览器资源还没更新,需要缓存策略的服务器肯定都得支持的。
有了这个请求,head请求在基本没太多用处了,除非在telnet上调试还能用上。
If-Modified-Since : 用在请求头里,见Last-Modified 。
Etag: 标识资源是否发生变化,etag的生成算法各是各样,通常是用文件的inode+size+LastModified进行Hash后得到
的,可以根据应用选择适合自己的。Last-Modified 只能精确到秒的更新,如果一秒内做了多次更新,etag就能派上用场。貌似大家很少有这
样精确的需求,浪费了http header的字节数,建议不要使用。
Expires : 指定缓存到期GMT的绝对时间,这个是http 1.0里就有的。这个元素有些缺点,一,服务器和浏览器端时间不一致时会有问
题。二,一旦失效后如果忘记重新设置新的过期时间会导致cache失效。三,服务器端需要根据当前Date时间 + 应该cache的相对时间去计算这
个值,需要cpu开销。 我不推荐使用。
Cache-Control:
这个是http 1.1中为了弥补 Expires 缺陷新加入的,现在不支持http 1.1的浏览器已经很少了。
max-age:指定缓存过期的相对时间秒数,max-ag=0或者是负值,浏览器会在对应的缓存中把Expires设置为1970-01-01
08:00:00 ,虽然语义不够透明,但却是我最推荐使用的。
s-maxage:类似于max-age,只用在共享缓存上,比如proxy.
public:通常情况下需要http身份验证的情况,响应是不可cahce的,加上public可以使它被cache。
no-cache:强制浏览器在使用cache拷贝之前先提交一个http请求到源服务器进行确认。这对身份验证来说是非常有用的,能比较好的遵守
(可以结合public进行考虑)。它对维持一个资源总是最新的也很有用,与此同时还不完全丧失cache带来的好处,因为它在本地是有拷贝的,但是在
用之前都进行了确认,这样http请求并未减少,但可能会减少一个响应体。
no-store: 告诉浏览器在任何情况下都不要进行cache,不在本地保留拷贝。
must-revalidate:强制浏览器严格遵守你设置的cache规则。
proxy-revalidate:强制proxy严格遵守你设置的cache规则。
用法举例: Cache-Control: max-age=3600, must-revalidate
其他一些使用cache需要注意的东西,不要使用post,不要使用ssl,因为他们不可被cache,另外保持url一致。只在必要的地方,通常是动
态页面使用cookie,因为coolie很难cache。至于apache如何支持cache和php怎么用header函数设置cache,暂不做
介绍,网上资料比较多。
如何设置合理的cache时间 ?
http://image2.sinajs.cn/newchart/min/n/sz000609.gif?1230015976759
拿我分时图举例,我们需要的更新频率是1分钟。但为了每次都拿到最新的资源,我们在后面加了个随机数,这个数在同一秒内的多次刷新都会变化。我们的js
虽然能够很好的控制,一分钟只请求一次,但是如果用户点了刷新按纽呢?这样的调用是完全cache无关的,连返回304的机会都没有。
试想,如果很多人通过同一个代理出去的,那么所有的请求都会穿透代理,弄不好被网管封掉了。如果我们做只做一秒的cache,对直接访问源服务器的用户
没太多影响,但对于代理服务器来说,他的请求可能会从10000 req/min 减少为 60 req/min ,这是160倍。
对于我们行情图片这样的情况,刷新频率为1分钟,比较好的做法是把后面的随机数(num)修改为 num=t-t%60 其中t是当前时间戳,这样你一
分钟内刷这个url是不变的,下一分钟会增加1,会再次产生一个新请求。而我的max-age设置为默认59秒,即使设置 120秒其实也没什么影响。
可能你会说万一赶上临界点可能拿不到最新的数据,其实对用户来说,用那个多变的随即数和我这个分钟级的随即数,看到的效果是相同的下面我给你分析一下:
如果用户打开了我们的分时间页面,当前随即数对他来说是新的,所以他会拿到一个当前最新的图片,然后他点了刷新按纽,用户会产生 http请求,即使
url没变,服务器有最新图片也一定会返回,否则返回304,一分钟后js刷新图片,分钟数加了1,会得到全新资源。这和那个随时变化的随即数效果有区
别吗?都拿到了最新的数据,但是却另外收益了cache带来的好处,对后端减少很多压力。
三,我划分的3个刷新级别
名词解释 全新请求: url产生了变化,浏览器会把他当一个新的资源(发起新的请求中不带If-Modified-Since)。
1,在地址栏中输入http://sports.sinajs.cn/today.js?maxage=11地址按回车。重复n次,直到cache时间
11秒过去后,才发起请求,这个请求是全新的,不带If-Modified-Since。
2,按F5刷新. 发起一个全新的请求,然后按F5会产生一个带If-Modified-Since的请求,如果返回304,将不再发起新的请求,直
到第一次请求设置的cache过期,然后发起一个全新的请求。
3, ctrl+F5 ,总会发起一个全新的请求。
下面是按F5刷新的例子演示: http://sports.sinajs.cn/today.js?maxage=11
( 如果这个值大于浏览器最大cache时间maxage,将以浏览器最大cache为准)
----------------------------------------------------------发起一个全新请求
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
HTTP/1.x 200 OK
Server: Cloudia
Last-Modified: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=11 (浏览器会cache这个页面内容,然后将cache过期时间设置为当前时间+11秒)
Content-Length: 312
Connection: Keep-Alive
---------------------------------------------------------- 按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT (按F5刷新,If-Modified-
Since将上次服务器传过来的Last-Modified时间带过来)
Cache-Control: max-age=0
HTTP/1.x 304 Not Modified
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11 (这个max-age有些多余,浏览器发现Not Modified,将使用本地cache
数据,但不会重新设置本地过期时间)
----------------------------------------------------------
继续按F5刷新n次.......
这11秒内未产生http请求.直到11秒过去了...............
----------------------------------------------------------按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive (cache过期后,发起的是一个全新的请求,未带)
HTTP/1.x 200 OK
Server: Cloudia
Last-Modified: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=11
Content-Encoding: deflate
Content-Length: 312
Connection: Keep-Alive
Content-Type: application/x-javascript
----------------------------------------------------------按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=0
HTTP/1.x 304 Not Modified
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11
----------------------------------------------------------
四,我对HTTP协议做的一点创新(?maxage=6000000)
上面看到了url后面有 ?maxage=xx 这样的用法,这不是一个普通的参数,作用也不仅仅是看起来那么简单。他至少有以下几个好处:
1,可以控制HTTP header的的 max-age 值。
2, 让用户为每个资源灵活定制精确的cache时间长度。
3, 可以代表资源版本号。
首先谈论对后端的影响:
服务器实现那块,不用再load类似mod_expires,mod_headers 这样额外的module,也不用去加载那些规则去比较,它属于什
么目录,或者什么文件类型,应该cache多少时间,这样的操作是需要开销的。
再说说对前端的影响:
比如同一个分时行情图片,我们的分时页中需要1分钟更新,而某些首页中3分钟更新好。不用js控制的话,那我cache应该设置多少呢? 有了
maxage就能满足这种个性化定制需求。
另一种情况是,我们为了cache,把某个图片设置了一个永久cache,但是由于需求,我必须更新这个图片,那怎么让用户访问到这个更新了的图片呢?
从 yahoo的资料和目前所有能找到的资料中都描述了同一种方法,更改文件名字,然后引用新的资源。 我觉得这方法太土, 改名后,老的还不能删除,
可能还有地方在用,同一资源可能要存两份,再修改,又得改个名,存3份,不要不把inode当资源。我就不那样做,只需要把
maxage=6000000 修改成 maxage=6000001 ,问题就解决了。
maxage=6000000 所产生的威力 (内存块消耗减少了250倍 ,请求数减少了37倍) :
体育那边要上一个新功能,一开始动态获取那些数据,我觉得那样太浪费动态池资源,就让他们把xml文件到转移到我的js池上来,为了方便,他们把那个
84k 的flash文件也放在了一起,而且是每个用户必须访问的。说实在的,我不欢迎这种大块头,因为它不可压缩,按正常来说,它应该代表一个3M的
文件。我的服务器只这样设计的,如果一次发送不完的就暂存在内存里,每个内存块10k,如果不带参数默认maxage=120 。 我发现,由于这个文
件,10w connections的时候,我消耗了10000个内存块。我自己写的申请连续内存的算法也是消耗cpu地,一个84k的文件,发送一次
后,剩余的 64k就应该能装的下,于是我把最小内存块大小改为64K。 这样消耗10w conn的时候消耗1500个左右内存快,虽然内存消耗总量
没怎么变小,但是它能更快的拿到64K的连续内存资源,cpu也节约下来了。接下来我让 meijun把所应用的flash资源后面加上
maxage=6000000 (大概=79天,浏览器端最长cache能达到着个就不错了), 10w connections的时候,只消耗了不到
40个内存块,也就是说内存块消耗减少了250倍 ,请求数减少了37倍。 35w+ connections, 5.67w req/s的时候也
就消耗100块左右,比线性增加要少很多。也就是这点发现让我有了做这个技术分享的冲动,其他都是顺便讲讲。
五,Yslow优化网站性能的14条军规点评
其中黑色部分,跟后端是紧密相连的,在我们的内容中都已经涉及到了,而且做了更深入的讨论。兰色部分,5,6,7是相关页面执行速度的,构建前端页面的
人应该注意的。 11属于避免使用的方法。 红色部分我着重说一下:
gzip 我不推荐使用,因为有些早期IE支持的不好,它的表现为直接用IE访问没问题,用js嵌进去,就不能正常解压。这样的用户占比应该在2%左
右。这个问题我跟踪了近一个月,差点放弃使用压缩。后来发现我以前用deflate压缩的文件却能正常访问。 改用deflate问题解决。
apache 1.x使用mod_gzip ,到了 2.x 改用cmod_deflate,不知道是否跟这个原因有关。 另外对于小文件压缩来说,
deflate 可比 gzip 省不少字节。
减少 DNS 查询: 这里也是有个取舍的,一般浏览器最多只为一个域名创建两个连接通道。如果我一个页面嵌了 image.xx.com 的很多图
片,你就会发现,图片从上往下一张张显示出来这个过程。这造成了浏览器端的排队。我们可以通过增加域名提高并发度,例如
image0.xx.com ,image1.xx.com ,image2.xx.com,image3.xx.com 这样并发度就提上去了,但是
会造成很多cache失效,那很简单,假如我们对文件名相加,对4取mod,就能保证,某个图片只能通过某个域名进行访问。不过,我也很反对一页面请求
了数十个域名,很多域名下只有一到两个资源的做法,这样的时间开销是不划算的。
另外,我在这里再添一个第15条:错开资源请求时间,避免浏览器端排队。
随着ajax的广泛使用,动态刷新无处不在,体育直播里有个页面调用了我一个域名下的6个文件,3个js,3个xml。刷新频率大致是两个10秒的,两
个 30秒的,两个一次性载入的。观察发现正常响应时间都在7ms,但是每过一会就会出现一次在100ms以上的,我就很奇怪,服务器负载很轻呢。
meijun帮我把刷新时间错开,11秒的,9秒的,31秒的,这样响应在100ms以上的概率减少了好几倍,这就是所谓的细节决定成败吧。
1. 尽可能的减少 HTTP 的请求数 [content]
2. 使用 CDN(Content Delivery Network) [server]
3. 添加 Expires 头(或者 Cache-control ) [server]
4. Gzip 组件 [server]
5. 将 CSS 样式放在页面的上方 [css]
6. 将脚本移动到底部(包括内联的) [javascript]
7. 避免使用 CSS 中的 Expressions [css]
8. 将 JavaScript 和 CSS 独立成外部文件 [javascript] [css]
9. 减少 DNS 查询 [content]
10. 压缩 JavaScript 和 CSS (包括内联的) [javascript] [css]
11. 避免重定向 [server]
12. 移除重复的脚本 [javascript]
13. 配置实体标签(ETags) [css]
14. 使 AJAX 缓存
六,上线了 != Finished
奥运期间我按1500w~2000w connections在线,设计了一套备用系统,现在看来,如果用户真达到了这个数目我会很危险,因为有部分服
务器引入了32bit的centos 5未经实际线上检验,而我当时简单的认为它应该和centos 4表现出一样的特性。所以现在未经过完全测试的
lib库和新版本,我都很谨慎的使用。没在真实环境中检验过,不能轻易下结论。
很多项目组好象不停的忙,做新项目,上线后又继续下个新项目,然后时不时的转过头去修理以前的bug。如果一个项目上线后,用户量持续上升,就应该考虑
优化了,一个人访问,和100w人访问,微小的修改对后端影响是不能比较的,不该请求的资源就让它cache在用户的硬盘上,用户访问块了,你也省资
源。上线仅仅代表可以交差了而已,对于技术人员来说持续的对一个重要项目进行跟踪和优化是必要的。
七,提速度同时节约成本方法汇总
1,编写节约的HTTP服务器 (高负载下速度明显提升,节约5~10倍服务器)
对一些重要的服务器量身定做。或者选用比较高效的开源软件进行优化。
2,不同服务混合使用 (节约1~2倍服务器)
如果我们一台服务器只支持30w conn的话,那么剩余的75% cpu资源,95%的内存资源,和几乎所有的磁盘资源都可以部署动态池系统,我觉得
DB对网卡中断的消耗还是有限的,我也不用新买网卡了。
3,对于纯数据部分启用新的域名(速度有提升,上行带宽节约1倍以上)
比如我们另外购买了sinajs.cn 来做数据服务,以避免cookie,节约带宽. Cookie不但会浪费服务器端处理能力,而且它要上行数据,
而通常情况上行比下行慢。
4, 使用长连接 (速度明显提升,节约带宽2倍以上,减少网络拥塞3~无数倍)
对于一次性请求多个资源,或在比较短的间隔内会有后续请求的应用,使用长连接能明显提升用户体验,减少网络拥塞,减少后端服务器建立新连接的开销。
5,数据和呈现分离,静态数据和动态数据分离 (速度明显提升,同时节约3倍带宽)
div+css 数据和呈现分离以后,据说文件大小能降到以前的1/3。
把页面中引用的js文件分离出来,把动态部分和静态部分也分离开来。
6,使用deflate压缩算法 (速度明显提升,节约3.33倍带宽)
一般来说压缩过的文件大小不到以前的30% 。
将上面分离出来的数据进行压缩(累计节约带宽10倍)。
7, 让用户尽可能多的Cache你的资源 (速度明显提升,节约3~50倍服务器资源和带宽资源)
将上面分离出来的css和不经常变动的js数据部分cache住合适的时间。(理想情况,累计节约带宽30~500倍) 。
以上改进可以让速度大幅度提升的同时,服务器资源节约 5~20 倍 ,减少网络拥塞3~无数倍, 上行带宽节约1倍以上,下行带宽节约30~500
倍,甚至更多。