Feathers.js 部署与运维 - 生产环境最佳实践
发布时间:2024-07-15
作者:一介布衣
标签:Feathers.js, 部署, 运维, Docker, 生产环境
前言
经过前面十几篇文章的学习,相信大家已经能够熟练开发 Feathers.js 应用了。今天咱们来学习最后也是最重要的一环 - 如何将应用部署到生产环境。说实话,开发和部署是两个完全不同的世界,很多在开发环境运行良好的应用,到了生产环境就各种问题。
我记得第一次部署 Node.js 应用的时候,踩了无数的坑:环境变量配置错误、数据库连接超时、内存泄漏、进程崩溃等等。后来总结了一套完整的部署流程,才让应用在生产环境稳定运行。
今天我就把这些年积累的部署和运维经验分享给大家,让你的 Feathers.js 应用能够稳定、高效地运行在生产环境中。
生产环境准备
1. 环境配置
javascript
// config/production.json
{
"host": "0.0.0.0",
"port": 3030,
"public": "./public/",
"origins": [
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"mongodb": "mongodb://username:password@mongodb:27017/production_db?authSource=admin",
"redis": {
"host": "redis",
"port": 6379,
"password": "your_redis_password"
},
"authentication": {
"secret": "your-super-secret-production-key",
"strategies": ["jwt", "local"],
"path": "/authentication",
"service": "users",
"jwt": {
"header": { "typ": "access" },
"audience": "https://yourdomain.com",
"subject": "anonymous",
"issuer": "feathers",
"algorithm": "HS256",
"expiresIn": "7d"
}
},
"paginate": {
"default": 10,
"max": 100
},
"rateLimit": {
"windowMs": 900000,
"max": 100
},
"cors": {
"origin": ["https://yourdomain.com", "https://www.yourdomain.com"],
"credentials": true
}
}
// .env.production
NODE_ENV=production
PORT=3030
HOST=0.0.0.0
# 数据库
MONGODB_URI=mongodb://username:password@mongodb:27017/production_db?authSource=admin
REDIS_URL=redis://username:password@redis:6379
# 认证
JWT_SECRET=your-super-secret-production-key
BCRYPT_ROUNDS=12
# 文件上传
UPLOAD_PATH=/app/uploads
MAX_FILE_SIZE=10485760
# 邮件服务
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
# 监控
SENTRY_DSN=https://your-sentry-dsn
LOG_LEVEL=info
# 缓存
CACHE_TTL=3600
CACHE_MAX_SIZE=1000
2. 安全配置
javascript
// src/app.js - 生产环境安全配置
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const compression = require('compression');
// 安全头部
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", "wss:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// 压缩响应
app.use(compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 100个请求
message: {
error: {
message: '请求过于频繁,请稍后再试',
code: 429
}
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
// 跳过健康检查
return req.path === '/health';
}
});
app.use('/api', limiter);
// API 特定的速率限制
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 登录尝试限制
skipSuccessfulRequests: true
});
app.use('/authentication', authLimiter);
3. 日志配置
javascript
// src/logger.js
const winston = require('winston');
const { format } = winston;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.json()
),
defaultMeta: {
service: 'feathers-app',
version: process.env.npm_package_version
},
transports: [
// 错误日志
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,
maxFiles: 10
})
]
});
// 开发环境控制台输出
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: format.combine(
format.colorize(),
format.simple()
)
}));
}
// 生产环境错误监控
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1
});
logger.add(new winston.transports.Console({
level: 'error',
format: format.combine(
format.timestamp(),
format.json()
),
handleExceptions: true,
handleRejections: true
}));
}
module.exports = logger;
Docker 容器化
1. Dockerfile
dockerfile
# Dockerfile
FROM node:18-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制 package 文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production && npm cache clean --force
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产镜像
FROM node:18-alpine AS production
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S feathers -u 1001
# 设置工作目录
WORKDIR /app
# 复制依赖和构建结果
COPY --from=builder --chown=feathers:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=feathers:nodejs /app/dist ./dist
COPY --from=builder --chown=feathers:nodejs /app/package*.json ./
COPY --from=builder --chown=feathers:nodejs /app/config ./config
# 创建必要的目录
RUN mkdir -p logs uploads && chown -R feathers:nodejs logs uploads
# 切换到非 root 用户
USER feathers
# 暴露端口
EXPOSE 3030
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# 启动应用
CMD ["node", "dist/index.js"]
2. 健康检查
javascript
// healthcheck.js
const http = require('http');
const options = {
host: 'localhost',
port: process.env.PORT || 3030,
path: '/health',
timeout: 2000
};
const request = http.request(options, (res) => {
console.log(`健康检查状态: \${res.statusCode}`);
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on('error', (err) => {
console.log('健康检查失败:', err);
process.exit(1);
});
request.end();
3. Docker Compose
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3030:3030"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/feathers_app
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
volumes:
- ./uploads:/app/uploads
- ./logs:/app/logs
restart: unless-stopped
networks:
- app-network
mongo:
image: mongo:5.0
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password
- MONGO_INITDB_DATABASE=feathers_app
volumes:
- mongo-data:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
restart: unless-stopped
networks:
- app-network
redis:
image: redis:7-alpine
command: redis-server --requirepass password
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:ro
- ./ssl:/etc/nginx/ssl:ro
- ./uploads:/var/www/uploads:ro
depends_on:
- app
restart: unless-stopped
networks:
- app-network
volumes:
mongo-data:
redis-data:
networks:
app-network:
driver: bridge
4. Nginx 配置
nginx
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream app {
server app:3030;
}
# 速率限制
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=1r/s;
# 文件上传大小限制
client_max_body_size 10M;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL 配置
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# 安全头部
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
# API 代理
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app/;
proxy_http_version 1.1;
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;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# 认证接口特殊限制
location /api/authentication {
limit_req zone=auth burst=5 nodelay;
proxy_pass http://app/authentication;
proxy_http_version 1.1;
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;
}
# WebSocket 支持
location /socket.io/ {
proxy_pass http://app/socket.io/;
proxy_http_version 1.1;
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;
}
# 静态文件
location /uploads/ {
alias /var/www/uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# 健康检查
location /health {
proxy_pass http://app/health;
access_log off;
}
}
}
监控和日志
1. 应用监控
javascript
// src/monitoring.js
const prometheus = require('prom-client');
// 创建指标收集器
const collectDefaultMetrics = prometheus.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
// 自定义指标
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP 请求持续时间',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'HTTP 请求总数',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new prometheus.Gauge({
name: 'websocket_connections_active',
help: '活跃的 WebSocket 连接数'
});
const databaseOperations = new prometheus.Counter({
name: 'database_operations_total',
help: '数据库操作总数',
labelNames: ['operation', 'collection', 'status']
});
// 中间件
const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route?.path || req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
});
next();
};
// 导出指标
module.exports = {
metricsMiddleware,
register: prometheus.register,
metrics: {
httpRequestDuration,
httpRequestTotal,
activeConnections,
databaseOperations
}
};
2. 性能监控
javascript
// src/performance.js
const logger = require('./logger');
class PerformanceMonitor {
constructor() {
this.metrics = {
requests: new Map(),
memory: [],
cpu: []
};
this.startMonitoring();
}
startMonitoring() {
// 内存监控
setInterval(() => {
const memUsage = process.memoryUsage();
this.metrics.memory.push({
timestamp: Date.now(),
rss: memUsage.rss,
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal,
external: memUsage.external
});
// 保留最近1小时的数据
const oneHourAgo = Date.now() - 60 * 60 * 1000;
this.metrics.memory = this.metrics.memory.filter(
m => m.timestamp > oneHourAgo
);
// 内存泄漏检测
if (memUsage.heapUsed > 500 * 1024 * 1024) { // 500MB
logger.warn('内存使用过高', {
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal
});
}
}, 30000); // 30秒检查一次
// CPU 监控
setInterval(() => {
const cpuUsage = process.cpuUsage();
this.metrics.cpu.push({
timestamp: Date.now(),
user: cpuUsage.user,
system: cpuUsage.system
});
// 保留最近1小时的数据
const oneHourAgo = Date.now() - 60 * 60 * 1000;
this.metrics.cpu = this.metrics.cpu.filter(
c => c.timestamp > oneHourAgo
);
}, 30000);
}
trackRequest(req, res, next) {
const start = process.hrtime.bigint();
const requestId = req.headers['x-request-id'] || Math.random().toString(36);
req.requestId = requestId;
req.startTime = start;
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1000000; // ms
const requestInfo = {
id: requestId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: duration,
userAgent: req.headers['user-agent'],
ip: req.ip,
timestamp: new Date()
};
// 慢请求警告
if (duration > 5000) { // 5秒
logger.warn('慢请求检测', requestInfo);
}
// 记录请求
logger.info('请求完成', requestInfo);
});
next();
}
getMetrics() {
return {
memory: this.getMemoryStats(),
cpu: this.getCpuStats(),
uptime: process.uptime(),
version: process.version,
pid: process.pid
};
}
getMemoryStats() {
if (this.metrics.memory.length === 0) return null;
const latest = this.metrics.memory[this.metrics.memory.length - 1];
const avg = this.metrics.memory.reduce((sum, m) => sum + m.heapUsed, 0) / this.metrics.memory.length;
return {
current: latest,
average: avg,
samples: this.metrics.memory.length
};
}
getCpuStats() {
if (this.metrics.cpu.length === 0) return null;
const latest = this.metrics.cpu[this.metrics.cpu.length - 1];
return {
current: latest,
samples: this.metrics.cpu.length
};
}
}
module.exports = new PerformanceMonitor();
部署脚本
1. 自动化部署
bash
#!/bin/bash
# deploy.sh
set -e
echo "🚀 开始部署 Feathers.js 应用..."
# 配置
APP_NAME="feathers-app"
DOCKER_IMAGE="$APP_NAME:latest"
CONTAINER_NAME="$APP_NAME-container"
# 构建镜像
echo "📦 构建 Docker 镜像..."
docker build -t $DOCKER_IMAGE .
# 停止旧容器
echo "🛑 停止旧容器..."
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true
# 启动新容器
echo "🔄 启动新容器..."
docker-compose up -d
# 等待应用启动
echo "⏳ 等待应用启动..."
sleep 30
# 健康检查
echo "🏥 执行健康检查..."
if curl -f http://localhost:3030/health; then
echo "✅ 部署成功!"
else
echo "❌ 健康检查失败,回滚部署..."
docker-compose down
exit 1
fi
# 清理旧镜像
echo "🧹 清理旧镜像..."
docker image prune -f
echo "🎉 部署完成!"
2. 零停机部署
bash
#!/bin/bash
# zero-downtime-deploy.sh
set -e
APP_NAME="feathers-app"
NEW_IMAGE="$APP_NAME:$(date +%s)"
BLUE_CONTAINER="$APP_NAME-blue"
GREEN_CONTAINER="$APP_NAME-green"
# 检查当前运行的容器
if docker ps | grep -q $BLUE_CONTAINER; then
CURRENT_CONTAINER=$BLUE_CONTAINER
NEW_CONTAINER=$GREEN_CONTAINER
NEW_PORT=3031
else
CURRENT_CONTAINER=$GREEN_CONTAINER
NEW_CONTAINER=$BLUE_CONTAINER
NEW_PORT=3030
fi
echo "🔄 当前容器: $CURRENT_CONTAINER"
echo "🆕 新容器: $NEW_CONTAINER"
# 构建新镜像
echo "📦 构建新镜像..."
docker build -t $NEW_IMAGE .
# 启动新容器
echo "🚀 启动新容器..."
docker run -d \
--name $NEW_CONTAINER \
--network feathers-network \
-p $NEW_PORT:3030 \
-e NODE_ENV=production \
$NEW_IMAGE
# 等待新容器就绪
echo "⏳ 等待新容器就绪..."
for i in {1..30}; do
if curl -f http://localhost:$NEW_PORT/health; then
echo "✅ 新容器就绪"
break
fi
if [ $i -eq 30 ]; then
echo "❌ 新容器启动失败"
docker stop $NEW_CONTAINER
docker rm $NEW_CONTAINER
exit 1
fi
sleep 2
done
# 更新负载均衡器配置
echo "🔄 更新负载均衡器..."
# 这里需要根据你的负载均衡器类型进行配置
# 例如更新 Nginx upstream 配置
# 等待流量切换完成
echo "⏳ 等待流量切换..."
sleep 10
# 停止旧容器
echo "🛑 停止旧容器..."
docker stop $CURRENT_CONTAINER || true
docker rm $CURRENT_CONTAINER || true
echo "🎉 零停机部署完成!"
生产环境优化
1. 进程管理
javascript
// ecosystem.config.js - PM2 配置
module.exports = {
apps: [{
name: 'feathers-app',
script: './dist/index.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production',
PORT: 3030
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
max_memory_restart: '1G',
node_args: '--max-old-space-size=1024',
watch: false,
ignore_watch: ['node_modules', 'logs'],
max_restarts: 10,
min_uptime: '10s'
}]
};
2. 数据库优化
javascript
// src/mongodb-optimized.js
const { MongoClient } = require('mongodb');
const mongoOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
// 连接池配置
maxPoolSize: 20,
minPoolSize: 5,
maxIdleTimeMS: 30000,
// 连接超时
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
connectTimeoutMS: 10000,
// 重试配置
retryWrites: true,
retryReads: true,
// 压缩
compressors: ['zlib'],
zlibCompressionLevel: 6,
// 读写关注
readPreference: 'secondaryPreferred',
readConcern: { level: 'majority' },
writeConcern: { w: 'majority', j: true }
};
module.exports = function (app) {
const connection = app.get('mongodb');
const mongoClient = MongoClient.connect(connection, mongoOptions)
.then(client => {
console.log('MongoDB 连接成功');
// 监控连接池
client.on('connectionPoolCreated', () => {
console.log('MongoDB 连接池已创建');
});
client.on('connectionPoolClosed', () => {
console.log('MongoDB 连接池已关闭');
});
return client.db();
})
.catch(error => {
console.error('MongoDB 连接失败:', error);
process.exit(1);
});
app.set('mongoClient', mongoClient);
};
总结
通过这篇文章,我们学习了 Feathers.js 应用的完整部署流程:
✅ 生产环境准备:
- 环境配置和安全设置
- 日志和监控配置
- 性能优化策略
✅ 容器化部署:
- Docker 镜像构建
- Docker Compose 编排
- Nginx 反向代理配置
✅ 监控和运维:
- 应用性能监控
- 日志收集和分析
- 健康检查机制
✅ 自动化部署:
- 部署脚本编写
- 零停机部署策略
- 进程管理优化
掌握了这些知识,你就能够将 Feathers.js 应用稳定、高效地部署到生产环境,并进行有效的运维管理。
至此,我们的 Feathers.js 系列文章就全部完成了!从入门到精通,从开发到部署,希望这个系列能够帮助大家真正掌握 Feathers.js 这个强大的框架。
系列文章回顾:
感谢大家的阅读和支持!如有问题欢迎留言讨论!