Express 性能调优

最近遇到了一个奇怪的事情,自己负责的一个项目的 RC(Release Condidate)版本突然在某一天打开时访问非常之慢,达到了完全不能接受的程度。奇怪的是,就在前一天,页面的访问速度至少是不会让人觉得不可接受的。更加奇怪的是,代码我完全没有改动过。

NODE_ENV=production

在查找了一番资料之后,我在 Express 官网上发现了这样一段以前一直没有特别留意过的一段话:

NODE_ENV 环境变量指定运行应用程序的环境(通常是开发或者生产环境)。为了改进性能,最简单的方法是将 NODE_ENV 设置为“production”。

将 NODE_ENV 设置为“production”会使 Express:

  • 高速缓存视图模板。
  • 高速缓存从 CSS 扩展生成的 CSS 文件。
  • 生成简短的错误消息。

测试表明仅仅这样做就可以使应用程序性能提高 3 倍多!

遂顿悟!

事情是这样的,之前我们项目的 RC 版本都是使用的 production 作为环境变量,而且与正式发布的版本使用的是同样量级的数据大小,但最近我们项目有一个相对较大的版本更新,于是我为了配置一些特殊的变量供需求方能方便的检查效果和进一步评估改动,所以把 RC 版本所在服务器的 NODE_ENV 变量改成了其他值(为了适配另外的一套配置)。而测试环境下由于数据量非常小,所以根本察觉不到这个问题。

为了验证这个有文档说明来佐证的原因,我使用 ab(ApacheBench,一般用来做压力测试)进行了 A/B 测试(A/B testing,与前文的 ApacheBench 不是同一个概念)。

需要提前说明的是,为了尽可能排除网络环境的干扰,我是在内网环境下测试的,两次测试完全只有环境变量不同(也就是说里面的配置、所用的数据库、是否有缓存等都完全是一样的)。而且测试的是项目的一个主要接口,这个接口提供了页面的大部分数据,内部的业务逻辑也比较复杂,做的事情比较多。

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# ======================================================
# NODE_ENV=development
# ======================================================
$ >> ab -c 10 -t 3 http://localhost:3000/XXXXX

This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)


Server Software:
Server Hostname: localhost
Server Port: 3000

Document Path: XXXXX
Document Length: 42477 bytes

Concurrency Level: 10
Time taken for tests: 3.002 seconds
Complete requests: 124
Failed requests: 0
Total transferred: 5467136 bytes
HTML transferred: 5437056 bytes
Requests per second: 41.31 [#/sec] (mean)
Time per request: 242.097 [ms] (mean)
Time per request: 24.210 [ms] (mean, across all concurrent requests)
Transfer rate: 1778.48 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 191 231 20.2 230 300
Waiting: 191 230 20.0 229 296
Total: 191 232 20.2 230 300

Percentage of the requests served within a certain time (ms)
50% 230
66% 234
75% 235
80% 238
90% 246
95% 285
98% 294
99% 298
100% 300 (longest request)
Finished 124 requests

# ======================================================
# NODE_ENV=production
# ======================================================
$ >> ab -c 10 -t 3 http://localhost:3000/XXXXX

This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)


Server Software:
Server Hostname: localhost
Server Port: 3000

Document Path: XXXXX
Document Length: 42477 bytes

Concurrency Level: 10
Time taken for tests: 3.006 seconds
Complete requests: 209
Failed requests: 0
Total transferred: 8926808 bytes
HTML transferred: 8877693 bytes
Requests per second: 69.53 [#/sec] (mean)
Time per request: 143.828 [ms] (mean)
Time per request: 14.383 [ms] (mean, across all concurrent requests)
Transfer rate: 2900.06 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 1
Processing: 113 142 17.5 137 189
Waiting: 112 141 17.5 137 188
Total: 114 142 17.5 137 189

Percentage of the requests served within a certain time (ms)
50% 137
66% 143
75% 156
80% 160
90% 168
95% 179
98% 183
99% 185
100% 189 (longest request)
Finished 209 requests

可以看见,对比 QPS(或 RPS,Requests per second),development:production = 41.31:69.53 [#/sec] (mean);对比平均用户请求等待时间(Time per request),development:production = 242.097:143.828 [ms] (mean)。基本上单个请求快了 100 ms 左右。

虽然没有官网上说的「3 倍」那么夸张,不过按文档上说还会「高速缓存视图模板」和「高速缓存从 CSS 扩展生成的 CSS 文件」,而我测试的是一个纯 API 接口,只返回 JSON 数据,所以理论上 HTML 页面打开的速度的对比应该会更加明显(之后也测试了这种情况,速度要快 1/3 左右)。

其他 Express 性能调优技巧

总结一下官网上所说的关于性能调优的基本还有以下几条:

  • 使用 gzip 压缩响应:用 compression 中间件;
  • 代码中不使用同步函数:项目启动时可以一次性使用同步函数进行一些有时序性的操作(比如加载路由),但在之后的长期运行过程中,具体对外的接口执行流程中不应该存在同步函数;
  • 正确进行日志记录:不要简单的用 console(都是同步函数)或自己写文件操作,直接用成熟的日志包(如 Winston、Bunyan、log4js 等),必要的情况下可能还需要使用消息队列;
  • 使用进程管理器:除了提高应用的可用性之外,还可以根据 CPU 核心数生成相应的应用实例,充分利用 Node.js 的优势压榨 CPU 的资源,常用的有 StrongLoop Process Manager、PM2、Forever;
  • 使用反向代理:直接用 Nginx 做反向代理吧,一方面可以做 CDN 缓存静态资源,另一方面还能够利用它做简单的负载均衡。不过需要注意的几点是:
    • 多实例下「粘性会话」需要把状态存储在第三方高速介质中,一般用 Redis 就好;或者,采用 JWT 鉴权方案实现接口无状态(更推荐,同时为了安全站点一定要强制使用 HTTPS);
    • Nginx 做反向代理时不应该将其与应用放在同一台主机上,原因可以看这里:Nginx 反向代理为什么可以提高网站性能?

Reference