[译] 为何要对生产环境的 Node.js 使用反向代理?

1,966 阅读10分钟

原文: medium.com/intrinsic/w…

早在 2012 年,PHP 和 Ruby on Rails 仍是统治渲染 web 应用的服务器端技术。但是,一个大胆的竞争者在社区中搅起风暴。这项在当年的一篇博文中宣称通过了百万级并发连接测试的技术并非其他,正是 Node.js,其流行程度从那时至今也在稳定增长。

不像当时的大多数同类技术,Node.js 自带一个 web server。拥有这个 server 意味着开发者可以绕过诸如 php.ini 的种种配置文件以及一个包含了若干 .htaccess 文件的层级结构。内置的 web server 同样提供了其他便利,如处理上传文件的能力或易于实现 WebSockets 等。

由 Node.js 驱动的 web 应用每天都顺畅地处理数以亿计的请求。世界上大多数大公司都在某些方面被 Node.js 影响。说 Node.js 可用于生产环境肯定是个保守的说法。但是,对于构建 Node.js 仍有一个适用的建议:

不应直接把一个 Node.js 进程暴露到 web 上,而应该将其隐藏在一个反向代理背后。

在我们揭示原因之前,先看看那是什么吧。

反向代理是什么?

一个反向代理(reverse proxy)基本上就是一种用于接受请求并将它们转发到某处的另一个 HTTP server 上的特殊 web server,也会接受响应并转发回给原始的请求者。

反向代理通常不会精确地发送原始请求,典型的做法是会在某些方面对请求作出修改。例如,如果反向代理位于 www.example.org:80 并转发请求到 ex.example.org:8080 的话,大概就是它重写了原始的 Host header 以匹配目标地址。反向代理也可能通过其他方式修改请求,比如清理一个有缺陷的请求或是在协议之间作出转换。

一旦反向代理收到一个响应,可能也会对其进行某方面的转换。常用的途径同样是修改 Host header 以匹配原始请求。

请求的 body 也能被修改。一种通常的修改是在响应时执行 gzip 压缩。另一种常见改变是当基础服务只是 HTTP 时启用 HTTPS 支持。

反向代理也可以将到来的请求分发给多个后端实例。如果一个服务暴露于 api.example.org,那么一个反向代理可以转发请求到 api1.internal.example.orgapi2.internal.example.org 等处。

有很多种不同的反向代理。两种更流行的分别是 NginxHAProxy。这两种工具都能够执行 gzip 压缩和增加 HTTPS 支持,它们也能很好地适用于其它领域。Nginx 更流行一些,并且也有诸如从文件系统运行静态文件服务器等一些其它有益的能力,所以我们将在本文中使用它作为例子。

现在我们知道 何为 反向代理了,下面来看看 为何 我们要将其用于 Node.js。

为何应该使用一个反向代理?

SSL 终端

SSL 终端是使用反向代理的最主要原因之一。将一个应用的协议从 http 变为 https 可不止添加一个 s 字母那么简单。Node.js 本身 能够https 执行必要的加密和解密,也能被配置为读取必要的证书文件。

但是,配置一个用于和我们的应用通信的协议,并管理一个永不过期的 SSL 凭证,并非真的是我们的应用需要关注的事情。检查一个代码库中的证书不只是麻烦,同时也是一种安全风险。在应用启动时从特定位置获取证书也有其风险。

有鉴于此,最好在应用之外执行 SSL 终端,通常就由一个反向代理来承担。受惠于像 certbot 这样的技术,通过 Nginx 维护证书也像设置一个定时任务一样简单。这样的一个任务可以自动安装新证书并动态重配置 Nginx 进程。与重启每个 Node.js 应用实例相比,这是一个破坏性小得多的过程。

同时,通过允许一个反向代理来执行 SSL 终端,这也意味着 只有 被反向代理的作者编写的代码可以访问你的私有 SSL 证书。反之,如果由你的 Node.js 应用处理 SSL,那么你的应用用到的每一个单独的第三方模块 -- 即便是潜在的恶意模块,都能访问你的私有 SSL 证书了。

gzip 压缩

gzip 是另一个你应该由应用让渡到反向代理的特性。gzip 压缩策略最好在管理级别设置,而不是不得不为每个应用指定和配置。

最好使用某些逻辑来决定对什么采用 gzip。例如,很小的文件,或许比 1kb 还小的,或许就不值得压缩,因为其 gzip 压缩版本没准会变得更大,亦或客户端对其解压的 CPU 开销也更不划算。同时,当处理二进制数据时,取决于其格式可能也无法从压缩中获益。有时 gzip 也无法被简单地启用或禁用,为了兼容压缩算法需要检查接收到的 Accept-Encoding header。

集群化

JavaScript 是一种单线程语言,相应的,Node.js 天然也是一种单线程的服务器平台(尽管 Node.js v10 中开始出现的实验性 worker 线程支持致力于改变这一点)。这意味着要从一个 Node.js 应用中获取尽可能更大的吞吐量需要运行和 CPU 核数差不多相同的实例数量。

Node.js 自带的 cluster 模块可以实现集群化。收到的 HTTP 请求将会被交给一个 master 进程而后再被分发给 cluster workers。

但是,动态伸缩 cluster workers 会有点费劲。通常也会通过运行一个额外的 Node.js 进程作为分发主进程来增加吞吐量。但是,跨机器伸缩进程对于 cluster 来说还是有点强人所难了。

基于这些原因,有时使用一个反向代理来分发正在运行的 Node.js 进程会更好。反向代理能被动态配置以指向新出现的应用进程。说实在的,一个应用就应该只关注其自身的工作,而不是管理多个拷贝并分发请求。

