跳到主要内容

Feathers.js 部署与运维 - 生产环境最佳实践

· 阅读需 12 分钟
一介布衣
全栈开发者

发布时间:2024-07-15
作者:一介布衣
标签:Feathers.js, 部署, 运维, Docker, 生产环境

前言

经过前面十几篇文章的学习,相信大家已经能够熟练开发 Feathers.js 应用了。今天咱们来学习最后也是最重要的一环 - 如何将应用部署到生产环境。说实话,开发和部署是两个完全不同的世界,很多在开发环境运行良好的应用,到了生产环境就各种问题。

我记得第一次部署 Node.js 应用的时候,踩了无数的坑:环境变量配置错误、数据库连接超时、内存泄漏、进程崩溃等等。后来总结了一套完整的部署流程,才让应用在生产环境稳定运行。

今天我就把这些年积累的部署和运维经验分享给大家,让你的 Feathers.js 应用能够稳定、高效地运行在生产环境中。

生产环境准备

1. 环境配置

// 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. 安全配置

// 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. 日志配置

// 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
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. 健康检查

// 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

# 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.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. 应用监控

// 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. 性能监控

// 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. 自动化部署

#!/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. 零停机部署

#!/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. 进程管理

// 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. 数据库优化

// 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 这个强大的框架。


系列文章回顾:

感谢大家的阅读和支持!如有问题欢迎留言讨论!