首页
/ Fastify 生产部署最佳实践:反向代理配置、性能退化根因分析与容量规划指南

Fastify 生产部署最佳实践:反向代理配置、性能退化根因分析与容量规划指南

2026-09-05 17:14:43作者:彭桢灵Jeremy

本文基于 Fastify 官方部署建议文档(docs/Guides/Recommendations.md)系统讲解生产环境中部署 Fastify 的完整方法论:为什么必须使用反向代理、如何用 HAProxy / Nginx 完成 TLS 终结与多实例负载均衡、哪些写法会导致性能退化、Kubernetes 探针为什么连接不上、以及如何按 vCPU 规模做容量规划。读完本文,你将能够直接落地一套可复制的生产部署方案,并结合 Fastify 源码理解每条建议背后的实现依据。

为什么必须使用反向代理:Node.js 直连互联网是反模式

Fastify 官方建议明确指出:让 Fastify 应用直接处理多域名、多端口(HTTP 与 HTTPS 都监听)并直接暴露给互联网,是被强烈反对的反模式。这与 PHP、Python 时代需要专用 Web 服务器或 CGI 网关不同——Node.js 的标准库内置了易用性很高的 HTTP 服务器,使得应用可以"直接"处理 HTTP 请求,这带来了一种危险的诱惑。

官方列出这条建议背后的两条核心理由:

  1. 应用被要求同时处理 TLS 终结、域名路由、静态资源等职责,稀释了应用本身的专注度,引入了不必要的复杂度
  2. 这种架构阻碍了水平扩展——当流量增长时,你无法简单地在前面加机器。

官方文档列举了一个典型场景,说明反向代理如何解决一组常见生产需求:

  • 应用需要多个实例来处理负载;
  • 应用需要 TLS 终结(TLS termination);
  • 应用需要把 HTTP 请求重定向到 HTTPS
  • 应用需要同时服务多个域名
  • 应用需要服务静态资源(例如 jpeg 文件)。

结论是:这些职责全部应交给反向代理(HAProxy、Nginx,或云厂商的 LB/Ingress),Fastify 实例只专注于处理 HTTP 请求本身。下面的两节分别给出可直接使用的 HAProxy 与 Nginx 完整配置。

HAProxy 配置:TLS 终结、HTTP→HTTPS 重定向与多域名分发

以下配置来自官方文档,覆盖前述五个需求:80 端口 HTTP 全量 308 重定向到 443、443 端口按 SNI 加载证书、/static 前缀流量切到静态资源后端、按 Host 头分发到不同域名的 Node.js 后端组。

# The global section defines base HAProxy (engine) instance configuration.
global
  log /dev/log syslog
  maxconn 4096
  chroot /var/lib/haproxy
  user haproxy
  group haproxy

  # Set some baseline TLS options.
  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

# Each defaults section defines options that will apply to each subsequent
# subsection until another defaults section is encountered.
defaults
  log   global
  mode  http
  option        httplog
  option        dontlognull
  retries       3
  option redispatch
  # The following option makes haproxy close connections to backend servers
  # instead of keeping them open. This can alleviate unexpected connection
  # reset errors in the Node process.
  option http-server-close
  maxconn       2000
  timeout connect 5000
  timeout client 50000
  timeout server 50000

  # Enable content compression for specific content types.
  compression algo gzip
  compression type text/html text/plain text/css application/javascript

# A "frontend" section defines a public listener, i.e. an "http server"
# as far as clients are concerned.
frontend proxy
  # The IP address here would be the _public_ IP address of the server.
  # Here, we use a private address as an example.
  bind 10.0.0.10:80
  # This redirect rule will redirect all traffic that is not TLS traffic
  # to the same incoming request URL on the HTTPS port.
  redirect scheme https code 308 if !{ ssl_fc }
  # Technically this use_backend directive is useless since we are simply
  # redirecting all traffic to this frontend to the HTTPS frontend. It is
  # merely included here for completeness sake.
  use_backend default-server

# This frontend defines our primary, TLS only, listener. It is here where
# we will define the TLS certificates to expose and how to direct incoming
# requests.
frontend proxy-ssl
  # The `/etc/haproxy/certs` directory in this example contains a set of
  # certificate PEM files that are named for the domains the certificates are
  # issued for. When HAProxy starts, it will read this directory, load all of
  # the certificates it finds here, and use SNI matching to apply the correct
  # certificate to the connection.
  bind 10.0.0.10:443 ssl crt /etc/haproxy/certs

  # Here we define rule pairs to handle static resources. Any incoming request
  # that has a path starting with `/static`, e.g.
  # `https://one.fastify.example/static/foo.jpeg`, will be redirected to the
  # static resources server.
  acl is_static path -i -m beg /static
  use_backend static-backend if is_static

  # Here we define rule pairs to direct requests to appropriate Node.js
  # servers based on the requested domain. The `acl` line is used to match
  # the incoming hostname and define a boolean indicating if it is a match.
  # The `use_backend` line is used to direct the traffic if the boolean is
  # true.
  acl example1 hdr_sub(Host) one.fastify.example
  use_backend example1-backend if example1

  acl example2 hdr_sub(Host) two.fastify.example
  use_backend example2-backend if example2

  # Finally, we have a fallback redirect if none of the requested hosts
  # match the above rules.
  default_backend default-server

