跳到主要内容

部署与运维

问题

如何部署 Node.js 应用?PM2、Docker 有什么作用?如何实现日志管理和监控?

答案

Node.js 应用部署涉及进程管理、容器化、日志、监控等多个方面。PM2 是常用的进程管理工具,Docker 提供容器化部署方案。


PM2 进程管理

基本用法

npm install -g 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

相关链接