Appearance
Node.js 部署与运维
Node.js应用的部署和运维是确保应用稳定运行的关键环节。本章详细介绍Node.js应用的部署策略、运维实践和监控方法。
部署策略
环境配置
javascript
// config.js - 环境配置管理
const path = require('path');
class Config {
constructor() {
this.env = process.env.NODE_ENV || 'development';
this.isProduction = this.env === 'production';
this.isDevelopment = this.env === 'development';
this.isTest = this.env === 'test';
this.port = process.env.PORT || 3000;
this.host = process.env.HOST || '0.0.0.0';
this.database = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'user',
password: process.env.DB_PASSWORD || 'password',
ssl: this.isProduction
};
this.jwt = {
secret: process.env.JWT_SECRET || 'dev-secret',
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
};
this.redis = {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
};
this.logLevel = process.env.LOG_LEVEL || 'info';
}
validate() {
const required = this.isProduction
? ['DB_PASSWORD', 'JWT_SECRET', 'SESSION_SECRET']
: [];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`生产环境缺少必需的环境变量: ${missing.join(', ')}`);
}
}
}
const config = new Config();
config.validate();
module.exports = config;
Docker部署
text
# Dockerfile
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制应用代码
COPY . .
# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# 更改文件所有权
RUN chown -R nextjs:nodejs /app
USER nextjs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# 启动命令
CMD ["npm", "start"]
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- DB_HOST=database
- DB_USER=postgres
- DB_PASSWORD=secretpassword
- REDIS_HOST=redis
depends_on:
- database
- redis
restart: unless-stopped
networks:
- app-network
database:
image: postgres:14
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=secretpassword
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stopped
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridge
PM2进程管理
javascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-node-app',
script: './app.js',
// 环境变量
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
// 进程管理
instances: 'max', // 使用所有CPU核心
exec_mode: 'cluster', // 集群模式
max_memory_restart: '1G', // 内存超限自动重启
// 监控
watch: false, // 生产环境关闭文件监听
ignore_watch: ['node_modules', 'logs'],
// 日志
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
// 性能
max_restarts: 10,
min_uptime: '10s',
// 安全
kill_timeout: 5000,
wait_ready: true
}]
};
CI/CD流水线
GitHub Actions部署
yaml
# .github/workflows/deploy.yml
name: Deploy Node.js App
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linting
run: npm run lint
- name: Run security audit
run: npm audit --audit-level moderate
build-and-deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Install dependencies
run: npm ci --only=production
- name: Build
run: npm run build
env:
NODE_ENV: production
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm install --production
pm2 reload my-node-app
pm2 save
部署脚本
bash
#!/bin/bash
# deploy.sh - 部署脚本
set -e # 遇到错误立即退出
echo "开始部署..."
# 检查Node.js版本
NODE_VERSION=$(node -v)
echo "Node.js 版本: $NODE_VERSION"
# 检查npm版本
NPM_VERSION=$(npm -v)
echo "NPM 版本: $NPM_VERSION"
# 检查环境变量
if [ -z "$NODE_ENV" ]; then
echo "错误: NODE_ENV 未设置"
exit 1
fi
if [ "$NODE_ENV" = "production" ]; then
# 生产环境检查
if [ -z "$DB_PASSWORD" ] || [ -z "$JWT_SECRET" ]; then
echo "错误: 生产环境必需的环境变量未设置"
exit 1
fi
fi
# 拉取最新代码
echo "拉取最新代码..."
git pull origin main
# 安装依赖
echo "安装依赖..."
npm ci --only=production
# 运行数据库迁移
echo "运行数据库迁移..."
npm run migrate
# 构建应用
if [ "$NODE_ENV" = "production" ]; then
echo "构建生产环境应用..."
npm run build
fi
# 重启应用
echo "重启应用..."
pm2 reload my-node-app
# 等待应用启动
sleep 5
# 运行健康检查
HEALTH_CHECK=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ $HEALTH_CHECK -eq 200 ]; then
echo "部署成功!"
pm2 save
else
echo "健康检查失败,回滚..."
pm2 reload my-node-app --reverse
exit 1
fi
echo "部署完成!"
监控和日志
应用监控
javascript
// monitoring.js - 应用监控
const os = require('os');
const cluster = require('cluster');
class ApplicationMonitor {
constructor() {
this.metrics = {
cpu: [],
memory: [],
requests: [],
errors: []
};
this.startMonitoring();
}
startMonitoring() {
// CPU使用率监控
setInterval(() => {
const cpuUsage = process.cpuUsage();
const loadAvg = os.loadavg();
this.metrics.cpu.push({
timestamp: Date.now(),
usage: cpuUsage,
loadAvg: loadAvg,
processCpu: process.cpuUsage()
});
// 限制历史数据大小
if (this.metrics.cpu.length > 1000) {
this.metrics.cpu.shift();
}
}, 5000);
// 内存使用监控
setInterval(() => {
const memoryUsage = process.memoryUsage();
this.metrics.memory.push({
timestamp: Date.now(),
usage: memoryUsage,
heapUsedPercent: (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100
});
if (this.metrics.memory.length > 1000) {
this.metrics.memory.shift();
}
}, 5000);
}
getMetrics() {
return {
cpu: this.metrics.cpu[this.metrics.cpu.length - 1],
memory: this.metrics.memory[this.metrics.memory.length - 1],
uptime: process.uptime(),
pid: process.pid,
ppid: process.ppid,
title: process.title,
version: process.version,
platform: process.platform,
arch: process.arch
};
}
logRequest(req, res, startTime) {
const duration = Date.now() - startTime;
this.metrics.requests.push({
timestamp: Date.now(),
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: duration,
ip: req.ip,
userAgent: req.get('User-Agent')
});
if (this.metrics.requests.length > 10000) {
this.metrics.requests.shift();
}
}
logError(error, context = {}) {
this.metrics.errors.push({
timestamp: Date.now(),
message: error.message,
stack: error.stack,
context: context
});
if (this.metrics.errors.length > 1000) {
this.metrics.errors.shift();
}
}
}
// Express中间件集成
function monitoringMiddleware(monitor) {
return (req, res, next) => {
const startTime = Date.now();
// 记录响应结束事件
res.on('finish', () => {
monitor.logRequest(req, res, startTime);
});
next();
};
}
// 错误处理中间件
function errorLoggingMiddleware(monitor) {
return (err, req, res, next) => {
monitor.logError(err, {
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
next(err);
};
}
const monitor = new ApplicationMonitor();
module.exports = { monitor, monitoringMiddleware, errorLoggingMiddleware };
日志管理
javascript
// logger.js - 高级日志管理
const winston = require('winston'); // npm install winston
const LokiTransport = require('winston-loki'); // npm install winston-loki
// 创建logger实例
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'my-node-app' },
transports: [
// 控制台输出
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// 文件输出
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// 在生产环境添加远程日志
if (process.env.NODE_ENV === 'production') {
logger.add(new LokiTransport({
host: process.env.LOKI_HOST || 'http://localhost:3100',
json: true,
labels: {
job: 'nodejs-app',
instance: os.hostname()
},
level: 'info'
}));
}
// 日志中间件
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent')
});
});
next();
}
module.exports = { logger, requestLogger };
性能优化
缓存策略
javascript
// cache.js - 多层缓存实现
const NodeCache = require('node-cache'); // npm install node-cache
const redis = require('redis');
class MultiLevelCache {
constructor() {
// 内存缓存(快速访问)
this.memoryCache = new NodeCache({ stdTTL: 600 }); // 10分钟
// Redis缓存(分布式缓存)
this.redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
this.redisClient.on('error', (err) => {
console.error('Redis错误:', err);
});
}
async get(key) {
// 首先检查内存缓存
let value = this.memoryCache.get(key);
if (value !== undefined) {
return value;
}
// 然后检查Redis缓存
try {
const redisValue = await this.redisClient.get(key);
if (redisValue) {
// 将值存入内存缓存以加速后续访问
this.memoryCache.set(key, JSON.parse(redisValue));
return JSON.parse(redisValue);
}
} catch (err) {
console.error('Redis获取失败:', err);
}
return null;
}
async set(key, value, ttl = 600) {
// 同时设置内存和Redis缓存
this.memoryCache.set(key, value, ttl);
try {
await this.redisClient.setEx(key, ttl, JSON.stringify(value));
} catch (err) {
console.error('Redis设置失败:', err);
}
}
async del(key) {
// 同时删除内存和Redis缓存
this.memoryCache.del(key);
try {
await this.redisClient.del(key);
} catch (err) {
console.error('Redis删除失败:', err);
}
}
async clear() {
this.memoryCache.flushAll();
try {
await this.redisClient.flushdb();
} catch (err) {
console.error('Redis清空失败:', err);
}
}
}
// 缓存中间件
function cacheMiddleware(cache, ttl = 300) {
return async (req, res, next) => {
const cacheKey = `response:${req.originalUrl}`;
const cachedResponse = await cache.get(cacheKey);
if (cachedResponse) {
res.set('X-Cache', 'HIT');
res.status(cachedResponse.statusCode).json(cachedResponse.data);
return;
}
// 包装res.json方法以捕获响应
const originalJson = res.json;
res.json = function(data) {
// 存储响应到缓存
cache.set(cacheKey, {
statusCode: res.statusCode,
data: data
}, ttl);
res.set('X-Cache', 'MISS');
originalJson.call(this, data);
};
next();
};
}
const cache = new MultiLevelCache();
module.exports = { cache, cacheMiddleware };
数据库连接池优化
javascript
// database.js - 优化的数据库连接池
const mysql = require('mysql2/promise');
class DatabasePool {
constructor() {
this.pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'test',
// 连接池配置
connectionLimit: process.env.DB_CONNECTION_LIMIT || 10,
queueLimit: 0,
acquireTimeout: 60000,
timeout: 60000,
// 连接保活
enableKeepAlive: true,
keepAliveInitialDelay: 0,
// SSL配置(生产环境)
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: false
} : false,
// 字符集
charset: 'utf8mb4'
});
}
async query(sql, params) {
let connection;
try {
connection = await this.pool.getConnection();
const [results] = await connection.execute(sql, params);
return results;
} catch (err) {
console.error('数据库查询错误:', err);
throw err;
} finally {
if (connection) {
connection.release();
}
}
}
async transaction(transactionFn) {
const connection = await this.pool.getConnection();
try {
await connection.beginTransaction();
const result = await transactionFn(connection);
await connection.commit();
return result;
} catch (err) {
await connection.rollback();
throw err;
} finally {
connection.release();
}
}
async healthCheck() {
try {
const connection = await this.pool.getConnection();
await connection.ping();
connection.release();
return { status: 'healthy', timestamp: new Date() };
} catch (err) {
return { status: 'unhealthy', error: err.message, timestamp: new Date() };
}
}
async close() {
await this.pool.end();
}
}
module.exports = new DatabasePool();
部署检查清单
部署前检查
javascript
// deployment-checklist.js
class DeploymentChecklist {
static async runPreDeploymentChecks() {
console.log('开始部署前检查...');
const checks = [
this.checkEnvironmentVariables,
this.checkDatabaseConnection,
this.checkDependencies,
this.checkDiskSpace,
this.checkPortAvailability,
this.checkSSLConfiguration
];
let allPassed = true;
for (const check of checks) {
try {
await check();
console.log(`✓ ${check.name} 检查通过`);
} catch (error) {
console.error(`✗ ${check.name} 检查失败:`, error.message);
allPassed = false;
}
}
if (!allPassed) {
throw new Error('部署前检查未全部通过');
}
console.log('所有部署前检查通过!');
return true;
}
static async checkEnvironmentVariables() {
const required = ['NODE_ENV', 'PORT', 'DB_HOST', 'DB_USER', 'DB_PASSWORD'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`缺少环境变量: ${missing.join(', ')}`);
}
}
static async checkDatabaseConnection() {
// 实际的数据库连接检查
console.log('检查数据库连接...');
// 这里应该实现实际的数据库连接测试
}
static async checkDependencies() {
// 检查依赖版本和安全漏洞
console.log('检查依赖...');
// 这里应该实现依赖检查逻辑
}
static async checkDiskSpace() {
// 检查磁盘空间
console.log('检查磁盘空间...');
// 这里应该实现磁盘空间检查
}
static async checkPortAvailability() {
// 检查端口可用性
console.log('检查端口可用性...');
// 这里应该实现端口检查逻辑
}
static async checkSSLConfiguration() {
// 检查SSL配置
console.log('检查SSL配置...');
// 这里应该实现SSL检查逻辑
}
}
module.exports = DeploymentChecklist;
健康检查和自愈
健康检查端点
javascript
// health-check.js
const os = require('os');
const database = require('./database');
class HealthChecker {
constructor() {
this.startupTime = new Date();
}
async getHealthStatus() {
const checks = await Promise.allSettled([
this.checkDatabase(),
this.checkMemory(),
this.checkDisk(),
this.checkLoad()
]);
const status = {
status: 'pass',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {
database: this.formatCheckResult(checks[0]),
memory: this.formatCheckResult(checks[1]),
disk: this.formatCheckResult(checks[2]),
load: this.formatCheckResult(checks[3])
}
};
// 如果有任何检查失败,将整体状态设为fail
if (checks.some(result => result.status === 'rejected')) {
status.status = 'fail';
}
return status;
}
async checkDatabase() {
const result = await database.healthCheck();
return result;
}
checkMemory() {
const usage = process.memoryUsage();
const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
return {
status: heapUsedPercent < 80 ? 'pass' : 'warn',
heapUsedPercent: heapUsedPercent,
usage: usage
};
}
checkDisk() {
// 简化的磁盘检查(实际应用中需要更复杂的实现)
const total = os.totalmem();
const free = os.freemem();
const usedPercent = ((total - free) / total) * 100;
return {
status: usedPercent < 80 ? 'pass' : 'warn',
usedPercent: usedPercent
};
}
checkLoad() {
const load = os.loadavg();
const avgLoad = load[0];
return {
status: avgLoad < 2.0 ? 'pass' : 'warn',
load: load
};
}
formatCheckResult(result) {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
status: 'fail',
error: result.reason.message
};
}
}
}
const healthChecker = new HealthChecker();
// 在Express应用中使用
function healthCheckMiddleware(healthChecker) {
return async (req, res) => {
try {
const status = await healthChecker.getHealthStatus();
res.status(status.status === 'pass' ? 200 : 503).json(status);
} catch (error) {
res.status(503).json({
status: 'fail',
error: error.message,
timestamp: new Date().toISOString()
});
}
};
}
module.exports = { healthChecker, healthCheckMiddleware };
通过实施这些部署和运维最佳实践,可以确保Node.js应用在生产环境中稳定、高效地运行。