推荐
使用反向代理
Node.js 是一个早期采用框架的平台,这些框架在标准库中提供了易于使用的 Web 服务器。以前,在使用 PHP 或 Python 等语言时,需要特定支持该语言的 Web 服务器或设置某种形式的 [CGI 网关][cgi] 来与语言配合工作。而在 Node.js 中,可以编写直接处理 HTTP 请求的应用程序。因此,有一种诱惑是编写能够处理多个域名请求、监听多个端口(例如 HTTP 和 HTTPS)并直接暴露到互联网上的应用程序来处理这些请求。
Fastify 团队强烈认为这是反模式并且是非常糟糕的做法:
- 它通过分散应用的焦点增加了不必要的复杂性。
- 它阻止了[水平扩展][scale-horiz]。
有关为什么应该使用反向代理的更详细讨论,请参阅 [如果 Node.js 已经准备好生产环境,我为什么要使用反向代理?][why-use]
作为一个具体的例子,考虑以下情况:
- 应用需要多个实例来处理负载。
- 应用需要 TLS 终止。
- 应用需要将 HTTP 请求重定向到 HTTPS。
- 应用需要为多个域名提供服务。
- 应用需要提供静态资源,例如 jpeg 文件。
有许多反向代理解决方案可供选择,并且您的环境可能会决定使用哪种方案,例如 AWS 或 GCP。鉴于上述情况,我们可以使用 [HAProxy][haproxy] 或 [Nginx][nginx] 来解决这些需求:
HAProxy配置示例
# 全局部分定义了HAProxy(引擎)实例的基本配置。
global
log /dev/log syslog
maxconn 4096
chroot /var/lib/haproxy
user haproxy
group haproxy
# 设置一些基本的TLS选项。
tune.ssl.default-dh-param 2048
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11
ssl-default-server-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
# 每个默认部分定义了将在后续子部分中应用的选项,直到遇到另一个默认部分为止。
defaults
log global
mode http
option httplog
option dontlognull
retries 3
option redispatch
# 下面的选项使HAProxy关闭到后端服务器的连接而不是保持打开。这可以缓解Node进程中的意外连接重置错误。
option http-server-close
maxconn 2000
timeout connect 5000
timeout client 50000
timeout server 50000
# 启用特定内容类型的压缩功能。
compression algo gzip
compression type text/html text/plain text/css application/javascript
# 启用特定内容类型的压缩功能。
compression algo gzip
compression type text/html text/plain text/css application/javascript
# "frontend" 部分定义了一个公共监听器,即一个“http服务器”。
# 对客户端而言。
frontend 代理
# 这里的IP地址是服务器的_公共_IP地址。
# 在这里我们使用私有地址作为示例。
bind 10.0.0.10:80
# 此重定向规则将所有非TLS流量重定向到HTTPS端口上的相同请求URL。
redirect scheme https code 308 if !{ ssl_fc }
# 技术上来说,这个use_backend指令是多余的,因为我们只是将所有流量重定向到了HTTPS前端。这里只是为了完整性而包含它。
# 此frontend定义了我们主要的、仅TLS监听器。在这里我们将定义要暴露的TLS证书以及如何处理传入请求。
frontend 代理-ssl
# 在此示例中的`/etc/haproxy/certs`目录中包含一组名为对应域名颁发的PEM格式证书文件。当HAProxy启动时,它将读取该目录,加载所有找到的证书,并使用SNI匹配来为连接应用正确的证书。
bind 10.0.0.10:443 ssl crt /etc/haproxy/certs
# 在这里我们定义规则对处理静态资源。任何传入请求路径以`/static`开头,例如
# `https://one.example.com/static/foo.jpeg`,将被重定向到静态资源服务器。
acl 是静态资源 path -i -m beg /static
use_backend 静态后端 if 是静态资源
# 在这里定义规则对,根据请求的域名将请求导向合适的Node.js服务器。`acl`行用于匹配传入的主机名,并定义一个布尔值表示是否匹配。
# `use_backend` 行用于在布尔值为真时将流量导向后端。
acl 示例1 hdr_sub(Host) one.example.com
use_backend example1-backend if 示例1
acl 示例2 hdr_sub(Host) two.example.com
use_backend example2-backend if 示例2
# 最后,我们有一个备用重定向规则,在没有匹配到上述任何请求主机时使用。
default_backend default-server
## 后端配置说明
一个“后端”用于告诉HAProxy在哪里获取代理请求的信息。这些部分定义了我们的Node.js应用和其他服务器的位置(如静态资源)。
backend default-server
# 在这个示例中,我们为所有未匹配的域名请求设置了一个默认的后端服务器。
# 注意,该后端服务器不需要处理TLS请求。这被称为“TLS终止”:TLS连接在反向代理处结束。
# 还可以将请求代理到自身提供TLS服务的后端服务器上,但这超出了本示例范围。
server server1 10.10.10.2:80
## 示例1后端配置
此后端配置用于处理`https://one.example.com` 的请求,并以轮询方式将请求代理到三个后端服务器。
backend example1-backend
server example1-1 10.10.11.2:80
server example1-2 10.10.11.2:80
server example2-2 10.10.11.3:80
## 示例2后端配置
此后端配置用于处理`https://two.example.com` 的请求,并以轮询方式将请求代理到三个后端服务器。
backend example2-backend
server example2-1 10.10.12.2:80
server example2-2 10.10.12.2:80
server example2-3 10.10.12.3:80
## 静态资源后端配置
此后端处理静态资源请求。
backend static-backend
server static-server1 10.10.9.2:80
[cgi]: https://en.wikipedia.org/wiki/Common_Gateway_Interface
[水平扩展]: https://en.wikipedia.org/wiki/Scalability#Horizontal
[为什么使用]: https://web.archive.org/web/20190821102906/https://medium.com/intrinsic/why-should-i-use-a-reverse-proxy-if-node-js-is-production-ready-5a079408b2ca
[haproxy]: https://www.haproxy.org/
Nginx
# 此upstream块将3个服务器组合成一个名为backend fastify_app的组
# 前两个主服务器通过轮询分配,第三个作为备份,在前两个不可达时使用
# 这里假设您的fastify服务器监听端口80。
# 更多信息:https://nginx.org/en/docs/http/ngx_http_upstream_module.html
upstream fastify_app {
server 10.10.11.1:80;
server 10.10.11.2:80;
server 10.10.11.3:80 backup;
}
# 此server块要求NGINX在收到端口80(通常是普通HTTP)的请求时
# 将其重定向到相同的请求URL,但使用HTTPS协议。
# 此块是可选的,并且通常用于您正在处理SSL终止的情况,如本例所示。
server {
# default_server是一个特殊参数,要求NGINX将此server块设置为默认的地址/端口
# 在这种情况下,默认的是任何地址和端口80
listen 80 default_server;
listen [::]:80 default_server;
# 使用server_name指令,您可以要求NGINX仅在匹配服务器名称时使用此server块
# listen 80;
# listen [::]:80;
# server_name example.tld;
# 此location块将请求中的所有路径与上述重定向进行匹配。
location / {
return 301 https://$host$request_uri;
}
}
# 此server块要求NGINX在端口443上启用SSL并接受HTTP/2连接
# 请求在此处被代理到fastify_app服务器组,通过端口3000。
server {
# 此listen指令要求NGINX接受任何地址、端口443上的请求,并使用SSL。
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
# 使用server_name指令,您可以要求NGINX仅在匹配服务器名称时使用此server块
# listen 443 ssl;
# listen [::]:443 ssl;
# server_name example.tld;
# 启用HTTP/2支持
http2 on;
# 启用HTTP/2支持
http2 on;
# 您的SSL/TLS证书(链)和私钥,格式为PEM
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/private.pem;
# 基于 https://ssl-config.mozilla.org/ 的通用最佳实践基线
ssl_session_timeout 1d;
ssl_session_cache shared:FastifyApp:10m;
ssl_session_tickets off;
# 这告诉NGINX仅接受TLS 1.3,大多数现代浏览器(包括具有某些更新的IE 11)应该可以支持。
# 如果您需要支持旧版浏览器,则可能需要添加额外的回退协议。
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
# 此设置向浏览器发送一个头信息,告诉它们仅使用HTTPS与该服务器通信。
add_header Strict-Transport-Security "max-age=63072000" always;
# 如果您希望启用OCSP Stapling,则需要以下指令。
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/chain.pem;
# 自定义名称服务器以解析上游服务器名称
# resolver 127.0.0.1;
# 此部分匹配所有路径并将请求代理到上述后端服务器组。注意转发原始请求信息的额外头信息。
location / {
# 更多信息:https://nginx.org/en/docs/http/ngx_http_proxy_module.html
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 这是将请求代理到指定服务器的指令。
# 如果你使用的是上游组,则不需要指定端口。
# 如果你要直接代理到一个服务器,例如:
# proxy_pass http://127.0.0.1:3000 那么需要指定端口。
proxy_pass http://fastify_app;
}
}
[nginx]: https://nginx.org/
Kubernetes
readinessProbe
默认使用 (pod IP作为主机名 。Fastify 默认监听 127.0.0.1
。在这种情况下,探测无法访问应用程序。为了使其正常工作,应用程序必须监听 0.0.0.0
或在 readinessProbe.httpGet
规范中指定自定义主机名,如下例所示:
readinessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 5
生产环境容量规划
为了正确配置您的Fastify应用的生产环境,强烈建议您针对不同的环境配置进行自己的测量。这些配置可能使用真实的CPU核心、虚拟CPU核心(vCPU)或甚至是部分vCPU核心。在本指南中,我们将使用术语“vCPU”来表示任何类型的CPU。
可以使用诸如k6 或 autocannon 这样的工具来进行必要的性能测试。
尽管如此,您也可以参考以下经验法则:
-
为了获得最低的延迟,建议每个应用实例(例如k8s pod)使用2个vCPU。第二个vCPU主要用于垃圾回收器(GC)和libuv线程池。这将最小化用户的延迟以及内存使用量,因为GC会更频繁地运行。此外,主线程不需要停止以让GC运行。
-
为了优化吞吐量(在每个可用的vCPU上处理尽可能多的请求),可以考虑为每个应用实例使用较少数量的vCPU。仅使用1个vCPU来运行Node.js应用程序也是完全可以接受的。
-
您还可以尝试使用更少的数量的vCPU,这可能在某些用例中提供更好的吞吐量。有报告称,在Kubernetes中,API网关解决方案可以很好地工作于0.1-0.2 vCPU之间。
请参阅从内部理解Node.js事件循环 ,以更详细地了解Node.js的工作原理,并更好地确定您的特定应用需求。
运行多个实例
在某些情况下,可能需要在同一服务器上运行多个Fastify应用程序。一个常见的例子是在单独的端口上暴露指标端点,以防止公共访问,在使用反向代理或入口防火墙不可行时。
在同一Node.js进程中并行启动和运行多个Fastify实例是完全可以接受的,即使在高负载系统中也是如此。每个Fastify实例仅根据其接收到的流量产生相应的负载,并占用该Fastify实例所需的内存。