# A "backend" is used to tell HAProxy where to request information for the
# proxied request. These sections are where we will define where our Node.js
# apps live and any other servers for things like static assets.
backend default-server
  # In this example we are defaulting unmatched domain requests to a single
  # backend server for all requests. Notice that the backend server does not
  # have to be serving TLS requests. This is called "TLS termination": the TLS
  # connection is "terminated" at the reverse proxy.
  # It is possible to also proxy to backend servers that are themselves serving
  # requests over TLS, but that is outside the scope of this example.
  server server1 10.10.10.2:80

# This backend configuration will serve requests for `https://one.fastify.example`
# by proxying requests to three backend servers in a round-robin manner.
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

# This one serves requests for `https://two.fastify.example`
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

# This backend handles the static resources requests.
backend static-backend
  server static-server1 10.10.9.2:80

关键配置点解读

  • option http-server-close:让 HAProxy 主动关闭到后端的连接而不是保持长连接。官方注释特别指出,这能缓解 Node 进程侧出现的"意外连接被重置"类错误——这是 Node.js 后端与反向代理组合时的一个实战经验项。
  • redirect scheme https code 308 if !{ ssl_fc }:80 端口上所有非 TLS 流量按原 URL 做 308 重定向到 HTTPS。308 与 301 的语义差别在于它会保留请求方法与请求体语义,适合需要严格保持请求行为的重定向。
  • bind 10.0.0.10:443 ssl crt /etc/haproxy/certs:启动时加载整个证书目录,并按 SNI 匹配为每条连接选择对应域名的证书,从而实现单监听器服务多域名。
  • acl + use_backend 规则对hdr_sub(Host) 按 Host 头做子串匹配,把不同域名分发到不同后端组;未匹配的 Host 落到 default_backend
  • 后端均为纯 HTTP:后端服务器不需要自己服务 TLS,TLS 在反向代理处终结(TLS termination),这正是把安全职责从 Fastify 应用剥离的体现。

Nginx 配置:upstream 负载均衡、HTTPS 强制与 HTTP/2

Nginx 示例展示了一个更常见的单机代理形态:upstream 定义 2 主 1 备的 Fastify 后端组,80 端口全量 301 跳转 HTTPS,443 端口开启 TLS 1.3 与 HTTP/2 后反代到 upstream。

# This upstream block groups 3 servers into one named backend fastify_app
# with 2 primary servers distributed via round-robin
# and one backup which is used when the first 2 are not reachable
# This also assumes your fastify servers are listening on port 80.
upstream fastify_app {
  server 10.10.11.1:80;
  server 10.10.11.2:80;
  server 10.10.11.3:80 backup;
}

# This server block asks NGINX to respond with a redirect when
# an incoming request from port 80 (typically plain HTTP), to
# the same request URL but with HTTPS as protocol.
# This block is optional, and usually used if you are handling
# SSL termination in NGINX, like in the example here.
server {
  # default server is a special parameter to ask NGINX
  # to set this server block to the default for this address/port
  # which in this case is any address and port 80
  listen 80 default_server;
  listen [::]:80 default_server;

  # With a server_name directive you can also ask NGINX to
  # use this server block only with matching server name(s)
  # listen 80;
  # listen [::]:80;
  # server_name example.tld;

  # This matches all paths from the request and responds with
  # the redirect mentioned above.
  location / {
    return 301 https://$host$request_uri;
  }
}

