部署与运维
问题
如何部署 Node.js 应用?PM2、Docker 有什么作用?如何实现日志管理和监控?
答案
Node.js 应用部署涉及进程管理、容器化、日志、监控等多个方面。PM2 是常用的进程管理工具,Docker 提供容器化部署方案。
PM2 进程管理
基本用法
- npm
- Yarn
- pnpm
- Bun
npm install -g pm2
yarn global add pm2
pnpm add -g pm2
bun add --global pm2
# 启动应用
pm2 start app.js
pm2 start app.js --name my-app
pm2 start app.js -i max # 集群模式,使用所有 CPU
# 管理
pm2 list # 查看所有进程
pm2 stop my-app
pm2 restart my-app
pm2 delete my-app
pm2 reload my-app # 零停机重启
# 日志
pm2 logs
pm2 logs my-app --lines 100
# 监控
pm2 monit
配置文件
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: './dist/index.js',
instances: 'max', // 使用所有 CPU
exec_mode: 'cluster', // 集群模式
// 环境变量
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
},
// 日志
log_file: './logs/combined.log',
out_file: './logs/out.log',
error_file: './logs/error.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss',
// 重启策略
max_memory_restart: '1G', // 内存超限重启
max_restarts: 10,
min_uptime: 5000,
// 监听文件变化(开发用)
watch: false,
ignore_watch: ['node_modules', 'logs']
}]
};
# 使用配置启动
pm2 start ecosystem.config.js
pm2 start ecosystem.config.js --env production
# 保存进程列表
pm2 save
# 开机自启
pm2 startup
Docker 部署
Dockerfile
# 多阶段构建
FROM node:20-alpine AS builder
WORKDIR /app
# 安装依赖
COPY package*.json ./
RUN npm ci --only=production
# 构建
COPY . .
RUN npm run build
# 生产镜像
FROM node:20-alpine AS runner
WORKDIR /app
# 安全:非 root 用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# 复制构建产物
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
USER nodejs
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://postgres:password@db:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
postgres_data:
# 构建和运行
docker-compose up -d
docker-compose logs -f app
# 扩容
docker-compose up -d --scale app=3
日志管理
结构化日志
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label })
},
timestamp: pino.stdTimeFunctions.isoTime,
base: {
service: 'my-app',
version: process.env.npm_package_version
}
});
// 使用
logger.info({ userId: 123, action: 'login' }, 'User logged in');
logger.error({ err, requestId: 'abc' }, 'Request failed');
// Express 中间件
import pinoHttp from 'pino-http';
app.use(pinoHttp({ logger }));
日志轮转
// 使用 pino-roll
import pino from 'pino';
const transport = pino.transport({
target: 'pino-roll',
options: {
file: './logs/app',
frequency: 'daily',
mkdir: true,
size: '10m',
compress: 'gzip'
}
});
const logger = pino(transport);
日志收集(ELK)
# docker-compose.yml
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
filebeat:
image: elastic/filebeat:8.10.0
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
健康检查
import express from 'express';
const app = express();
// 存活检查
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// 就绪检查
app.get('/ready', async (req, res) => {
try {
// 检查依赖
await db.query('SELECT 1');
await redis.ping();
res.json({
status: 'ready',
checks: {
database: 'ok',
redis: 'ok'
}
});
} catch (err) {
res.status(503).json({
status: 'not ready',
error: err.message
});
}
});
// 详细指标
app.get('/metrics', (req, res) => {
res.json({
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: process.cpuUsage(),
timestamp: new Date().toISOString()
});
});
环境配置
// config.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});
export const config = envSchema.parse(process.env);
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/db
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-key-at-least-32-characters
常见面试问题
Q1: PM2 有哪些主要功能?
答案:
| 功能 | 说明 |
|---|---|
| 进程管理 | 启动、停止、重启、删除 |
| 集群模式 | 多实例负载均衡 |
| 日志管理 | 统一日志收集 |
| 监控 | CPU、内存使用监控 |
| 零停机重启 | reload 滚动重启 |
| 开机自启 | 系统重启后自动恢复 |
| 环境变量 | 不同环境配置 |
# 集群模式:充分利用多核 CPU
pm2 start app.js -i max
# 零停机重启:逐个重启实例
pm2 reload my-app
Q2: Docker 部署 Node.js 有哪些最佳实践?
答案:
# 1. 使用 Alpine 小镜像
FROM node:20-alpine
# 2. 多阶段构建减小体积
FROM node:20-alpine AS builder
# ... 构建阶段
FROM node:20-alpine AS runner
COPY --from=builder ...
# 3. 使用非 root 用户
RUN addgroup -S app && adduser -S app -G app
USER app
# 4. 只复制必要文件
COPY package*.json ./
RUN npm ci --only=production
# 5. 使用 .dockerignore
# node_modules
# *.log
# .git
# 6. 健康检查
HEALTHCHECK --interval=30s CMD curl -f http://localhost:3000/health
# 7. 正确处理信号
CMD ["node", "dist/index.js"] # 直接运行,不用 npm
Q3: 如何实现零停机部署?
答案:
# PM2 方式
pm2 reload my-app # 滚动重启
# Docker Compose 方式
docker-compose up -d --no-deps --build app
// 优雅关闭
process.on('SIGTERM', async () => {
console.log('Received SIGTERM, shutting down gracefully');
// 停止接收新请求
server.close(async () => {
// 等待现有请求完成
await Promise.all([
db.end(),
redis.quit()
]);
console.log('Server closed');
process.exit(0);
});
// 超时强制退出
setTimeout(() => {
console.error('Forced shutdown');
process.exit(1);
}, 30000);
});
Q4: 生产环境日志应该怎么做?
答案:
| 实践 | 说明 |
|---|---|
| 结构化 | JSON 格式,便于解析 |
| 级别 | 生产用 info 或 warn |
| 轮转 | 按大小或时间轮转 |
| 无敏感信息 | 过滤密码、token |
| 请求 ID | 链路追踪 |
| 集中收集 | ELK、Datadog 等 |
// 结构化日志示例
logger.info({
event: 'user_login',
userId: user.id,
ip: req.ip,
userAgent: req.headers['user-agent'],
duration: Date.now() - startTime,
requestId: req.id
});
Q5: 如何处理 Node.js 应用的环境变量?
答案:
// 1. 验证环境变量
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing ${envVar}`);
}
}
// 2. 使用 Zod 验证和转换
import { z } from 'zod';
const envSchema = z.object({
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
DEBUG: z.string().transform(v => v === 'true').default('false')
});
export const config = envSchema.parse(process.env);
// 3. 不同环境不同配置
// .env.development
// .env.production
// .env.test
// 4. Docker 中使用
// docker run -e DATABASE_URL=... app
// docker-compose.yml 中定义 environment