企业路由

当着手于大型 web 应用,特别是被有多个团队的企业创建的应用时,使用一个反向代理来决定如何转发请求是非常有用的。例如,指向 example.org/search/* 的请求可被路由到内部搜索应用,而其它指向 example.org/profile/* 的请求可以被分发到内部资料应用上。

这样的加工处理提供了其它强大的特性,如粘滞会话、蓝/绿部署、A/B测试等等。我个人就曾在这样一个由应用执行这些逻辑的代码库中工作,这种实现方式让应用极难维护。

性能收益

Node.js 是高可塑性的。它可以从文件系统架设静态资源服务、对 HTTP 响应执行 gzip 压缩、内建支持 HTTPS,另有很多其它特性。它甚至有能力通过 cluster 模块,运行一个应用的多个实例并分发其自身的请求。

然而,基本上让一个反向代理来处理这些操作,而不是靠 Node.js 应用去做,才是符合我们利益的。除了以上列出的原因之外,另一个想要在 Node.js 之外做这些操作的原因是由于效率。

SSL 加密和 gzip 压缩是两个高计算密集型的操作。专用型反向代理工具,如 Nginx 和 HAProxy,对这些操作术业有专攻,执行速度要快于 Node.js。用 Nginx 这样的 web server 从磁盘上读取静态内容也比 Node.js 来得快。有时甚至比起用额外的 Node.js 进程来执行集群化,用 Nginx 反向代理实现的效率都更高,内存和 CPU 的占用都更少。

但是,耳听为虚。让我们运行一些基准测试!

以下负载测试是使用 siege (译注:一个 http/https 回归测试和基准测试工具)执行的。我们用并发值 10 运行命令(模拟用户的请求),并且该命令会迭代运行 2 万次(总体会有 20 万次请求)。

为检验内存使用量我们在基准测试期间运行命令 pmap <pid> | grep total 若干次并取 平均值 作为结果(译注:Linux 中的 pmap 命令用于查看进程用了多少内存)。当使用单个 worker 线程运行 Nginx 时,最终会运行两个实例,一个是主线程,另一个是 worker 线程;然后将两个值相加。当运行一个包含 2 个节点的 Node.js 集群时,将有 3 个进程,一个是主进程,另两个是 worker 进程。下表中的 approx memory 列是给定测试中每个 Nginx 和 Node.js 进程的总和。

基准测试的结果

node-cluster 基准测试中我们使用了 2 个 worker,这意味着共有 3 个 Node.js 进程在运行:1 个 master 和 2 个 worker。在 nginx-cluster-node 基准测试中我们有 2 个 Node.js 进程在运行。每个 Nginx 测试都有一个单独的 Nginx 主进程和一个单独的 Nginx worker 进程。基准测试包括从磁盘读取一个文件,且无论是 Nginx 还是 Node.js 都被配置为将文件缓存在内存中。

使用 Nginx 为 Node.js 执行 SSL 终端带来了约 16% (746rps 到 865rps) 的吞吐量增长。使用 Nginx 执行 gzip 压缩则让吞吐量增加了约 50% (5,047rps 到 7,590rps)。使用 Nginx 管理一个进程集群造成了约 1% (8,006rps 到 7,908rps) 的性能损失,大概是归因于在回环网络设备间传递额外请求的开销吧。

一个基本的 Node.js 单进程单内存使用量是约 600MB,而 Nginx 进程的约 50MB。这些使用量会根据使用了那些特性而小幅波动,例如,Node.js 使用了额外的约 13MB 来执行 SSL 终端,以及 Nginx 使用了额外的 4MB(译注:652 - 601 - 46.1)来作为 Node.js 的反向代理并架设从文件系统读取静态内容的服务。可以注意到一个有趣的事情是 Nginx 在其生命周期中保持了稳定的内存吞吐量;然而 Node.js 的内存吞吐量由于 JavaScript 天然的垃圾回收机制而时常起伏。

以下是执行此次基准测试的软件版本:

  • Nginx: 1.14.2
  • Node.js: 10.15.3
  • Siege: 3.0.8

测试执行在一台有 16GB 内存的机器上,有一块 i7-7500U CPU 4x2.70GHz,Linux 内核为 4.19.10。项目地址位于:github.com/IntrinsicLa…

简化应用代码

基准测试全面又出彩,但从我的观点来说,将部分工作从 Node.js 应用让渡给反向代理带来的最大收益是代码的简化。我们得以减少了大量可能造成潜在 bug 的必须应用代码并将其换成了声明式的配置。开发者中的一个普遍观点是他们对于让 Nginx 等外部团队编写这部分代码,而不是由他们自己编写更有信心。

不同于要安装和管理 gzip 压缩中间件并在多个 Node.js 项目中保持其最新,我们可以在一处统一配置它。和加载 SSL 证书后再重启应用进程不同,我们可以使用已有的证书管理工具。与其在应用中添加条件语句检查进程是 master 还是 worker,不如将其交给其它工具判断。

反向代理允许我们的应用聚焦于业务逻辑并忽略协议和进程管理。


尽管 Node.js 拥有运行在生产环境的完美能力,但将反向代理和生产环境的 HTTP Node.js 应用结合使用带来了种种收益。像 SSL 和 gzip 这样的操作变得更快。管理 SSL 证书也会更简单。大量所需应用代码也被减少了。



--End--

查看更多前端好文
请搜索 fewelife 关注公众号

转载请注明出处