首先说下页面性能优化的目的是什么
我们一般会说某某页面打开好慢,某某页面反应好慢,或者说大促时某某页面打不开。这些其实换成专业的名词主要就是这些页面的首屏时间、首包时间、总下载时间等太长,页面的稳定性不高、健壮性不够。
以上我准备从两大方面和大家分享下当当卖场在整体优化过程中所做的一些实践&结果。
当当卖场系统承接了当当大部分的展示型业务,包括当首、各馆首、店铺、榜单、专题、促销等等。那么我们如何保证这么多产品线的性能,即如何保证它整体的稳定、健壮、速度了?
整体设计上:
1. 组件化
2. 数据生成机制的优化
3. 数据加载形式的优化
页面本身的优化:
1. html页面相关优化
a) textarea延迟渲染
b) 图片懒加载
c) Js处理
i. Js压缩
ii. s放置尾部
2. 网络相关优化
a) 多域名服务
b) DNS预解析
c) URL链接合并
d) CDN服务
e) GZIP压缩
3. 服务端相关优化
a) 多级缓存策略
b) 并发处理策略
整体设计上对页面的优化,我们以当首为例:
当首在设计上遵循了卖场系统的整体方案:
1. 组件化的优化
组件化是卖场的一个核心功能,通过组件化可以实现模块功能的完全复用,大大降低了开发成本。同时相对传统的页面整体处理,组件化使得页面操作更灵活,也大大的降低了由于部分区域显示问题造成的整体页面的错误,加强了整体页面的健壮性,同时也为之后在整体页面中的按需加载打好基础,如下图就是组件化的页面的一个生成流程。
那具体什么是组件化了?除了刚才说的优点外又存在什么样的问题了?以下面截图中的当首【图书楼层】和【美妆楼层】为例
从截图看,这两个楼层的UI是不同的,但是如果仅仅看图中红框所标示的位置其实又没有一个很大的区别,都是一些图片、文本、商品,无非是个数、展示项或者一些样式的不同,这些特性就是我们组件化的基础。我们对于这些页面中的最小元素(商品、图片、文本)称为原子组件,通过布局(即div)将不同的原子组件封装一起的整体我们称之为简单组件。
通过对页面展示内容的归纳总结,我们可以最终生成有限的几种原子组件,其他任意模块的展示都可以通过这几种原子组件+布局+css+js自动生成,一定程度上开发人员甚至可以不用介入基础的功能开发,这样大大提高了开发者的效率。
但是这种组件式的开发也有一定的局限性,这个是需要我们做好平衡:
1、原子组件的规范化代表着UI的同学在填充样式的时候也需要遵循同样的规范,某些时候这种规范也会制约着UI同学的发挥。
2、 组件的自动生成也同时代表着一些很个性的UI结构或设计实现起来会很麻烦,有时甚至需要变通才能达到预期的目的,比如说后期我们引进的【特殊组件】的开发,整体还是组件化的,不过其本身的开发规则和常规的开发形式就是一样的。
3、布局切割时造成的Div层级加深,这个在某些时候也会一定程度上影响渲染效果。
4、整体来说组件化是利大于弊的,在细节上的方案需要看各自产品线的侧重点的不同做对应的协调。
2. 数据生成机制的优化
a) 通过后端作业主动生成每个模块的数据,相对常规的通过用户被动触发生成数据,大大减少请求所损耗的时间,从而保证了页面在任何时间段内的稳定性。
b) 数据生成的同时会审核每个模块数据的有效性,如果无效我们将会保留上个版本的数据,这样保证了页面的健壮,无论何时都是一个正确的结果,不会出现开天窗的情况。
3. 数据加载的优化
a) 非首屏模块异步加载:通过ajax按需加载模块内容,提高用户体验的同时大大减少了页面的大小,可以提升整体页面的打开速度。如下图所示,只有到达当前窗口内容才会加载。
从监控截图,看通过这种处理,一周内的性能曲线以及可用性曲线基本上是一个平稳且较好的曲线。同时,首屏也基本上控制在3s内。
Html页面相关优化
textarea延迟渲染
textarea延迟渲染通过把不需要在首屏展示的html代码分模块放入一个一个的textarea里,大大减少了DOM节点数,从而给浏览器合理的喘息(UI Update)时间,等首屏真正在显示器上绘制出来后,再得到 textarea.text() ,填充回对应的DOM树。
textarea延迟渲染通,相对于其他延迟加载异步渲染解决方案,最大好处,还是减少首屏绘制时的DOM节点总数,从而降低页面首屏时间。
以当当首页的首屏轮播图为例
具体操作:
图片懒加载
现在页面内的图片越来越多,在图片加载时均采用了一种名为懒加载的方式,这是一个比较成熟的技术。具体表现为,当页面被请求时,只加载可视区域的图片,其它部分的图片则不加载,只有这些图片出现在可视区域时才会动态加载这些图片,从而节约了网络带宽和提高了初次加载的速度。
这个在卖场内的大部分页面都在使用,尤其是商品图上。以当当的列表页为例,其中大部分网络资源都是商品图片,针对这种情况我们一般都会使用图片的懒加载。
代码实现上我们目前使用的基于jquery的懒加载插件做的实现。
JS处理
Js压缩
Js放置尾部
若将 javascript 文件放到 head 里面,就意味着必须等到所有的 javascript 代码都被 下载、解析和执行完成 之后才开始呈现页面内容。这样就会造成呈现页面时出现明显的延迟,窗口一片空白。为避免这样的问题一般将全部 javascript 文件放到 body 元素中页面内容的后面。
网络相关优化
多域名服务
一般来说,浏览器对于相同域名的图片,最多用2-4个线程并行下载(不同浏览器的并发下载数是不同的)。而相同域名的其他图片,则要等到其他图片下载完后才会开始下载。
有时候,图片数据太多,通常的解决方法是将图片数据分到多个域名的服务器上,这在一方面是将服务器的请求压力分到多个硬件服务器上,另一方面,是利用了上述的浏览器的特性,从而提升了整个页面的加载速度。
当当内的商品图一般都是均匀的分布在10台图片服务器上。
DNS预解析
DNS 作为互联网的基础协议,其解析的速度似乎容易被开发人员忽视。现在大多数新浏览器已经针对DNS解析进行了优化,典型的一次DNS解析耗费20-120 毫秒,减少DNS解析时间和次数是个很好的优化方式。DNS Prefetching是具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能减少用户的等待时间,提升用户体验。
Chrome内置了DNS Prefetching技术, Firefox 3.5开始也引入了这一特性,由于Chrome和Firefox 本身对DNS预解析做了相应优化设置,所以设置DNS预解析的不良影响之一就是可能会降低Google Chrome浏览器及火狐Firefox 3.5浏览器的用户体验。
预解析的实现:
在页面header中使用link标签来强制对DNS预解析: 如下图
注:dns-prefetch需慎用,多页面重复DNS预解析会增加重复DNS查询次数。
URL链接合并
人工合并
这个就是人为的将一些通用的外链内容合并成一个文件,优点就是没有开发成本,缺点就是会增加一些人力成本。
自动合并
通过nginx-http-concat自动合并,它是一个淘宝的开源Nginx模块,是一个能把多个CSS和JS合并成一个请求的Nginx模块。优点是自动的,可以一劳永逸,缺点就是安装对应的nginx扩展,同时可能需要升级对应的nginx。
Github地址:https://github.com/alibaba/nginx-http-concat
CDN服务
所谓的CDN,就是一种内容分发网络,它采用智能路由和流量管理技术,及时发现能够给访问者提供最快响应的加速节点,并将访问者的请求导向到该加速节点,由该加速节点提供内容服务。通俗点说,就是你在北京访问的页面,它上面的资源就会优先读取北京本地的服务。
当然使用CDN有两个注意事项:
1、CDN加速服务很贵,如果你觉得你的网站值得加速,可以选择购买,目前来说当当的图片服务和一些静态资源都是使用的CDN服务;
2、CDN不适合局域性网站,比如你的网站只有某一个片区访问或者局域网访问,因为区域性网络本来就很近,无需CDN加速。
GZIP压缩
HTTP协议支持GZIP的压缩格式,通过GZIP压缩一定程度上减轻了服务器传输数据的压力。
可通过配置来自动将HTML信息压缩成GZIP,比如Nginx、tomcat。如果无法配置服务器级别的GZIP压缩机制,也可以改为程序压缩。
服务端相关优化
多级缓存策略
1.代码层缓存
Alternative Php Cache(APC)是 php 的一个免费公开的优化代码缓存。它用来提供免费,公开并且强健的架构来缓存和优化php 的中间代码。
卖场所有的php服务都默认开启apc缓存,以加快php的执行速度
2.内存数据缓存
不管是使用memcache缓存,还是redis缓存,根本目的是为了减少页面每次请求过程中所涉及到数据库操作、api操作以及逻辑处理上需花费的时间。
同时逻辑处理后生成的缓存数据是直接存储在服务器本身的内存上的,这样页面进行数据请求时的速度会更快,从而大大加速的页面本身的速度。
3.磁盘文件缓存
使用fastcgi_cache加速你的Nginx网站
应用场景:某次服装大促,预计PV为100万,前台服务器:4台,促销第一天晚21:00,监控centreon发现cpu达到90%左右,负载达到160左右,所以实行紧急方案,与其他前台域公用,增加了几台服务器,以此支撑大的访问量。但是第二天监控centreon发现cpu还是维持在30%左右,但是服装的促销页几乎就是个纯静态,整个页面请求时间才10ms,故从程序的角度优化的空间不大。那么如何降低cpu和负载呢?经过大家开会紧急讨论,决定采用ngnix的缓存机制。
nginx缓存机制
1. nginx有两种缓存机制:fastcgi_cache和proxy_cache
a) proxy_cache作用是缓存后端服务器的内容,可能是任何内容,包括静态的和动态的
b) fastcgi_cache作用是缓存fastcgi生成的内容,很多情况是php生成的动态内容
c) proxy_cache缓存减少了nginx与后端通信的次数,节省了传输时间和后端带宽
d) fastcgi_cache缓存减少了nginx与php的通信次数,更减轻了php和数据库的压力。
2. 在功能上,Nginx已经具备Squid所拥有的Web缓存加速功能、清除指定URL缓存的功能。而在性能上,Nginx对多核CPU的利用,胜过Squid不少。这使得一台Nginx可以同时作为“负载均衡服务器”与“Web缓存服务器”来使用。
3.所以可以根据实际情况结合使用proxy_cache和fastcgi_cache来架构Nginx的负载均衡系统。
4.针对本次服装大促,采用f5做负载均衡,前台服务器使用fastcgi缓存,效果非常好。
优化前后cpu和负载对比图
如下图所示,图1为9月11号晚,增加服务器后的cpu使用和负载情况。由图上可以清晰的看见,21:00,数据达到峰值,最高cpu为95%左右,最大负载160,增加完服务器以后,cpu维持在30%左右,负载为3.0。
图2为9月12日晚19:30,使用了fastcgi_cache,cpu急速降为5%以下,而负载低于0.3,效果非常明显。
下面重点说明一下fastcgi_cache的配置信息
http配置区(修改nginx.conf文件)
fastcgi_temp_path /data/fastcgi/tmp;
fastcgi_cache_path /data/fastcgi/cachelevels=1:2 keys_zone=one:100m inactive=3m max_size=1g;
server配置区--修改站点文件
fastcgi_cache one;
fastcgi_cache_valid 200 302 3m;
fastcgi_cache_key$request_method$request_uri;
各个参数的含义:
1. fastcgi_temp_path:生成fastcgi_cache临时文件目录
2. fastcgi_cache_path:fastcgi_cache缓存目录,可以设置目录哈希层级
3. keys_zone是这个缓存空间的名字,内存缓存空间大小为100MB
4. inactive:表示默认失效时间
5. max_size:表示最多用多少硬盘空间
6. 需要注意的是fastcgi_cache缓存是先写在fastcgi_temp_path再移到fastcgi_cache_path,所以这两个目录最好在同一个分区
7. fastcgi_cache_valid:定义哪些http头要缓存
8. fastcgi_cache_key:定义fastcgi_cache的key,Nginx会取这个key的md5作为缓存文件
9. fastcgi_cache:使用哪个缓存空间
如何手动清除缓存
Nginx的第三方扩展:https://github.com/FRiCKLE/ngx_cache_purge/
自己动手写个清除的脚本,也非常简单
并发处理策略
问题:
一个页面有10处基于http的API调用,由于产品设计限定,有3-5个API中的数据必须在页面首次显示中加载,该怎么办?
方案:
1. API同步请求,api顺序依次调用,加起来的总时间会很长,会导致页面加载时间轻松超过5s,严重影响用户体验;
2. 在服务器端实现API异步请求,API调用总耗时将等于几个api中最慢的哪个所好时间,可大幅降低页面加载时间;
3. 在前端通过ajax实现异步请求,也可大幅降低页面加载时间,但部分重要数据为了能够首屏展示,则不能放置在Ajax中做请求处理。
通过在页面中混合使用2和3方案,将会给页面加载速度和用户体验带来更好的提升,下来重点解释方案2的实现:
方案2实现细节:
PHP语言本身并不能像js那样具备异步回调的特性,要想实现异步请求的功能,需要用到libcurl库,需要用到curl_multi_*相关的一些PHP函数,具体步骤如下:
1.初始化一个请求队列:
$queue = curl_multi_init();
2. 设置每个请求的参数:
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1); #如果是post方法
curl_setopt($ch, CURLOPT_POSTFIELDS,$params); #设置post参数
curl_setopt($ch, CURLOPT_TIMEOUT,$delay);
...
3. 将每个请求压入请求队列,做好相关映射
curl_multi_add_handle($queue, $ch);
$map[(string) $ch] = $request; //$request['request'];
4.发送请求,等待返回结果
do {
while (($code =curl_multi_exec($queue, $active)) == CURLM_CALL_MULTI_PERFORM);
if ($code = CURLM_OK) {
break;
}
// a request was just completed --find out which one
while ($done =curl_multi_info_read($queue)) { ... }
}while ($active);
curl_multi_close($queue);
面向对象的设计:
为了对实现对底层细节的封装,设计一套interface,对于其他开发人员只需实现这套接口即可,无需关系curl_multi_***底层细节:
interface iMultiCurl {
public function get_url();
public function get_method();
public function get_params();
public function callback($result,$info);
public function load();
}
interface具体实现例子:
class accountDataHandler implementsiMultiCurl {
public function get_url() { return Conf::$cashUrl['url'] ;
}
public function get_method() {
return "GET";
} public functionget_params() {
...
}
public function callback($resultXml,$info) {
......
} }
该方案的缺点:不太容易实现对每个请求做独立缓存
目前这种请求方式已经在我们的店铺项目中应用,效果不错,大大降低的整体api的请求时长。
结尾语:
性能的优化是永无止境的,也不能采用单一的方式来度量它,有时很小的一个改变就可以做到一个较大的提升,但是如何从好做到更好就需要大家处理好每一个环节。最后,给大家提供下卖场系统的整体架构图,以供大家参考。
Q&A
问:当当的浏览器页面缓存如何做的?如果静态资源更新发布后又是如何刷新缓存的?
江培水:目前来说我们使用的还是版本控制。
问:服务器并发那里是因为那服务只能走http么,既然在服务端了是不是可以不用再走http请求?
江培水:对于这个问题需要看具体使用的场景,目前来说多数都是soa的形式,后端以api的形式提供给各系统调用,还是http的请求居多。
问:对于js和css的缓存当当有做处理么?
江培水:有,我们都将常用的公用的js和css放置CDN服务器上。
问:当当用bigpipe了吗?或者做过调研,如何评价这种技术?
江培水:当当目前没有用这个,不过我自己到有些想法,对这个要看具体的使用场景,使用bigpipe那后端数据需要怎么处理了?公共数据部分也需要每次都发生请求吗?这个算不上回答,也是我的疑惑。
问:当当是怎么做各项性能统计与分析的?
江培水:首先我们有自己的平台,会做一些统计,但是基于竞争对手的分析我们还是采用的统一的第三方平台【听云】,这个平台会告诉使用方具体慢的散点和一些基础数据
问:当当是怎么做各项性能统计与分析的?
江培水:比如我截图里的首屏时间和性能时间都是这个系统提供的,当然如果某天慢了我们就需要看听云上的具体散点数据,是网络?还是DNS?还是图片?
问:关于静态资源cdn的替换,是用的开源的插件,还是有内部系统统一处理?
江培水:通过版本控制,比如说商品图片,每次更新都会有不同的版本。
问:有做错误监控吗,可以分享下方案吗?
江培水:对于前端,听云的监控本身会关注坏点;api方面我们通过统一的api监控它的访问时长、波动、异常次数。
版权申明:内容来源网络,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
了解更多技术,欢迎关注下方公众号
网友评论