# This server block asks NGINX to respond to requests from
# port 443 with SSL enabled and accept HTTP/2 connections.
# This is where the request is then proxied to the fastify_app
# server group via port 3000.
server {
  # This listen directive asks NGINX to accept requests
  # coming to any address, port 443, with SSL.
  listen 443 ssl default_server;
  listen [::]:443 ssl default_server;

  # With a server_name directive you can also ask NGINX to
  # use this server block only with matching server name(s)
  # listen 443 ssl;
  # listen [::]:443 ssl;
  # server_name example.tld;

  # Enable HTTP/2 support
  http2 on;

  # Your SSL/TLS certificate (chain) and secret key in the PEM format
  ssl_certificate /path/to/fullchain.pem;
  ssl_certificate_key /path/to/private.pem;

  # A generic best practice baseline for based
  ssl_session_timeout 1d;
  ssl_session_cache shared:FastifyApp:10m;
  ssl_session_tickets off;

  # This tells NGINX to only accept TLS 1.3, which should be fine
  # with most modern browsers including IE 11 with certain updates.
  # If you want to support older browsers you might need to add
  # additional fallback protocols.
  ssl_protocols TLSv1.3;
  ssl_prefer_server_ciphers off;

  # This adds a header that tells browsers to only ever use HTTPS
  # with this server.
  add_header Strict-Transport-Security "max-age=63072000" always;

  # The following directives are only necessary if you want to
  # enable OCSP Stapling.
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /path/to/chain.pem;

  # Custom nameserver to resolve upstream server names
  # resolver 127.0.0.1;

  # This section matches all paths and proxies it to the backend server
  # group specified above. Note the additional headers that forward
  # information about the original request. You might want to set
  # trustProxy to the address of your NGINX server so the X-Forwarded
  # fields are used by fastify.
  location / {
    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;

    # This is the directive that proxies requests to the specified server.
    # If you are using an upstream group, then you do not need to specify a port.
    # If you are directly proxying to a server e.g.
    # proxy_pass http://127.0.0.1:3000 then specify a port.
    proxy_pass http://fastify_app;
  }
}

关键配置点解读

  • upstream 主备结构:两台主服务器轮询(round-robin),第三台标记 backup,仅当前两台不可达时接管。这里假设 Fastify 实例监听 80 端口。
  • HSTS 头Strict-Transport-Security: max-age=63072000,即两年):告知浏览器今后只走 HTTPS,与 80 端口的 301 跳转配合形成闭环。
  • OCSP Staplingssl_stapling / ssl_stapling_verify / ssl_trusted_certificate 三件套用于加速证书吊销检查,按需启用。
  • WebSocket 支持proxy_set_header Upgrade / Connection 'upgrade' 转发升级请求所需的头部。
  • X-Forwarded-* 头与 Fastify trustProxy 的配合:这是反向代理场景下最容易踩的坑。Nginx 配置把 X-Real-IPX-Forwarded-ForX-Forwarded-Proto 传给 Fastify,但 Fastify 默认不会信任这些头。Fastify 提供了 trustProxy 选项,启用后才会基于这些字段还原真实客户端地址与协议,例如 const fastify = Fastify({ trustProxy: true }),也可传入具体 IP/CIDR 列表(如 '127.0.0.1,192.168.1.1/24')只信任指定代理。相关行为在 test/trust-proxy.test.js 中有系统覆盖。

性能退化的常见根因:五条来自官方的生产经验

官方文档单列了"Common Causes Of Performance Degradation"一节,列出五类会增加延迟或降低吞吐的写法。这一节值得单独记住,因为每一条都对应 Fastify 或路由引擎的某个具体机制。

1. 热路径优先使用静态路由或简单参数路由

正则(RegExp)路由代价高,参数很多的路由也会拖累路由引擎的匹配性能。官方指向 Routes 文档的 Url building 小节:如果某个 URL 模板需要动态拼接大量参数,应考虑用更静态的路径结构替代,或减少路径参数数量。Fastify 的测试套件中 test/constrained-routes.test.jstest/versioned-routes.test.js 覆盖了约束路由的行为,也侧面说明约束路由是功能特性而非免费的默认能力。

2. 谨慎使用路由约束

版本约束(version constraint)可能降低路由性能,异步自定义约束应被视为最后手段。背景可参考 Routes 文档的 Constraints 小节。从源码结构看,约束信息参与路由节点的匹配判定,约束越多、匹配时的条件判断越多,热路径开销越高——这与"正则路由昂贵"是同一类问题:不要让路由匹配本身成为瓶颈。

3. 优先使用 Fastify 插件/Hooks,而非通用中间件

Fastify 的中间件适配器(middleware adapter)"能用",但在性能敏感路径上,原生的插件与 hooks 集成方式通常更好。详见 Middleware 参考文档。从框架设计看,hooks 是 Fastify 请求生命周期的一等公民(docs/Reference/Hooks.md),而中间件本质上是对 Express 风格的适配层,多一层抽象就多一层开销。

4. 定义 response schema 加速 JSON 序列化

为响应定义 schema 后,Fastify 可用预编译的序列化器替代通用的 JSON.stringify,官方在路由文档中给出的经验值是约 10%–20% 的吞吐提升。操作方式见 Getting Started 的 Serialize your data 小节

5. 默认关闭 Ajv 的 allErrors

官方建议保持 allErrors 禁用,仅在需要详细校验反馈的场景(例如表单密集型 API)才开启,延迟敏感的端点应避开它。理由有两层:

  • 开启 allErrors: true 后,校验器会收集全部校验错误而不是遇到第一个就返回,单请求做的校验工作更多;
  • 对不可信输入而言,更重的校验流程会让拒绝服务(DoS)攻击更容易达成。

