8.1 生产环境配置
8.1.1 环境变量配置
# .env.production
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
# 数据库配置
DB_HOST=prod-db.example.com
DB_PORT=5432
DB_NAME=myapp_prod
DB_USER=myapp_user
DB_PASSWORD=secure_password
# Redis配置
REDIS_HOST=prod-redis.example.com
REDIS_PORT=6379
REDIS_PASSWORD=redis_password
# 安全配置
JWT_SECRET=your_jwt_secret_key
SESSION_SECRET=your_session_secret
CORS_ORIGIN=https://yourdomain.com
# 第三方服务
CDN_URL=https://cdn.yourdomain.com
API_BASE_URL=https://api.yourdomain.com
SENTRY_DSN=https://your-sentry-dsn
# 缓存配置
CACHE_TTL=300
COMPONENT_CACHE_MAX=1000
PAGE_CACHE_MAX=5000
# 日志配置
LOG_LEVEL=info
LOG_FILE=/var/log/myapp/app.log
# 性能配置
CLUSTER_WORKERS=4
MAX_MEMORY=512
GC_INTERVAL=30000
8.1.2 生产构建配置
// build/webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = merge(baseConfig, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info']
},
mangle: {
safari10: true
},
format: {
comments: false
}
},
extractComments: false,
parallel: true
}),
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
normalizeWhitespace: true,
mergeLonghand: true,
mergeRules: true
}
]
}
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
enforce: true
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
},
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
name: 'vue',
chunks: 'all',
priority: 20
}
}
},
runtimeChunk: {
name: 'runtime'
}
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
// Gzip压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
deleteOriginalAssets: false
}),
// Brotli压缩
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11
},
threshold: 8192,
minRatio: 0.8,
deleteOriginalAssets: false
}),
// 包分析(可选)
...(process.env.ANALYZE ? [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
] : [])
],
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: 'warning'
}
})
8.1.3 服务器配置优化
// server/production.js
const express = require('express')
const compression = require('compression')
const helmet = require('helmet')
const rateLimit = require('express-rate-limit')
const cors = require('cors')
const cluster = require('cluster')
const numCPUs = require('os').cpus().length
const { createBundleRenderer } = require('vue-server-renderer')
const LRU = require('lru-cache')
class ProductionServer {
constructor() {
this.app = express()
this.setupMiddleware()
this.setupRenderer()
this.setupRoutes()
this.setupErrorHandling()
}
setupMiddleware() {
// 安全中间件
this.app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", process.env.CDN_URL],
scriptSrc: ["'self'", process.env.CDN_URL],
imgSrc: ["'self'", "data:", process.env.CDN_URL],
fontSrc: ["'self'", process.env.CDN_URL],
connectSrc: ["'self'", process.env.API_BASE_URL]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}))
// CORS配置
this.app.use(cors({
origin: process.env.CORS_ORIGIN.split(','),
credentials: true,
optionsSuccessStatus: 200
}))
// 压缩中间件
this.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 15分钟内最多100个请求
message: {
error: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false
})
this.app.use('/api/', limiter)
// 静态文件服务
this.app.use('/static', express.static('dist/static', {
maxAge: '1y',
etag: true,
lastModified: true,
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache')
}
}
}))
// 请求日志中间件
function requestLogger(req, res, next) {
const start = Date.now()
const requestId = require('uuid').v4()
// 添加请求ID到请求对象
req.requestId = requestId
res.setHeader('X-Request-ID', requestId)
// 记录请求开始
logger.info('Request started', {
requestId,
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
headers: req.headers
})
// 监听响应结束
res.on('finish', () => {
const duration = Date.now() - start
const logLevel = res.statusCode >= 400 ? 'error' : 'info'
logger.log(logLevel, 'Request completed', {
requestId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration,
contentLength: res.get('Content-Length'),
userAgent: req.get('User-Agent'),
ip: req.ip
})
})
next()
}
// SSR渲染日志
function logSSRRender(route, context, duration, error = null) {
if (error) {
logger.error('SSR render failed', {
route,
duration,
error: error.message,
stack: error.stack,
context: {
url: context.url,
userAgent: context.userAgent
}
})
} else {
logger.info('SSR render completed', {
route,
duration,
context: {
url: context.url,
userAgent: context.userAgent
}
})
}
}
// 性能日志
function logPerformance(metric, value, tags = {}) {
logger.info('Performance metric', {
metric,
value,
tags,
timestamp: Date.now()
})
}
// 业务日志
function logBusiness(event, data = {}) {
logger.info('Business event', {
event,
data,
timestamp: Date.now()
})
}
module.exports = {
logger,
requestLogger,
logSSRRender,
logPerformance,
logBusiness
}
8.5.2 ELK Stack集成
# logging/filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/myapp/*.log
fields:
service: vue-ssr-app
environment: production
fields_under_root: true
multiline.pattern: '^{'
multiline.negate: true
multiline.match: after
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_docker_metadata: ~
- add_kubernetes_metadata: ~
output.logstash:
hosts: ["logstash:5044"]
logging.level: info
logging.to_files: true
logging.files:
path: /var/log/filebeat
name: filebeat
keepfiles: 7
permissions: 0644
# logging/logstash.conf
input {
beats {
port => 5044
}
}
filter {
if [service] == "vue-ssr-app" {
json {
source => "message"
}
date {
match => [ "timestamp", "yyyy-MM-dd HH:mm:ss" ]
}
if [level] == "error" {
mutate {
add_tag => [ "error" ]
}
}
if [duration] {
mutate {
convert => { "duration" => "integer" }
}
}
if [statusCode] {
mutate {
convert => { "statusCode" => "integer" }
}
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "vue-ssr-app-%{+YYYY.MM.dd}"
}
if "error" in [tags] {
email {
to => "admin@yourdomain.com"
subject => "Vue SSR App Error Alert"
body => "Error detected: %{message}"
}
}
}
8.6 备份与恢复
8.6.1 数据库备份策略
#!/bin/bash
# scripts/backup-database.sh
set -e
# 配置变量
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-myapp_prod}
DB_USER=${DB_USER:-myapp_user}
BACKUP_DIR=${BACKUP_DIR:-/backups/database}
RETENTION_DAYS=${RETENTION_DAYS:-30}
S3_BUCKET=${S3_BUCKET:-myapp-backups}
# 创建备份目录
mkdir -p $BACKUP_DIR
# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${DB_NAME}_${TIMESTAMP}.sql.gz"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}"
echo "开始数据库备份: $BACKUP_FILE"
# 执行备份
PGPASSWORD=$DB_PASSWORD pg_dump \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
--verbose \
--no-password \
--format=custom \
--compress=9 \
| gzip > $BACKUP_PATH
if [ $? -eq 0 ]; then
echo "数据库备份成功: $BACKUP_PATH"
# 上传到S3
if [ ! -z "$S3_BUCKET" ]; then
echo "上传备份到S3..."
aws s3 cp $BACKUP_PATH s3://$S3_BUCKET/database/
if [ $? -eq 0 ]; then
echo "S3上传成功"
else
echo "S3上传失败"
exit 1
fi
fi
# 清理旧备份
echo "清理 $RETENTION_DAYS 天前的备份..."
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
# 清理S3旧备份
if [ ! -z "$S3_BUCKET" ]; then
aws s3 ls s3://$S3_BUCKET/database/ | \
while read -r line; do
createDate=$(echo $line | awk '{print $1" "$2}')
createDate=$(date -d "$createDate" +%s)
olderThan=$(date -d "$RETENTION_DAYS days ago" +%s)
if [[ $createDate -lt $olderThan ]]; then
fileName=$(echo $line | awk '{print $4}')
if [[ $fileName != "" ]]; then
aws s3 rm s3://$S3_BUCKET/database/$fileName
echo "删除S3旧备份: $fileName"
fi
fi
done
fi
else
echo "数据库备份失败"
exit 1
fi
echo "备份完成"
8.6.2 应用备份脚本
#!/bin/bash
# scripts/backup-application.sh
set -e
# 配置变量
APP_DIR=${APP_DIR:-/app}
BACKUP_DIR=${BACKUP_DIR:-/backups/application}
RETENTION_DAYS=${RETENTION_DAYS:-7}
S3_BUCKET=${S3_BUCKET:-myapp-backups}
# 创建备份目录
mkdir -p $BACKUP_DIR
# 生成备份文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="app_${TIMESTAMP}.tar.gz"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}"
echo "开始应用备份: $BACKUP_FILE"
# 创建应用备份
tar -czf $BACKUP_PATH \
--exclude='node_modules' \
--exclude='logs' \
--exclude='.git' \
--exclude='dist' \
-C $(dirname $APP_DIR) \
$(basename $APP_DIR)
if [ $? -eq 0 ]; then
echo "应用备份成功: $BACKUP_PATH"
# 上传到S3
if [ ! -z "$S3_BUCKET" ]; then
echo "上传备份到S3..."
aws s3 cp $BACKUP_PATH s3://$S3_BUCKET/application/
if [ $? -eq 0 ]; then
echo "S3上传成功"
else
echo "S3上传失败"
exit 1
fi
fi
# 清理旧备份
echo "清理 $RETENTION_DAYS 天前的备份..."
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
else
echo "应用备份失败"
exit 1
fi
echo "备份完成"
8.6.3 恢复脚本
#!/bin/bash
# scripts/restore-database.sh
set -e
# 配置变量
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-myapp_prod}
DB_USER=${DB_USER:-myapp_user}
BACKUP_FILE=$1
if [ -z "$BACKUP_FILE" ]; then
echo "用法: $0 <backup_file>"
exit 1
fi
if [ ! -f "$BACKUP_FILE" ]; then
echo "备份文件不存在: $BACKUP_FILE"
exit 1
fi
echo "开始数据库恢复: $BACKUP_FILE"
# 确认恢复操作
read -p "确定要恢复数据库吗?这将覆盖现有数据 (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "恢复操作已取消"
exit 1
fi
# 停止应用服务
echo "停止应用服务..."
kubectl scale deployment vue-ssr-app --replicas=0 -n vue-ssr-app
# 等待Pod停止
echo "等待Pod停止..."
kubectl wait --for=delete pod -l app=vue-ssr-app -n vue-ssr-app --timeout=60s
# 删除现有数据库
echo "删除现有数据库..."
PGPASSWORD=$DB_PASSWORD psql \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "DROP DATABASE IF EXISTS $DB_NAME;"
# 创建新数据库
echo "创建新数据库..."
PGPASSWORD=$DB_PASSWORD psql \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "CREATE DATABASE $DB_NAME;"
# 恢复数据
echo "恢复数据..."
if [[ $BACKUP_FILE == *.gz ]]; then
gunzip -c $BACKUP_FILE | PGPASSWORD=$DB_PASSWORD pg_restore \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
--verbose \
--no-password
else
PGPASSWORD=$DB_PASSWORD pg_restore \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
--verbose \
--no-password \
$BACKUP_FILE
fi
if [ $? -eq 0 ]; then
echo "数据库恢复成功"
# 重启应用服务
echo "重启应用服务..."
kubectl scale deployment vue-ssr-app --replicas=3 -n vue-ssr-app
# 等待服务就绪
echo "等待服务就绪..."
kubectl wait --for=condition=available deployment vue-ssr-app -n vue-ssr-app --timeout=300s
echo "恢复完成"
else
echo "数据库恢复失败"
exit 1
fi
8.7 安全配置
8.7.1 HTTPS配置
# ssl/cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@yourdomain.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: vue-ssr-tls
namespace: vue-ssr-app
spec:
secretName: vue-ssr-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- yourdomain.com
- www.yourdomain.com
8.7.2 安全中间件
// middleware/security.js
const rateLimit = require('express-rate-limit')
const slowDown = require('express-slow-down')
const helmet = require('helmet')
const validator = require('validator')
// 速率限制
const createRateLimit = (windowMs, max, message) => {
return rateLimit({
windowMs,
max,
message: { error: message },
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
logger.warn('Rate limit exceeded', {
ip: req.ip,
userAgent: req.get('User-Agent'),
url: req.url
})
res.status(429).json({ error: message })
}
})
}
// API限流
const apiLimiter = createRateLimit(
15 * 60 * 1000, // 15分钟
100, // 100个请求
'Too many API requests, please try again later'
)
// 登录限流
const loginLimiter = createRateLimit(
15 * 60 * 1000, // 15分钟
5, // 5次尝试
'Too many login attempts, please try again later'
)
// 慢速响应
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15分钟
delayAfter: 50, // 50个请求后开始延迟
delayMs: 500 // 每个请求延迟500ms
})
// 输入验证中间件
function validateInput(req, res, next) {
// 验证URL参数
for (const [key, value] of Object.entries(req.params)) {
if (typeof value === 'string') {
// 检查XSS
if (!validator.isLength(value, { max: 100 })) {
return res.status(400).json({ error: 'Parameter too long' })
}
// 检查SQL注入
if (value.includes(';') || value.includes('--') || value.includes('/*')) {
logger.warn('Potential SQL injection attempt', {
ip: req.ip,
parameter: key,
value: value
})
return res.status(400).json({ error: 'Invalid parameter' })
}
}
}
// 验证查询参数
for (const [key, value] of Object.entries(req.query)) {
if (typeof value === 'string') {
if (!validator.isLength(value, { max: 200 })) {
return res.status(400).json({ error: 'Query parameter too long' })
}
}
}
next()
}
// 安全头配置
const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
scriptSrc: ["'self'", "https://www.google-analytics.com"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://api.yourdomain.com"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
})
module.exports = {
apiLimiter,
loginLimiter,
speedLimiter,
validateInput,
securityHeaders
}
8.8 本章小结
本章详细介绍了Vue SSR应用的部署与运维,包括:
核心要点
生产环境配置
- 环境变量管理
- 构建优化配置
- 服务器性能调优
容器化部署
- Docker多阶段构建
- Docker Compose编排
- Nginx反向代理配置
Kubernetes部署
- 完整的K8s配置文件
- 自动扩缩容配置
- 服务发现与负载均衡
监控与告警
- Prometheus指标收集
- Grafana可视化仪表板
- 告警规则配置
日志管理
- 结构化日志配置
- ELK Stack集成
- 日志分析与查询
备份与恢复
- 数据库备份策略
- 应用备份方案
- 灾难恢复流程
安全配置
- HTTPS证书管理
- 安全中间件配置
- 输入验证与防护
最佳实践
部署策略
- 使用蓝绿部署或滚动更新
- 实施健康检查和就绪探针
- 配置资源限制和请求
监控策略
- 建立完整的监控体系
- 设置合理的告警阈值
- 定期检查和优化性能
安全策略
- 实施多层安全防护
- 定期更新依赖和补丁
- 进行安全审计和测试
练习作业
- 搭建完整的生产环境部署流程
- 配置监控告警系统
- 实施自动化备份策略
- 进行安全加固和测试
下一章我们将学习Vue SSR的高级特性与优化技巧。
this.app.use((req, res, next) => {
const start = Date.now()
res.on(‘finish’, () => {
const duration = Date.now() - start
console.log(${req.method} ${req.url} ${res.statusCode} ${duration}ms
)
})
next()
})
}
setupRenderer() { const serverBundle = require(‘../dist/vue-ssr-server-bundle.json’) const clientManifest = require(‘../dist/vue-ssr-client-manifest.json’) const template = require(‘fs’).readFileSync(‘./src/index.template.html’, ‘utf-8’)
// 创建缓存
const microCache = new LRU({
max: parseInt(process.env.COMPONENT_CACHE_MAX) || 1000,
maxAge: parseInt(process.env.CACHE_TTL) * 1000 || 300000
})
const pageCache = new LRU({
max: parseInt(process.env.PAGE_CACHE_MAX) || 5000,
maxAge: parseInt(process.env.CACHE_TTL) * 1000 || 300000
})
this.renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest,
cache: microCache,
shouldPrefetch: (file, type) => {
if (type === 'script') {
return !file.includes('chunk')
}
return true
},
shouldPreload: (file, type) => {
if (type === 'script' || type === 'style') {
return true
}
if (type === 'font') {
return file.endsWith('.woff2')
}
return false
}
})
this.pageCache = pageCache
}
setupRoutes() { // 健康检查 this.app.get(‘/health’, (req, res) => { res.json({ status: ‘ok’, timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), pid: process.pid }) })
// API路由
this.app.use('/api', require('./api'))
// SSR路由
this.app.get('*', this.handleSSR.bind(this))
}
async handleSSR(req, res) { const startTime = Date.now()
try {
// 检查页面缓存
const cacheKey = this.getCacheKey(req)
const cached = this.pageCache.get(cacheKey)
if (cached) {
res.setHeader('X-Cache', 'HIT')
return res.end(cached)
}
// 创建渲染上下文
const context = {
title: 'My App',
url: req.url,
userAgent: req.get('User-Agent'),
cookies: req.headers.cookie,
headers: req.headers
}
// 渲染页面
const html = await this.renderer.renderToString(context)
// 设置响应头
res.setHeader('Content-Type', 'text/html')
res.setHeader('X-Cache', 'MISS')
// 缓存页面
if (this.shouldCache(req)) {
this.pageCache.set(cacheKey, html)
}
const renderTime = Date.now() - startTime
res.setHeader('X-Render-Time', renderTime)
res.end(html)
} catch (error) {
this.handleRenderError(error, req, res)
}
}
getCacheKey(req) { const url = req.url const userAgent = req.get(‘User-Agent’) const acceptLanguage = req.get(‘Accept-Language’)
return `${url}:${this.hashString(userAgent)}:${this.hashString(acceptLanguage)}`
}
hashString(str) { if (!str) return “ let hash = 0 for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash // Convert to 32bit integer } return hash.toString() }
shouldCache(req) { // 不缓存包含查询参数的请求 if (Object.keys(req.query).length > 0) { return false }
// 不缓存POST请求
if (req.method !== 'GET') {
return false
}
// 不缓存包含认证信息的请求
if (req.headers.authorization || req.headers.cookie) {
return false
}
return true
}
handleRenderError(error, req, res) { console.error(‘SSR渲染错误:’, error)
// 发送错误到监控服务
if (process.env.SENTRY_DSN) {
// Sentry.captureException(error)
}
// 返回错误页面
if (error.code === 404) {
res.status(404).send('页面未找到')
} else {
res.status(500).send('服务器内部错误')
}
}
setupErrorHandling() { // 404处理 this.app.use((req, res) => { res.status(404).json({ error: ‘Not Found’ }) })
// 错误处理
this.app.use((error, req, res, next) => {
console.error('服务器错误:', error)
if (res.headersSent) {
return next(error)
}
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: error.message
})
})
// 进程错误处理
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error)
process.exit(1)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason)
process.exit(1)
})
}
start() { const port = process.env.PORT || 3000 const host = process.env.HOST || ‘0.0.0.0’
this.app.listen(port, host, () => {
console.log(`服务器运行在 http://${host}:${port}`)
})
} }
// 集群模式启动 if (cluster.isMaster && process.env.NODE_ENV === ‘production’) { const workers = parseInt(process.env.CLUSTER_WORKERS) || numCPUs
console.log(主进程 ${process.pid} 正在运行
)
console.log(启动 ${workers} 个工作进程
)
for (let i = 0; i < workers; i++) { cluster.fork() }
cluster.on(‘exit’, (worker, code, signal) => {
console.log(工作进程 ${worker.process.pid} 已退出
)
if (code !== 0 && !worker.exitedAfterDisconnect) {
console.log(‘启动新的工作进程…’)
cluster.fork()
}
})
} else {
const server = new ProductionServer()
server.start()
}
## 8.2 容器化部署
### 8.2.1 Docker配置
```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 nextjs -u 1001
# 设置工作目录
WORKDIR /app
# 安装dumb-init
RUN apk add --no-cache dumb-init
# 复制构建产物
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package*.json ./
COPY --from=builder --chown=nextjs:nodejs /app/server ./server
# 创建日志目录
RUN mkdir -p /var/log/myapp && chown nextjs:nodejs /var/log/myapp
# 切换到非root用户
USER nextjs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# 启动应用
CMD ["dumb-init", "node", "server/production.js"]
// healthcheck.js
const http = require('http')
const options = {
hostname: 'localhost',
port: process.env.PORT || 3000,
path: '/health',
method: 'GET',
timeout: 2000
}
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
process.exit(0)
} else {
process.exit(1)
}
})
req.on('error', () => {
process.exit(1)
})
req.on('timeout', () => {
req.destroy()
process.exit(1)
})
req.end()
8.2.2 Docker Compose配置
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
- postgres
- redis
volumes:
- ./logs:/var/log/myapp
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp_prod
- POSTGRES_USER=myapp_user
- POSTGRES_PASSWORD=secure_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
cpus: '0.25'
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass redis_password
volumes:
- redis_data:/data
restart: unless-stopped
deploy:
resources:
limits:
memory: 128M
cpus: '0.1'
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
- ./logs:/var/log/nginx
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
default:
driver: bridge
8.2.3 Nginx配置
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# 基本设置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 10M;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# 上游服务器
upstream app_servers {
least_conn;
server app:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# 限流配置
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
# HTTP重定向到HTTPS
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
# HTTPS服务器
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_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# 现代SSL配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# 安全头
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status "STATIC";
# 尝试本地文件,否则代理到应用
try_files $uri @app;
}
# API路由限流
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://app_servers;
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 健康检查
location /health {
access_log off;
proxy_pass http://app_servers;
proxy_http_version 1.1;
proxy_set_header Connection '';
}
# 主应用
location / {
limit_req zone=general burst=10 nodelay;
proxy_pass http://app_servers;
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 错误页面
error_page 502 503 504 /50x.html;
}
# 错误页面
location = /50x.html {
root /usr/share/nginx/html;
}
# 代理到应用的fallback
location @app {
proxy_pass http://app_servers;
proxy_http_version 1.1;
proxy_set_header Connection '';
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;
}
}
}
8.3 Kubernetes部署
8.3.1 Kubernetes配置文件
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: vue-ssr-app
labels:
name: vue-ssr-app
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: vue-ssr-app
data:
NODE_ENV: "production"
PORT: "3000"
HOST: "0.0.0.0"
CACHE_TTL: "300"
COMPONENT_CACHE_MAX: "1000"
PAGE_CACHE_MAX: "5000"
LOG_LEVEL: "info"
CLUSTER_WORKERS: "1"
---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: vue-ssr-app
type: Opaque
data:
DB_PASSWORD: c2VjdXJlX3Bhc3N3b3Jk # base64 encoded
REDIS_PASSWORD: cmVkaXNfcGFzc3dvcmQ= # base64 encoded
JWT_SECRET: eW91cl9qd3Rfc2VjcmV0X2tleQ== # base64 encoded
SESSION_SECRET: eW91cl9zZXNzaW9uX3NlY3JldA== # base64 encoded
---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vue-ssr-app
namespace: vue-ssr-app
labels:
app: vue-ssr-app
spec:
replicas: 3
selector:
matchLabels:
app: vue-ssr-app
template:
metadata:
labels:
app: vue-ssr-app
spec:
containers:
- name: vue-ssr-app
image: your-registry/vue-ssr-app:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: app-config
env:
- name: DB_HOST
value: "postgres-service"
- name: REDIS_HOST
value: "redis-service"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: DB_PASSWORD
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: REDIS_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: JWT_SECRET
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: SESSION_SECRET
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: logs
mountPath: /var/log/myapp
volumes:
- name: logs
emptyDir: {}
imagePullSecrets:
- name: registry-secret
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: vue-ssr-service
namespace: vue-ssr-app
labels:
app: vue-ssr-app
spec:
selector:
app: vue-ssr-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vue-ssr-hpa
namespace: vue-ssr-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vue-ssr-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 60
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vue-ssr-ingress
namespace: vue-ssr-app
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
spec:
tls:
- hosts:
- yourdomain.com
- www.yourdomain.com
secretName: vue-ssr-tls
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vue-ssr-service
port:
number: 80
- host: www.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vue-ssr-service
port:
number: 80
8.3.2 数据库部署
# k8s/postgres.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: vue-ssr-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: fast-ssd
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: vue-ssr-app
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_DB
value: "myapp_prod"
- name: POSTGRES_USER
value: "myapp_user"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: DB_PASSWORD
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command:
- pg_isready
- -U
- myapp_user
- -d
- myapp_prod
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- myapp_user
- -d
- myapp_prod
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: vue-ssr-app
spec:
selector:
app: postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432
type: ClusterIP
---
# k8s/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: vue-ssr-app
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- --appendonly
- "yes"
- --requirepass
- $(REDIS_PASSWORD)
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: REDIS_PASSWORD
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
namespace: vue-ssr-app
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
8.3.3 部署脚本
#!/bin/bash
# deploy.sh
set -e
# 配置变量
REGISTRY="your-registry.com"
IMAGE_NAME="vue-ssr-app"
TAG=${1:-latest}
NAMESPACE="vue-ssr-app"
echo "开始部署 Vue SSR 应用..."
# 构建Docker镜像
echo "构建Docker镜像..."
docker build -t $REGISTRY/$IMAGE_NAME:$TAG .
# 推送镜像到仓库
echo "推送镜像到仓库..."
docker push $REGISTRY/$IMAGE_NAME:$TAG
# 创建命名空间(如果不存在)
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
# 应用Kubernetes配置
echo "应用Kubernetes配置..."
kubectl apply -f k8s/
# 更新部署镜像
echo "更新部署镜像..."
kubectl set image deployment/vue-ssr-app vue-ssr-app=$REGISTRY/$IMAGE_NAME:$TAG -n $NAMESPACE
# 等待部署完成
echo "等待部署完成..."
kubectl rollout status deployment/vue-ssr-app -n $NAMESPACE --timeout=300s
# 检查部署状态
echo "检查部署状态..."
kubectl get pods -n $NAMESPACE
kubectl get services -n $NAMESPACE
kubectl get ingress -n $NAMESPACE
echo "部署完成!"
# 显示应用URL
INGRESS_IP=$(kubectl get ingress vue-ssr-ingress -n $NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
if [ ! -z "$INGRESS_IP" ]; then
echo "应用访问地址: https://$INGRESS_IP"
else
echo "请检查Ingress配置获取访问地址"
fi
8.4 监控与告警
8.4.1 Prometheus监控配置
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: 'vue-ssr-app'
static_configs:
- targets: ['vue-ssr-service:80']
metrics_path: '/metrics'
scrape_interval: 30s
scrape_timeout: 10s
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'postgres-exporter'
static_configs:
- targets: ['postgres-exporter:9187']
- job_name: 'redis-exporter'
static_configs:
- targets: ['redis-exporter:9121']
# monitoring/alert_rules.yml
groups:
- name: vue-ssr-app
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors per second"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High response time detected"
description: "95th percentile response time is {{ $value }} seconds"
- alert: HighMemoryUsage
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage detected"
description: "Memory usage is {{ $value | humanizePercentage }}"
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage detected"
description: "CPU usage is {{ $value }}%"
- alert: DatabaseDown
expr: up{job="postgres-exporter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Database is down"
description: "PostgreSQL database is not responding"
- alert: RedisDown
expr: up{job="redis-exporter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Redis is down"
description: "Redis cache is not responding"
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Pod is crash looping"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is crash looping"
8.4.2 应用指标收集
// server/metrics.js
const promClient = require('prom-client')
// 创建指标注册表
const register = new promClient.Registry()
// 添加默认指标
promClient.collectDefaultMetrics({ register })
// 自定义指标
const httpRequestsTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'],
registers: [register]
})
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
registers: [register]
})
const ssrRenderDuration = new promClient.Histogram({
name: 'ssr_render_duration_seconds',
help: 'Duration of SSR rendering in seconds',
labelNames: ['route'],
buckets: [0.1, 0.2, 0.5, 1, 2, 5],
registers: [register]
})
const cacheHitRate = new promClient.Gauge({
name: 'cache_hit_rate',
help: 'Cache hit rate',
labelNames: ['cache_type'],
registers: [register]
})
const activeConnections = new promClient.Gauge({
name: 'active_connections',
help: 'Number of active connections',
registers: [register]
})
const memoryUsage = new promClient.Gauge({
name: 'nodejs_memory_usage_bytes',
help: 'Node.js memory usage in bytes',
labelNames: ['type'],
registers: [register]
})
// 指标收集中间件
function metricsMiddleware(req, res, next) {
const start = Date.now()
res.on('finish', () => {
const duration = (Date.now() - start) / 1000
const route = req.route ? req.route.path : req.path
const method = req.method
const status = res.statusCode.toString()
httpRequestsTotal.inc({ method, route, status })
httpRequestDuration.observe({ method, route, status }, duration)
})
next()
}
// SSR渲染指标
function recordSSRRender(route, duration) {
ssrRenderDuration.observe({ route }, duration / 1000)
}
// 缓存指标
function updateCacheMetrics(cacheType, hits, total) {
const hitRate = total > 0 ? hits / total : 0
cacheHitRate.set({ cache_type: cacheType }, hitRate)
}
// 内存使用指标
function updateMemoryMetrics() {
const memUsage = process.memoryUsage()
memoryUsage.set({ type: 'heap_used' }, memUsage.heapUsed)
memoryUsage.set({ type: 'heap_total' }, memUsage.heapTotal)
memoryUsage.set({ type: 'external' }, memUsage.external)
memoryUsage.set({ type: 'rss' }, memUsage.rss)
}
// 定期更新内存指标
setInterval(updateMemoryMetrics, 10000)
// 指标端点
function metricsEndpoint(req, res) {
res.set('Content-Type', register.contentType)
res.end(register.metrics())
}
module.exports = {
register,
metricsMiddleware,
metricsEndpoint,
recordSSRRender,
updateCacheMetrics,
activeConnections
}
8.4.3 Grafana仪表板
{
"dashboard": {
"id": null,
"title": "Vue SSR Application Dashboard",
"tags": ["vue", "ssr", "nodejs"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{route}}"
}
],
"yAxes": [
{
"label": "Requests/sec"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
}
},
{
"id": 2,
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
},
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile"
}
],
"yAxes": [
{
"label": "Seconds"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
}
},
{
"id": 3,
"title": "Error Rate",
"type": "singlestat",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[5m]) / rate(http_requests_total[5m])",
"legendFormat": "Error Rate"
}
],
"valueName": "current",
"format": "percentunit",
"thresholds": "0.01,0.05",
"colorBackground": true,
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 8
}
},
{
"id": 4,
"title": "Cache Hit Rate",
"type": "singlestat",
"targets": [
{
"expr": "cache_hit_rate",
"legendFormat": "{{cache_type}}"
}
],
"valueName": "current",
"format": "percentunit",
"thresholds": "0.7,0.9",
"colorBackground": true,
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 8
}
},
{
"id": 5,
"title": "Memory Usage",
"type": "graph",
"targets": [
{
"expr": "nodejs_memory_usage_bytes{type=\"heap_used\"}",
"legendFormat": "Heap Used"
},
{
"expr": "nodejs_memory_usage_bytes{type=\"heap_total\"}",
"legendFormat": "Heap Total"
}
],
"yAxes": [
{
"label": "Bytes",
"logBase": 1
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 12
}
},
{
"id": 6,
"title": "SSR Render Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(ssr_render_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
},
{
"expr": "histogram_quantile(0.50, rate(ssr_render_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile"
}
],
"yAxes": [
{
"label": "Seconds"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 12
}
}
],
"time": {
"from": "now-1h",
"to": "now"
},
"refresh": "30s"
}
}
8.5 日志管理
8.5.1 结构化日志配置
”`javascript // utils/logger.js const winston = require(‘winston’) const path = require(‘path’)
// 自定义日志格式 const logFormat = winston.format.combine( winston.format.timestamp({ format: ‘YYYY-MM-DD HH:mm:ss’ }), winston.format.errors({ stack: true }), winston.format.json(), winston.format.printf(({ timestamp, level, message, …meta }) => { return JSON.stringify({ timestamp, level, message, …meta, service: ‘vue-ssr-app’, version: process.env.APP_VERSION || ‘1.0.0’, environment: process.env.NODE_ENV || ‘development’, pid: process.pid, hostname: require(‘os’).hostname() }) }) )
// 创建logger实例 const logger = winston.createLogger({ level: process.env.LOG_LEVEL || ‘info’, format: logFormat, defaultMeta: { service: ‘vue-ssr-app’ }, transports: [ // 控制台输出 new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) }),
// 文件输出
new winston.transports.File({
filename: path.join(process.env.LOG_DIR || './logs', 'error.log'),
level: 'error',
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.File({
filename: path.join(process.env.LOG_DIR || './logs', 'combined.log'),
maxsize: 10485760, // 10MB
maxFiles: 10
})
],
// 异常处理 exceptionHandlers: [ new winston.transports.File({ filename: path.join(process.env.LOG_DIR || ‘./logs’, ‘exceptions.log’) }) ],
// 拒绝处理 rejectionHandlers: [ new winston.transports.File({ filename: path.join(process.env.LOG_DIR || ‘./logs’, ‘rejections.log’) }) ] })
// 请求日志