Node.js/JavaScript 语言调研

19 Jun 2014

这篇文章更多的是从宏观的、语言的层面分析 Node.js,原来写在公司内部的 wiki 上。

##Node.js 是什么?为什么会诞生这样的东西? Node.js 是一个服务器端 JavaScript 解释器。发明者 Ryan Dahl 需要一个高性能的后端语言,在考察了几个语言之后,发现 JavaScript 没有 IO 处理接口,这就意味着他可以从头设计一套高性能单线程非阻塞接口,还不用处理其他语言接口的历史包袱,于是 Node.js 就诞生了。

这种单线程异步的方式比原来的多线程或多进程具备更高的性能,但仅仅是性能还不足以让这门语言得到大规模的应用,Node.js 采用 JavaScript 作为基础语言,这使得 Node 编程的入门门槛很低。Node 代码精简,功能 focus,文档详尽,包管理系统非常便于使用。各种优秀的特性,以及不陌生的开发语言使得 Node.js 成为前端敏捷开发的首选后端语言。

##为什么 Node.js 会流行

  1. JavaScript 代码规模的扩大,传统前端职责后延,使用 Node.js 上手快,门槛低。
  2. Node.js 有很好的包管理系统,第三方库丰富。
  3. 适合用于开发有大量短生命期请求,使用 RESTful API 的应用。。可以很简单的实现并发获取数据,对 JSON 对象也有原生支持。
  4. 在高并发下响应性能的更好一些。
  5. Node.js 可以创建实时性很高的服务 ,比如 web 聊天应用。

##性能对比 理论上,单线程异步的 Node.js 比多线程的 Apache + PHP 有更好的性能。在 PHP 中,每个连接都会生成一个新线程,(并为其分配一些配套内存),而 Node.js 解决这个问题的方法是:更改连接到服务器的方式。每个连接触发一个在 Node.js 引擎的进程中运行的事件。通过使用事件驱动模型,Node.js 不使用锁,也不需要为每个连接新开一个线程。是的上下文切换的代价大大减小,而且它不会直接阻塞 I/O 调用

我们简单测试一下这两者的性能。

###测试语言/脚本

Node.js

var sys = require('sys'),
http = require('http');
  
http.createServer(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
 
    res.write('<p>Hello World</p>');
    res.end();
}).listen(8080);

PHP

<?php 
    header("Content-Type: text/html");
    echo '<p>Hello World</p>';
?>

Go

package main
  
import (
    "fmt"
    "net/http"
)
  
func main() {
    hello := "<p>Hello World</p>"
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, hello)
    })
    http.ListenAndServe(":8080", nil)
}

测试脚本

ulimit -n 4096
ab -r -n 10000 -c 100 <URL>

###测试结果 Node.js,并发数 100

