本文曾发表于2013年4月的《程序员》杂志
近年来,随着用户数和PV的增加,淘宝网的后端服务器数量增长很快;并且我们知道,Web页面延迟时间和转化率之间有着直接的关联。出于提升系统吞吐量、降低成本、减少页面延迟、提升用户浏览体验、提高交易转化率的考虑,淘宝网在性能优化领域做了很多尝试。本文将从应用性能分析、基础设施优化、应用自身优化、前端性能优化这四个方面,对淘宝网的优化尝试做一个总结。
应用性能分析
1.前台应用介绍
淘宝网前台应用是指商品详情、店铺、购物车等买家直接可以看到和使用的应用,这类应用PV较高,服务器数量较多。从技术实现来说,淘宝前台应用都使用Velocity模板引擎渲染HTML,页面平均大小大于100KB,WebServer不保存数据,数据来自于后端的DB、RPC服务、消息中间件、Tair、SearchEngine、TFS等外部系统,除了写日志、读取配置和共通模板,磁盘读写很少,而相对于后端系统来说可处理的最大吞吐量较低,单台虚拟机平均TPS不到200。根据分析,这些应用都属于CPU密集型应用。
2.度量关键指标
优化工作开始前,要先给系统做次体检,拿到关键指标,然后针对关键指标进行优化,这样在优化工作完成后,更容易度量成果。关键指标有吞吐量、页面大小、响应时间和每请求内存数。
A.吞吐量通过线上环境单机压测,可以得到服务器预设最大负载情况下,应用单机的最高真实吞吐量。线上压测的方法有两种:对于所有HTTP请求都具备幂等性的系统,可以使用开源的AB、http_load等工具,回放前一天的流量给服务器,逐渐增加压力,当系统负载达到预设值时就得到了应用当前最高吞吐量;对于所有HTTP请求不完全具备幂等性的系统,可以采用Nginx引流的方式,将其他服务器的流量引到同一台服务器,以达到增加负载、得到最高吞吐量的目的。B.页面大小、响应时间页面大小和HTTP请求响应时间可以通过分析服务器访问日志得到。C.每请求内存数淘宝前台应用都是Java应用,内存使用较多,垃圾回收很容易成为瓶颈,所以设定这一指标来衡量应用的内存使用情况。每请求内存数的计算方法是:JVM单位时间内申请的内存/服务器单位时间内处理的请求数。3.查找应用瓶颈
瓶颈是系统中比较慢的部分,在瓶颈完成前其他部分需要等待,所以优化工作可以从分析瓶颈开始;应用代码的执行也符合2/8原则,即20%的代码执行会消耗80%的资源,找到这20%的代码去做优化才会有效果。自底向上,查找应用瓶颈可以分为下面几个部分:
A.系统瓶颈使用top、sar、vmstat、mpstat、iostat等系统工具、JDK源生工具去分析CPU、IO、Memory在压测时的表现,关注当前进程的Thread、锁、打开File数、Socket数、GC表现等情况,看哪一块存在问题或者会先成为瓶颈;对于关键代码,使用Perf等工具查看热点和CPU缓存命中率。经过分析,CPU计算的通用瓶颈在字符串的查找、拼接、替换,字符字节的编码、解码转换,压缩、解压缩操作,外部调用的瓶颈在IO开销、序列化和反序列化操作。B.代码瓶颈对于运行态的Java代码,业界有很多工具可以用来查找瓶颈,比如收费的JProfiler、YourKit等,免费的TPTP、NetBeansProfiler、VisualVM等,我们使用淘宝开发的适合线上运行的TProfiler工具(已开源),同时支持剖析和采样两种方式,可以得到对象创建排行和Java代码执行次数、执行时间排行。排在前边的热点代码,极有可能就是代码瓶颈所在,如下表所示:方法信息 | 执行时间 | 执行次数 | 总时间 |
com/xxx/web/core/NewList:execute() | 61 | 3102 | 190067 |
com/xxx/web/core/PerformScreen:performScreen() | 18 | 4822 | 87822 |
com/xxx/core/DefaultSearchAuction:doMultiSearch() | 43 | 708 | 30357 |
com/xxx/core/DefaultSearchCatManager:doSearch() | 4 | 1248 | 4552 |
C.模块瓶颈
基于前边的热点代码排行数据按模块做归类统计,可以得出每一个模块的CPU资源消耗比重,如下图所示:这样就可以得出:Velocity模板引擎是此应用的瓶颈模块,需要着重优化。
基础设施优化
1.软件升级
淘宝之前的Web应用构建在Apache+mod_jk+JBoss4之上,软件栈相对陈旧,新版本的特性和优化也无法利用。做了一次大的升级后,变成现在的Tengine+Tomcat7,在一些应用上实测吞吐量有近10%的提高,也验证了Nginx使用epollIO模型带来的优越性能。操作系统由原来的32位升级为64位,可识别的内存变大,增加内存后调大新生代堆大小4倍,某应用吞吐量提升达到70%,可见新生代大小对应用吞吐量非常的重要;淘宝有专门的JVM团队和Linux内核团队,使用taobao-jdk(补丁开源)、淘宝内核、开启JVM大内存页后实测,某应用吞吐量提升40%;TCP初始拥塞窗口调优,对用户平均下载时间也有不错的提升。在当前开源软件百花齐放的形势下,升级基础软件投入不大,却能给系统性能带来较大提升,非常划算。唯一要面对的问题是升级周期会比较长,因为线上环境需要长时间的beta以保证新软件的稳定。
2.JVM调优
根据前面的分析和实践,吞吐量与GC有直接的关系,在页面大小不变的情况下,调大新生代有益于提升吞吐量,减小页面大小(每请求内存数)也能提升吞吐量。目前JDK7已经发布,但G1垃圾回收器还在开发中,经过我们测试在GC表现上G1没有比CMS更好,所以目前还是选择响应时间优先的CMS垃圾回收器。我们的JVM部分行为参数和性能参数如下:
-Xms4g-Xmx4g-XX:PermSize=256m-XX:MaxPermSize=256m-Xmn2000m-XX:SurvivorRatio=10-XX:+UseConcMarkSweepGC-XX:+UseCMSCompactAtFullCollection-XX:+CMSParallelRemarkEnabled-XX:+CMSPermGenSweepingEnabled-XX:+CMSClassUnloadingEnabled-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction=82
除了基本的配置还可以做一些参数调优,比如在6u23之后默认开启的压缩指针,随着JDK7发布带来的分层编译、大内存页、逃逸分析等非常值得尝试的优化。除了参数调优,应用代码本身也可以调整,使其对GC更友好。在CMS垃圾回收机制下,MinorGC时业务线程会暂停25ms左右,MajorGC时业务线程会暂停500ms左右。用户的请求被暂停500ms是不能接受的,所以优化原则就是减少MajorGC,也就是减少Young区晋升到Tenured区的对象数。可以通过JVM源生工具jstat观察JVM各个分区间对象的迁移情况,然后合理分配堆每一个分区的大小、调整TenuringThreshold阀值。应用对象管理要尽可能缩短对象生命周期或尽可能少创建新对象,减少页面模板大小也是一个可行的办法。我们开发了TBJMap工具(已开源),可以分析JVM堆每一个分区里都有哪些内容,这对于优化应用代码非常具有参考价值。JVM性能表现的最佳状态是没有MajorGC,在淘宝有些应用已经做到了这点。
3.二方包优化
每一个工程都依赖很多jar包,这些jar包如果用的比较频繁对性能的影响至关重要,对二方包的优化不会随着业务代码的改变而性能下降,可以说一次优化永久受益。二方包优化有两个建议:可以做一次的工作不做多次,在beancopy的场景下使用CGLib代替BeanUtils,性能有超过20倍的提升;可以提前做的工作提前做,IP库二方包优化过程中把很多冗余操作提前处理掉,性能有接近1倍的提升。在技术选型时可以针对场景选择更优的二方包,比如LMAX-Exchange开源的高性能并发框架Disruptor。另外,如果改变了原来的二方包,代码不能提交回主干,将来会遇到版本升级困难的问题。
4.模板引擎优化
通过前面的分析可知,Velocity模板渲染是最大的模块瓶颈,除了减小模板大小,还可以从模板框架优化下手。因为Velocity是解释型语言,性能相对较差;执行过程中还有大量的反射调用,效率可想而知;字符字节的转换也尤其消耗CPU。淘宝基于Velocity开发了语法兼容的Sketch框架,将Velocity模板编译成Java类执行,减少了反射调用,内部用字节存储页面,节约了从渲染到输出的两次编码转换。使用Sketch框架以后,很多应用整体吞吐量有超过20%的提升。另外,淘宝Sketch框架将于今年开源。
应用自身优化1.压缩模板大小
在很多系统中,模板大小和吞吐量成反比,如果能大幅减小模板和HTML的大小,会给吞吐量带来很大提升。最简单的减小模板方法,就是删除空行和多余空格。对于URL比较多的页面,去掉“http:”这五个可省略的字符、长URL压缩、用URL别名代替全链接都可以带来不错的效果。如果for循环里重复数据较多,可以把数据移到for循环之外,多次出现的只渲染一次,浏览器端渲染时再通过前端代码把重复内容放回去,这种业务上的去重,往往能带来很好的优化效果。
2.设置最佳并发
并发用户数、资源利用率、吞吐量和响应时间的关系可以参考下图:
当服务器处于低负载区,随着并发用户数的增加,资源利用率和吞吐量直线上升,响应时间没有明显的变化;当服务器处于高负载区,随着并发用户数的增加,资源利用率趋于饱和,吞吐量达到最高点后开始下降,响应时间开始有明显的增加;这时并发用户数继续增加,服务器则处于假死状态,资源利用率继续趋于饱和,吞吐量开始急剧下降,响应时间开始急剧上升,直到系统不能处理任何请求,我们称之为服务器Down机。从这张图里我们可以得出,服务器吞吐量最大的时候对应的并发用户数,就是这个服务器的最佳并发数,当并发用户数大于这个值的时候,系统服务能力开始明显下降,做优化要找到这个最佳并发数,通过稳定性模块设置到系统中,稳定性模块可以对超过最佳并发数的请求进行限流,以保证系统达到最好的性能表现,不会因为大流量冲击而垮掉。我们一般通过线上压测来确定系统最佳并发数,对于CPU密集型应用也可以用如下公式计算:
最佳并发=((CPU时间+CPU等待时间)/CPU时间)*CPU核数
3.代码瓶颈优化
前面介绍了如何找到影响性能的瓶颈代码,可以针对代码瓶颈进行优化。举个例子,经过分析发现某系统每个请求都抛异常吞异常,导致服务器资源利用率上不去,吞吐量不高,修正后CPU使用率提高30%,系统吞吐量也提升近30%。抛JDK默认的异常比较影响性能,尤其是在线程调用栈很深的情况下,有的系统还使用异常作业务流程控制,有的异常直接被吞掉,危害都很大。taobao-jdk开发了异常监测功能,从JVM层面直接发现和暴露所有异常问题,杜绝了这一类瓶颈的出现。
4.外部调用优化
淘宝系统目前处于第三代分布式架构,为了优化外部调用,开发了并行RPC、并行搜索等功能,对于适合的场景可以有效降低响应时间。某些场景使用更优的ProtocolBuffers序列化框架,在某些对性能要求很高的场景,使用开发成本稍大、比ProtocolBuffers还快20%的Kryo框架。
5.面向CPU编程
对于CPU密集型应用,如果能减少CPU的使用则可以直接提升系统吞吐量。针对Web应用可以调低GZip压缩级别来降低HTTPServer对CPU的消耗。针对核心代码可以面向CPU编程:经常一起使用的Field可以放在一起,这样对CPU缓存比较友好;在多核服务器上对性能要求比较高的场景,可以补齐缓存行以减少伪共享的发生;按行处理不要按列处理数组,编写符合空间局部性的代码可以很好地提升性能;使用源生批量接口处理数组,这样一条CPU指令就可以完成操作;使用乐观策略(CAS)来代替同步和锁也可以有效提升性能。
6.架构优化
架构调整往往要对系统伤筋动骨,开发周期很长,但却可以带来最好的优化效果。列举几个我们常用的架构优化方法:动态资源静态化,把需要服务器动态生成、更新不频繁的内容CDN化,内容变化了可以回源更新CDN,这样大幅减少了服务器的动态内容输出;后台依赖前台化,给后端服务暴露对外的HTTP接口,使服务器依赖转变成JS依赖,可以提升后端性能,并且把强依赖变成弱依赖,提升整个系统的稳定性;后端渲染前端化,对于数据远小于面,页面布局比较规则的场景适用;DB依赖缓存化,这点业界用的非常之多;善用缓存,针对不同的场景可以缓存对象、缓存页面片段、缓存整个页面、缓存HTTP响应,使用缓存需要关注失效机制和数据预热,并尽可能提高缓存命中率。
前端性能优化
1.度量关键指标
我们可以通过前端埋点和NavigationTiming接口来采集网页在用户浏览器上的关键指标,包括DNS查询时间、TCP连接建立时间、HTTP请求时间、页面下载时间、开始渲染时间、domReady、可交互时间、onLoad时间,使用阿里度等工具可以得到首屏时间。有了这些指标就可以衡量前端优化的效果。业界还有一些工具会给出很多优化建议,比如dynaTraceAJAXEdition、YSlow、Chrome插件SpeedTracer等,淘宝也根据Yahoo的34条军规,开发了自己的TSlow。
2.前端性能优化
Yahoo34条军规已经成为前端WPO的标准,这里不再介绍。列几个对我们的场景比较适用的优化:减小Cookie大小;减少DNS查询并适当增加不同域名以优化资源并发加载;针对浏览器渲染,减少Dom数、按需延迟加载、次要信息异步化加载、使用BigRender技术控制渲染节奏优化首屏时间、使用前端模板引擎进行页面渲染。某应用后端数据大小是前端页面大小的1/10,如果只传数据不传页面可以大幅节省页面下载时间,我们采用前端渲染方案优化后,系统响应时间减少25%、页面大小减小60%、domReady时间减少60%、onLoad时间减少70%。在前端性能优化领域,Google一直在引领潮流,基于Chrome这个入口推动很多优化落地、推出PageSpeed、WebP、SPDY等技术,带给我们很多新的方向。