http://www.rockdai.com/?p=596
上午在微博上看到@Python发烧友 转发了这篇来自LinkedIn团队的文章《Blazing fast node.js: 10 performance tips from LinkedIn Mobile》。看过后觉得非常有参考价值,于是午休后花了一点时间将它翻译成中文,希望能给不习惯英文阅读的朋友带来一定方便。
以前做翻译的经验较少,处理得不好的地方还望海涵。:-)
一、避免使用同步代码(Avoid synchronous code)
Node.js是单线程的。为了使单线程可以处理多个并发请求,我们不应该让线程来等待一个阻塞的、同步的或是长时间运行的操作。Node.js的一个突出的特性就是它被完全地设计并实现为异步的。这使得其可以非常好地适用于事件驱动的程序。
遗憾的是,我们仍然有可能使用到同步/阻塞的调用。例如,很多对文件系统的操作都提供了同步和异步的两个版本,就像writeFile和writeFileSync。有时即使我们没有在自己的代码中使用同步的方法,但仍然有可能在无意中使用了包含阻塞调用的某个外部库。当您使用到同步/阻塞调用时,对性能的影响是巨大的。
2 | fs.writeFile( 'message.txt' , 'Hello Node' , function (err) { |
3 | console.log( "It's saved and the server remains responsive!" ); |
7 | fs.writeFileSync( 'message.txt' , 'Hello Node' ); |
8 | console.log( "It's saved, but you just blocked ALL requests!" ); |
我们最初的日志系统的实现中,不慎使用了一个写磁盘的同步调用。直到我们做性能测试之前这个情况一直没有被我们注意到。当我们在开发环境中对一个单实例的Node.js程序作基准测试的时候,这个同步调用使得吞吐率(requests per second)从几千一下子下降到了几十!
二、关闭socket池(Turn off socket pooling)
Node.js的http client自动使用socket池:在默认情况下,主机能同时并发打开的socket数量被限制为5个。虽然socket的重用也许能控制资源消耗的增长,但当我们需要处理多个从同一主机获取数据的并发请求时,这将是一个严重的瓶颈。在这些情况下,好的解决方案是增加maxSockets或者完全禁用socket池:
3 | var http = require( 'http' ); |
6 | var req = http.request(options) |
三、不要用Node.js处理静态文件(Don’t use Node.js for static assets)
对于静态文件,例如CSS和图片,建议使用标准的webserver来处理而非Node.js。例如LinkedIn mobiles使用的是nginx。并且我们使用CDN,将静态文件复制到遍布于全世界的各个节点。以上这些将带来两个好处:1、可以减轻Node.js服务器的负载;2、借助于CDN使得静态文件可以从离用户更近的节点传输予之,从而降低时延。
四、将渲染放到客户端做(Render on the client-side)
让我们来简单地对在服务器端和在客户端渲染一个页面进行一个比较。如果我们使用Node.js在服务器端进行渲染,在每次请求中我们将返回以下的HTML页面:
06 | < title >LinkedIn Mobile</ title > |
请注意到这个页面上除了用户的名称之外的所有东西都是静态的:也就是说,这些对于每个用户以及每次页面重载都是相同的。因此,一个更高效的做法是让Node.js以JSON格式只返回页面所需要的动态数据:
页面的其余部分——所有的静态HTML标记——可以放入一个JavaScript模板(譬如一个underscore.js模板):
06 | < title >LinkedIn Mobile</ title > |
这样做的性能提升来自前述的第三条建议,静态的JavaScript模板可以用我们的webserver(譬如nginx)来处理,甚至是使用CDN就更好了。此外,JavaScript模板可以被浏览器缓存或者保存在LocalStorage中。这样一来在页面第一次被加载后,需要发送给客户端的数据就只有动态的JSON了,极大地提升了效率。这个方法大大降低了Node.js的CPU、I/O消耗和负载。
五、使用gzip(Use gzip)
大多数的服务器和客户端程序都支持用gzip来压缩请求和响应。请确保无论是在发送响应到客户端还是向远程服务器发送请求时,您都充分地利用到了它。
六、并行化(Go parallel)
尝试着将类似请求远程服务器、调用数据库和访问文件系统等将会引致阻塞的操作并行地执行。这样做的好处是,可以将完成一系列阻塞操作的总时延减小到其中最慢的某项操作所需要的时间,否则其总时延将会是完成操作序列中所有操作需要等待的时间总和。为了保持回调和错误处理函数的结构明晰,我们使用了Step来做flow control。
七、去session化(Go session-free)
LinkedIn mobile使用Express框架来处理请求/响应周期(request/response cycle)。大多数express的示例代码中包括了以下的配置:
1 | app.use(express.session({ secret: "keyboard cat" })); |
在默认情况下session数据存储在内存中,在用户数量增加的时候这会显著地增加服务器的开销。我们可以转而使用一个外部的session存储方案,例如MongoDB或Redis,但此时每个请求将增加远程调用获取session数据带来的额外开销。有可能的话,最好的选择是完全不在服务器端存储状态信息。不进行上述的express配置,不保存session,这样可以获得更好的性能。
八、使用二进制的模块(Use binary modules)
请尽可能地使用编译后的二进制模块而非用JavaScript编写的模块。举个例子,当我们从一个用JavaScript编写的SHA模块切换到Node.js内置的编译版本后,我们看到了巨大的性能提升:
2 | var crypto = require( 'crypto' ); |
3 | var hash = crypto.createHmac( "sha1" ,key).update(signatureBase).digest( "base64" ); |
九、使用标准的V8 JavaScript而非客户端的库(Use standard V8 JavaScript instead of client-side libraries)
JavaScript的版本不统一,而大部分JavaScript库是提供给web浏览器使用的:例如一款浏览器或许支持类似forEach、map和reduce这样的函数,但其他浏览器并不支持。其结果是,客户端的库常常要用很多低效的代码来掩盖浏览器间的差异。另一方面,在使用Node.js时你确切地知道哪些JavaScript函数是可用的:驱动Node.js的V8 JavaScript引擎实现的是ECMAScript的ECMA-262,第五版。通过直接使用标准的V8函数,而不是客户端的库,您将会再一次体验到显著的性能提升。
十、保持代码精简、轻量(Keep your code small and light)
面向移动设备进行开发时,设备的速度更慢而且时延更高,这迫使我们保持代码精简、轻量。您同样也应该把这种思想带到服务器端代码的开发中。时不时地进行自省,并且问自己:“我们是否真的需要这个模块?”,“为什么我们要使用这个框架?它带来的开销是值得的吗?”,“我们能否用一种更简单的方法来实现?”。更精简,更轻量的代码通常也是更高效,更快的。