部署与 Web Server
在全栈开发中,完成代码编写仅仅是第一步。如何将应用安全、稳定地发布到服务器上,是全栈工程师的必修课。本节主要探讨 Nginx 反向代理以及进程管理。
1. Nginx 核心实战
Nginx 是一个高性能的 HTTP 和反向代理 web 服务器。在全栈架构中,它通常位于最前线,负责接收用户的请求,然后将其分发给后端的 Node.js 服务。
Nginx 在全栈架构中的作用
- 反向代理 (Reverse Proxy):隐藏真实后端服务器的 IP,提升安全性,同时解决跨域问题。
- 负载均衡 (Load Balancing):将流量分发到多个 Node.js 实例,提升系统的并发处理能力和可用性。
- 静态资源托管:处理 HTML/CSS/JS/图片 等静态文件,Nginx 的性能远超 Node.js。
- SSL 终结 (HTTPS 卸载):在 Nginx 层处理 HTTPS 证书的加解密,后端 Node.js 只需要处理明文的 HTTP 请求,减轻后端负担。
核心配置示例
# 定义后端 Node.js 实例组(用于负载均衡)
upstream node_backend {
server 127.0.0.1:3000 weight=1; # weight 表示权重
server 127.0.0.1:3001 weight=1;
}
server {
# 监听 80 端口,并重定向到 HTTPS (最佳实践)
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 证书配置
ssl_certificate /etc/nginx/ssl/example.crt;
ssl_certificate_key /etc/nginx/ssl/example.key;
# 1. 静态资源托管 (前端打包产物)
location / {
root /var/www/frontend/dist;
index index.html;
# 单页应用(SPA)路由兜底配置,防止刷新 404
try_files $uri $uri/ /index.html;
}
# 2. 反向代理到 Node.js 接口
location /api/ {
proxy_pass http://node_backend; # 转发到上面定义的 upstream
# 携带真实的客户端信息给后端
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;
}
}
进阶:负载均衡的常见策略
在 upstream 块中,除了默认的轮询(Round Robin)和权重(Weight)外,Nginx 还提供了其他流量分发策略,你可以根据业务场景进行选择:
IP Hash (
ip_hash): 每个请求按访问客户端 IP 的 hash 结果分配。这样同一个访客会固定访问同一个后端服务器,能有效解决 Node.js 多实例下基于内存的 Session 不共享问题(虽然现在更推荐用 Redis/JWT 解决)。upstream node_backend {
ip_hash;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}最少连接 (
least_conn): 把请求优先转发给当前活跃连接数最少的后端服务器,适合处理请求耗时差异较大的场景。upstream node_backend {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
进阶:静态资源托管与性能优化
处理 HTML/CSS/JS/图片 等文件时,单靠 root 指令仅仅是能访问。为了达到真正的“远超 Node.js”的性能,通常需要为静态资源配置强缓存和Gzip 压缩。
server {
listen 80;
server_name static.example.com;
# 1. 开启 Gzip 压缩,极大减小 JS/CSS/JSON 传输体积
gzip on;
gzip_min_length 1k; # 小于 1kb 的文件不压缩,因为压缩本身也消耗 CPU
gzip_comp_level 6; # 压缩级别(1-9),6 是性能与压缩率的最佳折中
gzip_types text/plain application/javascript text/css application/json image/svg+xml;
gzip_vary on;
# 2. 针对单页应用 (SPA) 的基础路由兜底
location / {
root /var/www/frontend/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 3. 对带 Hash 的静态文件 (js, css, 图片等) 开启强缓存
# 前提:前端构建工具(Webpack/Vite)输出的文件名必须带 hash,如 main.a1b2c3.js
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|eot|ttf|otf)$ {
root /var/www/frontend/dist;
expires 30d; # 告诉浏览器直接缓存 30 天,期间无需向服务器发请求
add_header Cache-Control "public, max-age=2592000, immutable";
access_log off; # 静态资源不打印访问日志,减少磁盘 I/O
}
}
2. 进程管理与 PM2
Node.js 是单线程的,如果发生未捕获的异常,整个进程就会崩溃退出。在生产环境中,我们需要一种机制来保证 Node.js 进程的高可用性。
PM2 的核心能力
PM2 是一个带有负载均衡功能的 Node 应用进程管理器。
- 崩溃自动重启:当应用崩溃时,PM2 会立即将其重启。
- Cluster 模式:可以利用多核 CPU。Node.js 默认只能使用一个 CPU 核心,PM2 可以根据 CPU 核数启动多个进程,并自动进行负载均衡。
- 日志管理:集中管理应用的
stdout和stderr日志。 - 平滑重启 (0 秒停机):在更新代码时,PM2 可以逐个重启进程,保证服务始终可用。
常用 PM2 命令
npm install pm2 -g # 全局安装
# 启动
pm2 start app.js --name "my-api" # 普通启动并命名
pm2 start app.js -i max # Cluster 模式:根据 CPU 核数启动最大数量的进程
# 查看与监控
pm2 list # 查看所有管理的进程状态
pm2 monit # 打开终端监控面板(CPU、内存、日志)
pm2 logs my-api # 查看特定应用的日志
# 管理
pm2 restart my-api # 重启应用
pm2 reload my-api # 平滑重启 (Zero Downtime Reload)
pm2 stop my-api # 停止应用
pm2 delete my-api # 从 PM2 列表中删除应用
# 开机自启
pm2 startup # 生成开机自启脚本
pm2 save # 保存当前进程列表,以便开机恢复
最佳实践:生态系统文件 (ecosystem.config.js)
在项目中通常不使用命令行启动,而是使用配置文件,便于版本控制:
module.exports = {
apps: [
{
name: "my-api",
script: "./dist/main.js",
instances: "max", // 开启 Cluster 模式
exec_mode: "cluster",
env: {
NODE_ENV: "development",
},
env_production: {
NODE_ENV: "production",
PORT: 3000,
},
},
],
};
运行:pm2 start ecosystem.config.js --env production
3. 优雅退出 (Graceful Shutdown)
当服务器需要更新或重启(如收到 pm2 reload 或 Docker 容器停止的信号)时,如果直接暴力杀掉进程,会导致正在处理中的用户请求失败,数据库连接异常断开。
优雅退出的流程:
- 监听操作系统的终止信号 (如
SIGTERM,SIGINT)。 - 停止接收新的 HTTP 请求。
- 等待正在处理的请求执行完毕。
- 安全地关闭数据库连接、Redis 连接等。
- 进程主动退出。
Node.js 代码示例:
const express = require("express");
const app = express();
const server = app.listen(3000);
// 监听 Docker 或 PM2 发送的停止信号
process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);
function gracefulShutdown(signal) {
console.log(`\n${signal} signal received: closing HTTP server`);
// 1. 停止接收新请求
server.close(() => {
console.log("HTTP server closed");
// 2. 关闭数据库连接等清理工作
// db.close()
// redis.disconnect()
console.log("All connections closed. Process exiting.");
// 3. 退出进程
process.exit(0);
});
// 设置一个强制退出的超时时间 (兜底策略,防止进程假死)
setTimeout(() => {
console.error(
"Could not close connections in time, forcefully shutting down",
);
process.exit(1);
}, 10000); // 10秒后强制退出
}
4. 大厂主流部署实践 (超越 PM2)
虽然 PM2 在中小型项目或单体服务器部署中非常流行,但在现代互联网大厂的核心业务中,单纯依赖 PM2 直接在宿主机部署 Node.js 已经不再是主流。随着云原生架构的普及,部署方式发生了巨大的演变。
实践一:Docker + Kubernetes (K8s) - 目前最绝对的主流
大厂几乎所有的 Node.js 微服务都是运行在 K8s 集群中的 Docker 容器里。
为什么大厂在 K8s 中抛弃了 PM2?
- 职责重叠:PM2 的核心能力(崩溃重启、负载均衡、日志收集、平滑升级)正是 K8s 的强项。在 K8s 内部再跑一个 PM2 属于“过度设计”,会增加系统的复杂度和资源消耗。
- PID 1 问题:Docker 容器的哲学是“单进程”。通常 Node.js 应该直接作为容器的 PID 1 进程运行,这样容器才能直接接收到操作系统的
SIGTERM信号,从而执行优雅退出。如果用 PM2 启动,信号会发给 PM2,PM2 再转发给 Node.js,这增加了不可控的环节。
大厂在容器里的启动方式:
通常不再使用 PM2 的 cluster 模式,而是直接 node dist/main.js(单进程运行)。如果需要利用多核 CPU,只需在 K8s 中增加该 Pod 的 Replicas(副本数),将负载均衡的任务交给 K8s 的 Service。
实践二:Serverless / FaaS (函数即服务)
对于一些轻量级的接口、BFF(Backend For Frontend)层或者定时任务,大厂越来越倾向于使用 Serverless 架构(如 AWS Lambda, 阿里云函数计算 FC)。
- 按需运行:没有请求时,Node.js 进程根本不运行,不消耗任何资源(0 费用)。
- 弹性极强:面对突发流量,云厂商会在几百毫秒内自动拉起成百上千个 Node.js 实例来处理请求。
- 无需运维:开发者只需写好函数代码(甚至不需要写
app.listen(3000)),上传即可运行,彻底告别服务器和进程管理。
实践三:边缘计算 (Edge Computing)
这是近年来非常前沿的部署方式,代表技术是 Cloudflare Workers、Vercel Edge Functions。
传统 Node.js 服务部署在特定机房(如北京),海外用户访问会有极高的延迟。而边缘计算是将轻量级的 JavaScript/TypeScript 代码直接部署在全球各地的 CDN 节点上。 用户请求会在离他物理距离最近的节点被执行,通常用于 SSR 渲染、身份鉴权、请求拦截与重定向。由于 V8 引擎启动速度的限制,这类环境通常使用更底层的 V8 Isolate 隔离技术,而非完整的 Node.js 环境。