Concurrency Level:      100
Time taken for tests:   1.914 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      1180000 bytes
HTML transferred:       180000 bytes
Requests per second:    5225.01 [#/sec] (mean)
Time per request:       19.139 [ms] (mean)
Time per request:       0.191 [ms] (mean, across all concurrent requests)
Transfer rate:          602.10 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       3
Processing:     3   19  15.2     15     171
Waiting:        3   19  15.2     15     171
Total:          6   19  15.2     16     171
Percentage of the requests served within a certain time (ms)
  50%     16
  66%     18
  75%     20
  80%     21
  90%     23
  95%     26
  98%     44
  99%     55
 100%    171 (longest request)

Node.js,并发数 2000

Concurrency Level:      2000
Time taken for tests:   34.893 seconds
Complete requests:      10000
Failed requests:        3284
   (Connect: 0, Receive: 1767, Length: 1517, Exceptions: 0)
Write errors:           0
Total transferred:      974680 bytes
HTML transferred:       148680 bytes
Requests per second:    286.59 [#/sec] (mean)
Time per request:       6978.553 [ms] (mean)
Time per request:       3.489 [ms] (mean, across all concurrent requests)
Transfer rate:          27.28 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   24  55.3      0     579
Processing:     1 2203 6471.0     35   34501
Waiting:        0   36  32.0     32     170
Total:         22 2228 6493.2     36   34679
Percentage of the requests served within a certain time (ms)
  50%     36
  66%     45
  75%    149
  80%    175
  90%   7914
  95%  21265
  98%  27955
  99%  27965
 100%  34679 (longest request)

PHP,并发数 100

Concurrency Level: 100
Time taken for tests:   1.784 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      2870000 bytes
HTML transferred:       180000 bytes
Requests per second:    5606.47 [#/sec] (mean)
Time per request:       17.837 [ms] (mean)
Time per request:       0.178 [ms] (mean, across all concurrent requests)
Transfer rate:          1571.34 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:     6   17  25.4     12     407
Waiting:        6   17  25.4     11     407
Total:          6   18  25.5     12     407
Percentage of the requests served within a certain time (ms)
  50%     12
  66%     12
  75%     20
  80%     21
  90%     22
  95%     23
  98%    109
  99%    204
 100%    407 (longest request)

PHP,并发数 2000

Concurrency Level:      2000
Time taken for tests:   42.637 seconds
Complete requests:      10000
Failed requests:        3962
   (Connect: 0, Receive: 1984, Length: 1978, Exceptions: 0)
Write errors:           0
Total transferred:      2303175 bytes
HTML transferred:       144450 bytes
Requests per second:    234.54 [#/sec] (mean)
Time per request:       8527.339 [ms] (mean)
Time per request:       4.264 [ms] (mean, across all concurrent requests)
Transfer rate:          52.75 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   49 104.6      0    1212
Processing:     0 3313 8906.0     16   42387
Waiting:        0   14  10.9     15      70
Total:          0 3362 8982.3     16   42632
Percentage of the requests served within a certain time (ms)
  50%     16
  66%     18
  75%     27
  80%    113
  90%  15459
  95%  29288
  98%  36002
  99%  36005
 100%  42632 (longest request)

Go,并发数 100

Concurrency Level:      100
Time taken for tests:   0.717 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      1340000 bytes
HTML transferred:       180000 bytes
Requests per second:    13955.33 [#/sec] (mean)
Time per request:       7.166 [ms] (mean)
Time per request:       0.072 [ms] (mean, across all concurrent requests)
Transfer rate:          1826.19 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    3   1.2      3       9
Processing:     1    4   1.1      4      12
Waiting:        0    4   1.1      4      12
Total:          2    7   1.8      7      17
Percentage of the requests served within a certain time (ms)
  50%      7
  66%      7
  75%      8
  80%      9
  90%     10
  95%     10
  98%     11
  99%     13
 100%     17 (longest request)

Go,并发数 2000

Concurrency Level:      2000
Time taken for tests:   21.014 seconds
Complete requests:      10000
Failed requests:        2466
   (Connect: 0, Receive: 1360, Length: 1106, Exceptions: 0)
Write errors:           0
Total transferred:      1157760 bytes
HTML transferred:       155520 bytes
Requests per second:    475.87 [#/sec] (mean)
Time per request:       4202.812 [ms] (mean)
Time per request:       2.101 [ms] (mean, across all concurrent requests)
Transfer rate:          53.80 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   44 187.7      4    1119
Processing:     0  918 3303.0      5   20949
Waiting:        0    6  26.4      4     359
Total:          6  961 3309.4     10   20984
Percentage of the requests served within a certain time (ms)
  50%     10
  66%     11
  75%     13
  80%     15
  90%   1907
  95%   7726
  98%  14346
  99%  20980
 100%  20984 (longest request)

在较低并发下,看每秒相应请求数,Node.js 比 PHP 稍低,而 Go 则比 PHP 高了一倍有余。在高并发情况下,Node.js 比 PHP 高, Go 则更好一点,但三者都出现了比较多的请求失败。 Node.js vs PHP, Benchmarking Node.js 两篇文章详细比较了并发数对 Node.js 的影响,包括响应数、内存和 CPU 使用等更多指标。

##听起来像老调重弹:Node.js 和 Ruby on Rails? full-stack 这不是第一次有这个概念了。十年前,最热门的莫过于 Ruby on Rails (RoR) ,RoR 的让后端只用写一种语言就可以了,Mac OS X 10.5 Lepoard 中默认安装了 RoR,很多科技界的作者惊呼 TextMate 和 RoR 就足以让你掏钱买一台 Mac 开始开发了。

但十年之后,转向 Node.js 最积极的公司,就是当年最积极采用 RoR 的公司,原因主要有以下几个:

虽然 Node.js 看起来很有潜力,但还是难免让人不注意到到今天 Node.js 和当时 RoR 的一些类似之处。

##Node.js 和 PHP 对比

##Node.js 维护性上的问题 ###动态类型语言,无法在编译时检查 不能 AOT,只能 JIT,一般来说效率方面低于静态类型语言

更容易发生 runtime 错误

###循环引用容易发生内存泄漏,以及不确定性垃圾回收机制

内存回收的过程完全是由 Google V8 引擎决定的,JavaScript 语言本身现在和可预见的未来都没有提供底层接口,开发者无法设置内存上限。不能简单的控制内存和 CPU 使用上限。

另外,目前主流浏览器和 JS 引擎都使用 Mark-and-Sweep 算法进行内存回收。这一算法必须暂停所有的工作线程。由于没有暴露控制接口,这意味着开发者无法控制内存回收时机和行为。这对于需要长期稳定运行的系统来说,会带来一些额外的挑战。

比如 Express 之类依赖中间件的框架,却没有很好的组件通讯机制,使用中不注意的话容易产生内存泄漏。

###数据结构少 没有 enum, struct, static, public/private/protected, constant 等, 所以 JavaScript 程序员总是免不了要重复发明轮子,才能实现一些相当基础的功能。

另外复杂数据结构的缺失代码可读性不高。

###late-binding 编译器很难优化,程序员很难理解,查错也很麻烦 JavaScript 中很容易写出天书一样的代码,比如说我写

var func = Function.prototype.bind.bind(Function.call);

有人能很快告诉我 func 函数会做什么吗?

###运维要求较高 理论上 Node.js 可以提供比 PHP 更好的性能,但 Node.js 缺乏底层接口,比如没有 PHP 中 ini_set('memory_limit', '256M') 这样简单有效的 API。遇到内存泄漏需要在线上机器上做 profiling,并不是每个工程师都可以很快掌握的。

##Node.js 会是未来的语言吗? ##纯技术层面:也许不是 JavaScript 并不是很有前瞻性的语言。如果你想通过学一门语言提高自己,我并不建议学习 JavaScript 和 Node.js。

在较大的项目中,如果只考虑技术方面的话,我更倾向于选择一门语言,这门语言可以满足:

静态类型可以在编译阶段的做静态类型检查,避免一些常见的错误。另外还可以利用这些信息做优化,相比之下,动态类型只能在运行时候做 JIT 编译优化,性能上有一定的差距。

适合大规模项目的后端语言常见的有:Java, Erlang, Scala, 以及比较新的 Clojure 等。以 Erlang 为例,Erlang 是一个为并行处理而设计的语言。Erlang 大量使用轻量级进程,并利用消息传递实现进程间通讯,进程间上下文切换比一般语言的线程切换要高效得多。

Google 的 Go 语言也是未来语言的有力竞争者。Go 的并行模式比 Node.js 更优雅一些。性能方面,Go 的性能不比 Node.js 差,甚至更好。

Apple 发明的 Swift 也是一个很有趣的语言。Chris Lattner 早先的工作成果主要是在 LLVM 编译器方面,而在 Swift 语言里能明显的看到编译器开发者的对语言的一些优化思路:Swift 是一个强类型、静态类型的语言,但通过编译阶段的 type inference,简化了不必要的类型声明,给人一种弱类型语言的风格,提高了开发效率。Swift 语言比较特殊的地方 (比如 Customized Intializer) 估计也是站在在编译器优化的角度来设计的。

虽然 Swift 不是为 Web 后端设计的语言,但它的结合脚本式语言和静态类型语言的思路是很有趣的。未来可能会看到采用类似设计思路的框架或语言用在后端上。

###实际上…很可能是的 Node.js 很可能成为 de facto 的前端技术栈。并在相当长的一段时间里作为前端后延的首选语言,这主要是因为:

comments powered by Disqus