前言本文也发表在我的个人博客,欢迎访问:没有profile的节点性能优化,谈优化都是耍流氓,性能优化的大前提是profile,有数据才知道程序慢在哪里。本文主要介绍Node后端的性能优化。前端同学可以看看Chrome的devtoolshttps://github.com/CN-Chrome-...1.Web应用优化性能的瓶颈往往在IOIO层优化磁盘IO为什么常见的IO速度慢计算机是:CPU一级二级缓存内存硬盘网络硬盘IO开销是非常昂贵的,硬盘IO花费的CPU时钟周期是内存的41000000/250=164000倍。在一般的应用中,优化首先要考虑磁盘IO,通常是数据层的优化。说到数据库优化,很多人第一时间想到的就是加索引,但是为什么加索引会查询的更快呢?索引应该怎么加?索引为什么快关于索引的原理,可以看这篇文章,索引原理。索引快的主要原因是索引占用空间少,可以有效减少磁盘IO次数。索引可以使用查询起来方便快捷的数据结构,比如b+树。如何添加索引?回到我们的话题。没有简介就谈优化,简直是耍流氓。以mongo为例。Mongo有慢查询功能。MongoDB查询优化分析本文介绍如何启用和使用mongo的慢查询功能。开启慢查询收集功能后,使用db.system.profile.find().pretty()语句找出哪些语句是慢的。以下面的查询语句为例:querynew_koala.llbrandomredpackagequery:{user_id:"56ddb33e23db696f89fdae2a",status:{$ne:1}}查询条件为user_id和status,所以给这两个字段加索引可以提高查询速度.当然,如果mongo不先启动慢查询,扫描mongo.log也是一种方式。grep'[0-9][0-9][0-9]ms'/var/log/mongodb/mongodb.log这样就可以找到所有查询时间大于100ms的记录。然后就可以对症下药了。缓存是有选择地使用它的好方法。上面提到内存IO比磁盘IO快很多,所以使用内存来缓存数据是一种有效的优化方法。常用的工具如redis、memcached等,缓存效果显着,所以很多时候说到优化,很多人都会想到加缓存,但是使用缓存是有代价的。你需要维护缓存的更新和失效。这是一件很麻烦的事情。使用缓存后,你会经常遇到缓存没有及时更新导致的问题。重要的是要多说几遍:缓存有副作用。缓存有副作用。缓存有副作用。并非所有数据都需要缓存。如果访问频率高,生成成本比较高,考虑是否缓存。也就是说如果影响到你的性能瓶颈就考虑缓存。.而且缓存还存在缓存雪崩、缓存穿透等问题需要解决。参见CachePenetration和CacheAvalancheStaticFileCaching图片、js文件等静态文件是不可变的,非常适合做缓存。常见的静态文件缓存服务包括nginx、vanish等。代码层面的优化。合并查询在这部分代码中,经常做的是将多个查询合并为一个,消除for循环,实际上减少了数据库查询。比如对于userIds中的user_idvaraccount=user_account.findOne(user_id)其实可以改写为:varuser_account_map={}//注意这个对象会消耗很多内存。user_account.find(user_idinuser_ids).forEach(account){user_account_map[account.user_id]=account}foruser_idinuserIdsvaraccount=user_account_map[user_id]这将N个查询合并为一个。其实就是减少IO。在过早优化和性能优化上做了太多的工作后,你往往会陷入一种什么都想优化的状态,所以你可能会陷入过早优化的深坑。这里参考别人的观点https://www.zhihu.com/questio...2.内存泄漏排查Node是基于V8js引擎的。到这里我们就了解了V8中内存相关的知识。V8的GC垃圾回收机制V8的内存分代在V8中,内存主要分为两代:新生代和老年代。新生代中的对象是存活时间相对较短的对象,老年代中的对象是存活时间较长或常驻内存的对象。默认情况下,新生代内存最大值在64位系统上为32MB,在32位系统上为16MB。V8的内存最大值在64位系统上为1464MB,在32位系统上为732MB。为什么要分两代呢?它用于最佳GC算法。新一代GC算法Scavenge速度快,但不适合大数据量;oldgeneration采用Mark-Sweep(标记去除)&Mark-Compact(标记压缩)算法,适用于大数据量,但速度较慢。使用更适合老年代和新生代的算法来优化GC速度。详见《深入浅出 nodejs》5.1V8的垃圾回收机制和内存限制在启动程序时在V8的GC日志中添加--trace_gc参数。V8在进行垃圾回收时,会打印出垃圾回收信息:?$node--trace_gcaa.js...[94036]68毫秒:清除8.4(42.5)->8.2(43.5)MB,2.4毫秒[分配失败]。[94036]74毫秒:清除8.9(43.5)->8.9(46.5)MB,5.1毫秒[分配失败]。[94036]由于高提升率,将标记速度提高到3[94036]85毫秒:清除16.1(46.5)->15.7(47.5)MB,3.8毫秒(自上次GC以来的106个步骤中+5.0毫秒)[分配失败]。[94036]95毫秒:清除16.7(47.5)->16.6(54.5)MB,7.2毫秒(自上次GC以来的14个步骤中增加1.3毫秒)[分配失败]。[94036]111毫秒:标记-清除23.6(54.5)->23.2(54.5)MB,6.2毫秒(自标记开始以来222步中+15.3毫秒,最大步长0.3毫秒)[GC中断][请求旧空间中的GC]....V8提供了很多程序启动选项:启动项含义–max-stack-size设置栈大小–v8-options打印V8相关命令–trace-bailout查找不能优化的函数,重写–trace-deopt查找函数无法优化的函数使用memwatch模块来检测内存泄漏。npm模块memwatch是一个非常好的内存泄漏检查工具,让我们先将这个模块安装到我们的应用程序中,执行以下命令:npminstall--savememwatch然后,在我们的代码中,添加:varmemwatch=require('memwatch');然后监听泄漏事件memwatch.on('leak',function(info){console.error('Memoryleakdetected:',info);});因此,当我们执行我们的测试代码时,我们将看到以下信息:{开始:2015年1月2日星期五10:38:49GMT+0000(GMT),结束:2015年1月2日星期五10:38:50GMT+0000(GMT)),growth:7620560,reason:'heapgrowthover5consecutiveGCs(1s)--2147483648bytes/hr'}memmemwatch发现内存泄漏!memwatch判断内存泄漏事件发生的规则如下:当你的堆内存在连续5个垃圾回收周期内持续增长,则派发内存泄漏事件。详情参见memwatch使用heapdumpdumpoutNode申请内存当snapshot检测到内存泄漏时,我们需要查看当时内存的状态,heapdump可以抓取当时内存的snapshotmemwatch.on('leak',function(info){console.error(info);varfile='/tmp/myapp-'+process.pid+'-'+Date.now()+'.heapsnapshot';heapdump.writeSnapshot(文件,function(err){if(err)console.error(err);elseconsole.error('Wrotesnapshot:'+file);});});运行我们的代码,会在磁盘/tmp目录下生成一些.heapsnapshot文件。使用Chrome的开发者工具分析内存消耗heapdump提供的内存快照可以通过Chrome的开发者工具查看。将.heapsnapshot文件导入Chrome开发者工具如何使用内存分析工具?Chrome开发者工具的JavaScript内存分析本文档详细介绍了如何使用开发者工具来分析内存使用情况。大家可以参考一下,这里就不赘述了。举个例子,使用比较视图。对比视图demo可以看出,通过对比前后的内存变化来找出内存泄漏的原因是非常简单方便的。然而,理想是美好的,现实却是残酷的。下面是日常开发中dump下的数据。使用对比视图:可见数组是内存增长的罪魁祸首,但我们只能得到这个线索,那么哪些数组消耗内存呢?点击数组查看详细信息:匿名数组很多,无法准确找出哪些数组占用内存。主要是后台使用了web框架sails,代码量大,干扰项太多,无法准确判断是哪些功能有问题。内存泄漏的原因一般来说,内存泄漏的原因有以下几种。谨慎使用内存作为缓存。如果不用,控制好缓存的大小和过期时间,防止出现永远无法释放的问题。队列消费不及时,数组、回调、生产者比消费者快,大量生产者堆积。不能释放的作用域或者变量作用域没有释放,不能立即回收的内存包括全局变量和闭包。尝试使用变量赋值为null|undefined来触发回收。这部分的详细解释请参考《深入浅出 nodejs》5.4内存泄漏。3.优化应用的CPU瓶颈。上面介绍了IO优化和内存优化。如果使用Node作为后端,会经常遇到CPU瓶颈。众所周知,Node是单线程的,所以不太能胜任CPU密集型操作,所以应该避免使用Node进行CPU密集型操作。那么出现类似CPU的问题怎么办呢?v8log:添加--prof参数,在应用结束时收集日志。执行命令后,会在该目录下生成一个日志文件*-v8.log。我们可以安装一个日志分析工具ticktick工具来分析日志来分析每个函数的处理时间。?$sudonpminstalltick-g?$node-tick-processor*-v8.log[Topdown(heavy)profile]:注意:不显示占用小于0.1%的被调用者。inclusiveselfnametickstotaltickstotal42636.7%00.0%函数:~