allErrors 属于校验器的自定义选项,可在全局或单个 schema 上通过 customOptions 配置,用法与显式关闭示例见 Validation and Serialization 文档(例如 customOptions: { allErrors: false })。测试用例 test/schema-validation.test.js 中也有 allErrors: true 的针对性验证,可作为行为参照。

Kubernetes 部署:readinessProbe 连不上应用的根因

Fastify 实例默认监听回环地址,而 Kubernetes 的 readinessProbe 默认使用 Pod IP 作为主机名发起探测。如果应用只监听回环地址,探针请求根本到不了应用,Pod 会一直判定为未就绪。官方给出的解决方案是二选一:

  1. 让应用监听 0.0.0.0
  2. 或在 readinessProbe.httpGet 中显式指定自定义 hostname。

官方示例(探针请求 /health,端口 4000):

readinessProbe:
    httpGet:
        path: /health
        port: 4000
    initialDelaySeconds: 30
    periodSeconds: 30
    timeoutSeconds: 3
    successThreshold: 1
    failureThreshold: 5

这一点在源码中有直接印证:lib/server.jslisten 的默认参数为 { port: 0, host: 'localhost' },即不显式指定 host 时绑定回环地址;当 host 为 localhost 时,lib/server.js 还会额外做 IPv4/IPv6 双栈绑定(multipleBindings)以同时覆盖 127.0.0.1::1。因此要暴露到 Pod 网络,必须显式传入 host: '0.0.0.0',这与 docs/Reference/Server.mdlisten 的说明一致。

生产容量规划:vCPU 分配的经验法则

官方强调:要为生产环境选对规格,最可靠的方式是对不同环境配置做自己的压测,环境可能使用物理核、vCPU 甚至分数 vCPU,文档中统一用 vCPU 指代任意 CPU 形态。可用的压测工具包括 Grafana k6 与 autocannon。

在此前提下,官方给出三条经验法则(rule of thumb):

  • 追求最低延迟:每个应用实例(如一个 k8s Pod)建议分配 2 vCPU。第二个 vCPU 主要被垃圾回收(GC)与 libuv 线程池使用。好处是:GC 可以更频繁地运行,从而降低内存占用;主线程也不用停下来让位给 GC,用户感知延迟最低。
  • 追求最大吞吐(单位 vCPU 处理尽可能多的请求/秒):应减少每个实例的 vCPU 数量,Node.js 应用跑在 1 vCPU 上完全没有问题,此时用更多小实例换总吞吐。
  • 极限实验:可以再尝试更小的规格,某些场景下吞吐反而更好。文档提到有 API 网关方案在 Kubernetes 上以 100m–200m vCPU 工作良好的报告——注意这只是"有报告",属于可实验的方向而非保证。

官方同时建议了解 Node.js 事件循环的内部机制(GC、libuv 线程池与事件循环主线程的关系),以便为自己的应用做出正确判断。

单进程运行多个 Fastify 实例

有些场景需要在同一台服务器上跑多个 Fastify 应用,官方给出的典型用例是:在没有反向代理或 Ingress 防火墙可用的情况下,把 metrics 端点暴露在独立端口上,避免被公网访问

官方的结论很明确:在同一个 Node.js 进程内启动多个 Fastify 实例并发运行是完全可行的,即使在高负载系统下也没问题。原因是"每个 Fastify 实例只产生与其接收流量相匹配的负载,加上该实例占用的内存"——空闲实例几乎不消耗 CPU,因此共享进程的心跳开销可以忽略。这与反向代理建议并不矛盾:多实例解决的是"职责隔离/端口隔离",反向代理解决的是"对外入口的统一治理",两者通常组合使用。

小结

Fastify 的部署建议可以浓缩为四件事:入口交给反向代理(HAProxy/Nginx 负责 TLS、重定向、多域名与静态资源,Fastify 专注请求处理,并用 trustProxy 正确还原客户端信息)、避开已知性能退化写法(正则/多参数路由、异步自定义约束、通用中间件、缺失 response schema、开启 allErrors)、修好 Kubernetes 探针的监听地址0.0.0.0 或自定义 hostname)、按延迟或吞吐目标规划 vCPU(2 vCPU 换延迟,1 vCPU 或更小规格换吞吐,最终以自行压测为准)。以上结论均可在当前仓库中交叉验证:监听默认行为见 lib/server.js,代理头信任见 docs/Reference/Server.md,校验选项见 docs/Reference/Validation-and-Serialization.md,路由与约束行为见 docs/Reference/Routes.md

登录后查看全文
热门项目推荐
相关项目推荐