引言:题库服务器部署的核心挑战
在当今数字化教育和在线测评环境中,将题库部署在服务器上已经成为标准做法。然而,这种部署方式面临着三大核心挑战:数据安全、访问流畅性和突发故障处理。题库作为教育机构的核心资产,不仅包含知识产权,还涉及用户隐私数据,一旦泄露或损坏,后果不堪设想。同时,用户期望在任何时间、任何地点都能快速访问题库,这要求系统具备高可用性和高性能。最后,硬件故障、网络攻击、软件bug等突发情况随时可能发生,如何快速恢复服务并最小化数据损失是必须解决的问题。
本文将从这三个维度出发,提供一套完整的解决方案,涵盖技术架构、实施策略和最佳实践,帮助您构建一个安全、高效、可靠的题库服务系统。
一、数据安全:构建多层防御体系
1.1 数据加密:保护静态和传输中的数据
数据加密是保护题库安全的第一道防线。我们需要在两个层面实施加密:静态数据加密(Data at Rest)和传输中数据加密(Data in Transit)。
静态数据加密
静态数据加密保护存储在磁盘上的数据。对于题库数据库,我们应该:
- 使用透明数据加密(TDE)技术
- 对敏感字段(如题目答案、用户信息)进行列级加密
- 定期轮换加密密钥
MySQL数据库TDE配置示例:
-- 启用MySQL的TDE功能(需要InnoDB引擎)
-- 1. 安装加密插件(MySQL企业版或Percona版本)
INSTALL PLUGIN keyring_file SONAME 'keyring_file.so';
-- 2. 配置my.cnf
/*
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
*/
-- 3. 创建加密表空间
ALTER TABLE questions ENCRYPTION='Y';
ALTER TABLE users ENCRYPTION='Y';
-- 4. 验证加密状态
SELECT TABLE_NAME, CREATE_OPTIONS
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'question_bank'
AND CREATE_OPTIONS LIKE '%ENCRYPTION="Y"%';
传输中数据加密
确保所有数据传输都通过加密通道进行:
- 强制使用HTTPS/TLS 1.2+协议
- 数据库连接使用SSL/TLS加密
- API通信使用JWT或OAuth 2.0认证
Node.js中配置HTTPS和数据库SSL连接:
const https = require('https');
const fs = require('fs');
const mysql = require('mysql2');
// 1. 配置HTTPS服务器
const options = {
key: fs.readFileSync('/path/to/private.key'),
cert: fs.readFileSync('/path/to/certificate.crt'),
minVersion: 'TLSv1.2', // 强制使用TLS 1.2+
ciphers: [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'AES128-GCM-SHA256',
'AES256-GCM-SHA384'
].join(':')
};
// 2. 创建HTTPS服务器
const server = https.createServer(options, (req, res) => {
// 处理请求
});
// 3. 数据库连接使用SSL
const pool = mysql.createPool({
host: 'db.example.com',
user: 'app_user',
database: 'question_bank',
password: 'secure_password',
ssl: {
ca: fs.readFileSync('/path/to/ca.pem'),
rejectUnauthorized: true
},
connectionLimit: 10
});
1.2 访问控制:最小权限原则
严格的访问控制是防止未授权访问的关键。我们采用RBAC(基于角色的访问控制)模型,结合多因素认证(MFA)。
RBAC模型设计
-- 创建角色表
CREATE TABLE roles (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255)
);
-- 创建权限表
CREATE TABLE permissions (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255)
);
-- 角色-权限关联表
CREATE TABLE role_permissions (
role_id INT,
permission_id INT,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
-- 用户-角色关联表
CREATE TABLE user_roles (
user_id INT,
role_id INT,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
-- 插入基础角色和权限
INSERT INTO roles (name, description) VALUES
('admin', '系统管理员'),
('teacher', '教师'),
('student', '学生');
INSERT INTO permissions (name, description) VALUES
('question:read', '读取题目'),
('question:write', '创建/修改题目'),
('question:delete', '删除题目'),
('user:manage', '管理用户');
-- 分配权限
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE (r.name = 'admin' AND p.name IN ('question:read', 'question:write', 'question:delete', 'user:manage'))
OR (r.name = 'teacher' AND p.name IN ('question:read', 'question:write'))
OR (r.name = 'student' AND p.name = 'question:read');
多因素认证实现
// 使用speakeasy库实现TOTP(基于时间的一次性密码)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// 1. 为用户生成MFA密钥
function generateMFASetup(userId) {
const secret = speakeasy.generateSecret({
name: `题库系统(${userId})`
});
// 保存加密后的密钥到数据库
const encryptedSecret = encrypt(secret.base32); // 使用AES加密
saveToDatabase(userId, encryptedSecret);
// 生成二维码供用户扫描
QRCode.toDataURL(secret.otpauth_url, (err, data_url) => {
console.log('扫描二维码:', data_url);
});
return { secret: secret.base32 };
}
// 2. 验证MFA代码
function verifyMFA(userId, token) {
const encryptedSecret = getFromDatabase(userId);
const secret = decrypt(encryptedSecret);
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 1 // 允许1个时间窗口的偏差
});
}
// 3. Express中间件:保护路由
function requireMFA(req, res, next) {
if (req.session.mfaVerified) {
return next();
}
const token = req.headers['x-mfa-token'];
if (!token) {
return res.status(403).json({ error: 'MFA token required' });
}
if (verifyMFA(req.user.id, token)) {
req.session.mfaVerified = true;
next();
} else {
res.status(401).json({ error: 'Invalid MFA token' });
}
}
// 4. 保护敏感路由
app.put('/api/questions/:id', requireMFA, (req, res) => {
// 只有通过MFA验证的用户才能修改题目
});
1.3 审计与监控:记录所有操作
完整的审计日志是事后追溯和合规性要求的关键。我们需要记录:
- 谁(用户ID)在什么时间(时间戳)访问了什么资源(API端点)
- 操作类型(查询、创建、修改、删除)
- 操作结果(成功/失败)
- 客户端信息(IP地址、User-Agent)
审计日志表设计:
CREATE TABLE audit_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
action VARCHAR(50) NOT NULL,
resource_type VARCHAR(50),
resource_id VARCHAR(100),
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
user_agent TEXT,
result ENUM('success', 'failure') NOT NULL,
details JSON,
INDEX idx_user_timestamp (user_id, timestamp),
INDEX idx_timestamp (timestamp)
);
自动记录审计日志的中间件:
const auditLog = (action, resourceType) => {
return async (req, res, next) => {
const start = Date.now();
// 监听响应完成事件
res.on('finish', async () => {
const duration = Date.now() - start;
const logEntry = {
user_id: req.user?.id || null,
action: action,
resource_type: resourceType,
resource_id: req.params.id || req.body.id,
ip_address: req.ip,
user_agent: req.get('User-Agent'),
result: res.statusCode < 400 ? 'success' : 'failure',
details: {
method: req.method,
path: req.path,
status_code: res.statusCode,
duration_ms: duration,
request_body: req.body,
query_params: req.query
}
};
// 异步写入日志,不阻塞主请求
await db.query('INSERT INTO audit_logs SET ?', logEntry);
});
next();
};
};
// 使用示例
app.post('/api/questions',
authenticate,
auditLog('create', 'question'),
async (req, res) => {
// 创建题目逻辑
}
);
1.4 数据备份与恢复:最后的防线
即使有最好的防护措施,数据丢失的风险仍然存在。因此,必须实施3-2-1备份策略:
- 3份数据副本
- 2种不同的存储介质
- 1份异地备份
自动化备份脚本(Bash + MySQL):
#!/bin/bash
# backup_question_bank.sh
# 配置
DB_HOST="db.example.com"
DB_USER="backup_user"
DB_PASS="secure_backup_password"
DB_NAME="question_bank"
BACKUP_DIR="/backup/mysql"
S3_BUCKET="s3://question-bank-backups"
RETENTION_DAYS=30
# 创建备份目录
mkdir -p $BACKUP_DIR
# 生成时间戳
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$TIMESTAMP.sql.gz"
# 执行备份(使用mysqldump)
echo "开始备份数据库: $DB_NAME"
mysqldump -h $DB_HOST -u $DB_USER -p$DB_PASS \
--single-transaction \
--routines \
--triggers \
--events \
$DB_NAME | gzip > $BACKUP_FILE
# 检查备份是否成功
if [ $? -eq 0 ]; then
echo "备份成功: $BACKUP_FILE"
# 上传到S3(需要配置AWS CLI)
aws s3 cp $BACKUP_FILE $S3_BUCKET/
# 本地保留最近3份
ls -t $BACKUP_DIR/*.sql.gz | tail -n +4 | xargs rm -f
# 清理S3旧备份
aws s3 ls $S3_BUCKET/ | awk '{print $4}' | while read file; do
# 检查文件日期,删除超过保留期的
aws s3 rm $S3_BUCKET/$file
done
else
echo "备份失败!"
# 发送告警通知
curl -X POST https://hooks.slack.com/services/... \
-d '{"text":"数据库备份失败!"}'
fi
恢复脚本示例:
#!/bin/bash
# restore_question_bank.sh
BACKUP_FILE=$1
DB_NAME="question_bank_restore"
if [ -z "$BACKUP_FILE" ]; then
echo "用法: ./restore_question_bank.sh <backup_file.sql.gz>"
exit 1
fi
echo "从 $BACKUP_FILE 恢复数据库..."
# 解压
gunzip -c $BACKUP_FILE > /tmp/restore.sql
# 恢复到临时数据库(避免覆盖生产数据)
mysql -h db.example.com -u root -p \
-e "CREATE DATABASE IF NOT EXISTS $DB_NAME;"
mysql -h db.example.com -u root -p $DB_NAME < /tmp/restore.sql
echo "恢复完成。请验证数据后手动切换到生产环境。"
二、访问流畅:优化性能与可用性
2.1 数据库优化:从查询到索引
数据库是题库系统的核心,优化数据库性能是确保访问流畅的关键。
索引策略
-- 题目表结构示例
CREATE TABLE questions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
subject_id INT NOT NULL, -- 学科ID
grade_level INT NOT NULL, -- 年级级别
question_type ENUM('single', 'multiple', 'essay') NOT NULL,
difficulty TINYINT NOT NULL, -- 难度1-5
content TEXT NOT NULL, -- 题目内容
options JSON, -- 选项(JSON格式)
answer TEXT, -- 答案
tags JSON, -- 标签数组
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
created_by INT NOT NULL, -- 创建者
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 创建复合索引(根据查询模式)
-- 1. 按学科、年级、难度查询(最常用)
CREATE INDEX idx_subject_grade_diff ON questions(subject_id, grade_level, difficulty);
-- 2. 按标签查询(使用JSON索引,MySQL 8.0+)
CREATE INDEX idx_tags ON questions((CAST(tags AS CHAR(32) ARRAY)));
-- 3. 按创建者和状态查询
CREATE INDEX idx_creator_status ON questions(created_by, status);
-- 4. 全文搜索索引(用于内容搜索)
CREATE FULLTEXT INDEX idx_content_search ON questions(content);
-- 查看查询计划
EXPLAIN SELECT * FROM questions
WHERE subject_id = 101
AND grade_level = 8
AND difficulty BETWEEN 3 AND 5;
查询优化技巧
// 使用连接池减少连接开销
const pool = mysql.createPool({
host: 'db.example.com',
user: 'app_user',
database: 'question_bank',
password: 'secure_password',
connectionLimit: 20, // 根据并发量调整
queueLimit: 100, // 队列上限,防止连接耗尽
acquireTimeout: 60000, // 获取连接超时
timeout: 60000 // 查询超时
});
// 使用预处理语句防止SQL注入并提升性能
async function getQuestionsByFilter(filters) {
const { subjectId, gradeLevel, difficulty, limit = 20, offset = 0 } = filters;
// 构建动态查询
let whereClause = [];
let params = [];
if (subjectId) {
whereClause.push('subject_id = ?');
params.push(subjectId);
}
if (gradeLevel) {
whereClause.push('grade_level = ?');
params.push(gradeLevel);
}
if (difficulty) {
whereClause.push('difficulty = ?');
params.push(difficulty);
}
// 添加分页参数
params.push(parseInt(limit), parseInt(offset));
const sql = `
SELECT id, content, difficulty, tags
FROM questions
${whereClause.length ? 'WHERE ' + whereClause.join(' AND ') : ''}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
// 使用连接池执行
const [rows] = await pool.query(sql, params);
return rows;
}
// 使用Redis缓存热点数据
const redis = require('redis');
const client = redis.createClient({ url: 'redis://cache.example.com:6379' });
async function getQuestionWithCache(questionId) {
const cacheKey = `question:${questionId}`;
// 尝试从缓存获取
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 缓存未命中,查询数据库
const [rows] = await pool.query('SELECT * FROM questions WHERE id = ?', [questionId]);
const question = rows[0];
if (question) {
// 写入缓存,设置TTL为5分钟
await client.setex(cacheKey, 300, JSON.stringify(question));
}
return question;
}
2.2 缓存策略:减少数据库压力
缓存是提升访问流畅性的利器,但需要合理设计策略。
多级缓存架构
// 内存缓存(Node.js进程内)
const NodeCache = require('node-cache');
const localCache = new NodeCache({ stdTTL: 60 }); // 1分钟
// Redis集群配置
const redis = require('redis');
const redisClient = redis.createCluster({
rootNodes: [
{ url: 'redis://node1:6379' },
{ url: 'redis://node2:6379' },
{ url: 'redis://node3:6379' }
]
});
// 缓存装饰器模式
function cacheable(ttl = 300) {
return function(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
// 1. 检查本地缓存
const local = localCache.get(cacheKey);
if (local !== undefined) {
return local;
}
// 2. 检查Redis缓存
const redisData = await redisClient.get(cacheKey);
if (redisData) {
const result = JSON.parse(redisData);
localCache.set(cacheKey, result, 60); // 回填本地缓存
return result;
}
// 3. 执行原始方法
const result = await originalMethod.apply(this, args);
// 4. 写入缓存
if (result !== null && result !== undefined) {
await redisClient.setex(cacheKey, ttl, JSON.stringify(result));
localCache.set(cacheKey, result, 60);
}
return result;
};
return descriptor;
};
}
// 使用示例
class QuestionService {
@cacheable(600) // 缓存10分钟
async getQuestionById(id) {
const [rows] = await pool.query('SELECT * FROM questions WHERE id = ?', [id]);
return rows[0];
}
}
缓存更新策略(Cache-Aside + Write-Through)
// 写操作时同步更新缓存
async function updateQuestion(questionId, updates) {
// 1. 更新数据库
const [result] = await pool.query(
'UPDATE questions SET ? WHERE id = ?',
[updates, questionId]
);
if (result.affectedRows > 0) {
// 2. 使相关缓存失效
const cacheKeys = [
`question:${questionId}`,
`questions:subject:${updates.subject_id}`,
`questions:grade:${updates.grade_level}`
];
// 删除缓存(不等待,异步)
redisClient.del(cacheKeys).catch(err => {
console.error('缓存删除失败:', err);
});
// 3. 可选:立即预热新缓存
const [rows] = await pool.query('SELECT * FROM questions WHERE id = ?', [questionId]);
if (rows[0]) {
redisClient.setex(`question:${questionId}`, 600, JSON.stringify(rows[0]));
}
}
return result;
}
2.3 负载均衡与CDN:分发与加速
对于高并发访问,需要使用负载均衡器和CDN来分发流量。
Nginx负载均衡配置
# /etc/nginx/nginx.conf
# 定义上游服务器组
upstream question_api {
# 负载均衡策略:加权轮询
server api1.example.com:3000 weight=3;
server api2.example.com:3000 weight=3;
server api3.example.com:3000 weight=2;
# 健康检查
check interval=3000 rise=2 fall=3 timeout=1000 type=http;
check_http_send "GET /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
# 会话保持(同一IP分配到同一服务器)
ip_hash;
}
# HTTPS服务器配置
server {
listen 443 ssl http2;
server_name api.questionbank.com;
# SSL证书
ssl_certificate /etc/ssl/certs/questionbank.crt;
ssl_certificate_key /etc/ssl/private/questionbank.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 限流:防止DDoS
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req zone=api_limit burst=20 nodelay;
# 限速:限制单个IP
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10;
location /api/ {
proxy_pass http://question_api;
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# 启用gzip压缩
gzip on;
gzip_types application/json text/plain;
}
# 健康检查端点
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
CDN配置(以Cloudflare为例)
// 边缘计算:在CDN边缘节点缓存静态资源
// Cloudflare Workers示例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// 静态资源缓存
if (url.pathname.startsWith('/static/')) {
// 设置CDN缓存策略
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('Cache-Control', 'public, max-age=31536000'); // 1年
return newResponse;
}
// API响应缓存(边缘缓存)
if (url.pathname.startsWith('/api/questions/')) {
const cache = caches.default;
const cacheKey = new Request(url.toString(), request);
// 检查缓存
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// 回源请求
response = await fetch(request);
// 缓存响应(仅GET请求,状态码200)
if (request.method === 'GET' && response.status === 200) {
const cacheResponse = response.clone();
cacheResponse.headers.set('Cache-Control', 'public, max-age=300'); // 5分钟
event.waitUntil(cache.put(cacheKey, cacheResponse));
}
return response;
}
return fetch(request);
}
2.4 异步处理与消息队列:解耦耗时操作
对于发送邮件、生成报表等耗时操作,使用消息队列异步处理,避免阻塞主请求。
RabbitMQ配置与使用
const amqp = require('amqplib');
// 1. 连接RabbitMQ
async function getChannel() {
const connection = await amqp.connect('amqp://user:pass@rabbitmq.example.com');
const channel = await connection.createChannel();
// 声明队列
await channel.assertQueue('question_export', {
durable: true, // 队列持久化
deadLetterExchange: 'dlx', // 死信队列
deadLetterRoutingKey: 'question_export_dlx'
});
return channel;
}
// 2. 生产者:发送导出任务
async function enqueueExportTask(userId, filters) {
const channel = await getChannel();
const task = {
userId,
filters,
timestamp: Date.now(),
retryCount: 0
};
channel.sendToQueue(
'question_export',
Buffer.from(JSON.stringify(task)),
{ persistent: true } // 消息持久化
);
console.log('导出任务已加入队列');
}
// 3. 消费者:处理导出任务
async function consumeExportTasks() {
const channel = await getChannel();
channel.consume('question_export', async (msg) => {
try {
const task = JSON.parse(msg.content.toString());
// 执行导出逻辑
await generateExportFile(task.userId, task.filters);
// 发送通知
await sendNotification(task.userId, '导出完成');
// 确认消息
channel.ack(msg);
} catch (error) {
console.error('处理失败:', error);
// 检查重试次数
if (task.retryCount < 3) {
task.retryCount++;
// 重新入队
channel.sendToQueue('question_export', Buffer.from(JSON.stringify(task)));
channel.ack(msg);
} else {
// 超过重试次数,发送到死信队列
channel.reject(msg, false);
}
}
});
}
// 4. 死信队列处理
async function setupDeadLetterQueue() {
const channel = await getChannel();
// 声明死信交换机
await channel.assertExchange('dlx', 'direct');
// 声明死信队列
await channel.assertQueue('question_export_dlx', {
durable: true
});
// 绑定
await channel.bindQueue('question_export_dlx', 'dlx', 'question_export_dlx');
// 监控死信
channel.consume('question_export_dlx', (msg) => {
console.error('永久失败的任务:', msg.content.toString());
// 发送告警
sendAlert('导出任务失败', msg.content.toString());
});
}
三、突发故障处理:构建弹性系统
3.1 故障检测与健康检查
及时发现故障是快速恢复的前提。我们需要实现多层次的健康检查。
应用层健康检查
// Express健康检查端点
app.get('/health', async (req, res) => {
const checks = {
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version
};
// 检查数据库连接
try {
await pool.query('SELECT 1');
checks.database = 'healthy';
} catch (e) {
checks.database = 'unhealthy';
}
// 检查Redis连接
try {
await redisClient.ping();
checks.redis = 'healthy';
} catch (e) {
checks.redis = 'unhealthy';
}
// 检查外部API(如果有)
try {
const response = await fetch('https://api.partner.com/health');
checks.partner_api = response.ok ? 'healthy' : 'unhealthy';
} catch (e) {
checks.partner_api = 'unhealthy';
}
// 综合状态
const allHealthy = Object.values(checks).every(v => v === 'healthy');
const status = allHealthy ? 200 : 503;
res.status(status).json(checks);
});
// 深度健康检查(用于内部监控)
app.get('/health/deep', async (req, res) => {
const checks = {};
// 1. 数据库查询性能
const start = Date.now();
await pool.query('SELECT COUNT(*) as count FROM questions');
checks.dbQueryTime = Date.now() - start;
// 2. Redis读写测试
const testKey = 'health:test';
await redisClient.set(testKey, 'ok', 'EX', 10);
const redisValue = await redisClient.get(testKey);
checks.redisReadWrite = redisValue === 'ok';
// 3. 磁盘空间检查
const df = require('diskusage');
const disk = df.checkSync('/');
checks.diskSpace = disk.available > 1024 * 1024 * 1024; // >1GB
// 4. 内存使用
const used = process.memoryUsage();
checks.memoryUsage = {
rss: Math.round(used.rss / 1024 / 1024) + 'MB',
heapUsed: Math.round(used.heapUsed / 1024 / 1024) + 'MB'
};
const allHealthy = checks.dbQueryTime < 1000 &&
checks.redisReadWrite &&
checks.diskSpace;
res.status(allHealthy ? 200 : 503).json(checks);
});
外部监控(Prometheus + Grafana)
# prometheus.yml 配置
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'question-bank-api'
static_configs:
- targets: ['api1.example.com:9090', 'api2.example.com:9090']
metrics_path: '/metrics'
scrape_interval: 5s
- job_name: 'mysql'
static_configs:
- targets: ['db.example.com:9104']
- job_name: 'redis'
static_configs:
- targets: ['cache.example.com:9121']
// 在应用中暴露Prometheus指标
const client = require('prom-client');
// 创建指标
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'path', 'status']
});
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'path'],
buckets: [0.1, 0.5, 1, 2, 5]
});
// Express中间件
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
httpRequestsTotal.inc({
method: req.method,
path: req.route?.path || req.path,
status: res.statusCode
});
end();
});
next();
});
// 暴露指标端点
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
3.2 自动故障转移与恢复
数据库主从复制与故障转移
-- MySQL主从复制配置(主库)
-- my.cnf 配置
[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7
-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'secure_repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 查看主库状态
SHOW MASTER STATUS;
-- 从库配置
[mysqld]
server-id = 2
relay_log = mysql-relay-bin
log_bin = mysql-bin
read_only = 1
-- 配置复制
CHANGE MASTER TO
MASTER_HOST='db-primary.example.com',
MASTER_USER='repl',
MASTER_PASSWORD='secure_repl_password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=123456;
START SLAVE;
SHOW SLAVE STATUS\G
自动故障转移脚本
#!/usr/bin/env python3
# db_failover.py
import mysql.connector
import time
import sys
PRIMARY_HOST = 'db-primary.example.com'
REPLICA_HOST = 'db-replica.example.com'
MONITOR_INTERVAL = 10
def check_primary_health():
"""检查主库健康状态"""
try:
conn = mysql.connector.connect(
host=PRIMARY_HOST,
user='monitor',
password='monitor_pass',
connect_timeout=5
)
cursor = conn.cursor()
cursor.execute("SELECT 1")
cursor.close()
conn.close()
return True
except Exception as e:
print(f"主库连接失败: {e}")
return False
def promote_replica():
"""提升从库为主库"""
try:
# 连接到从库
conn = mysql.connector.connect(
host=REPLICA_HOST,
user='root',
password='root_pass'
)
cursor = conn.cursor()
# 停止从库复制
cursor.execute("STOP SLAVE")
# 重置复制信息
cursor.execute("RESET SLAVE ALL")
# 检查是否需要执行未完成的事务
cursor.execute("SHOW SLAVE STATUS")
slave_status = cursor.fetchone()
# 提升为主库
cursor.execute("SET GLOBAL read_only = 0")
print("从库已提升为主库")
cursor.close()
conn.close()
return True
except Exception as e:
print(f"提升失败: {e}")
return False
def update_dns_or_vip():
"""更新DNS或VIP指向新主库"""
# 这里可以调用云服务商API更新DNS记录
# 或者使用keepalived切换VIP
print("更新DNS/VIP指向新主库")
pass
def main():
print("开始监控数据库健康...")
while True:
if not check_primary_health():
print("主库故障,开始故障转移...")
# 等待几秒确保故障不是临时的
time.sleep(30)
if not check_primary_health():
if promote_replica():
update_dns_or_vip()
print("故障转移完成")
sys.exit(0)
else:
print("故障转移失败,需要人工干预")
sys.exit(1)
else:
print("主库恢复,无需转移")
time.sleep(MONITOR_INTERVAL)
if __name__ == '__main__':
main()
应用层重试与熔断
// 使用circuit-breaker模式
const CircuitBreaker = require('opossum');
// 为数据库操作创建熔断器
const dbBreaker = new CircuitBreaker(
async (query, params) => {
return await pool.query(query, params);
},
{
timeout: 3000, // 超时时间
errorThresholdPercentage: 50, // 错误率阈值
resetTimeout: 30000, // 半开状态时间
volumeThreshold: 10 // 最小请求数
}
);
// 熔断器事件监听
dbBreaker.on('open', () => {
console.error('数据库熔断器打开,服务降级');
// 发送告警
sendAlert('数据库熔断', '数据库服务不可用');
});
dbBreaker.on('halfOpen', () => {
console.log('数据库熔断器半开,尝试恢复');
});
dbBreaker.on('close', () => {
console.log('数据库熔断器关闭,服务恢复');
});
// 使用熔断器执行查询
async function getQuestionsSafe(filters) {
try {
const result = await dbBreaker.fire(
'SELECT * FROM questions WHERE subject_id = ? AND grade_level = ?',
[filters.subjectId, filters.gradeLevel]
);
return result;
} catch (error) {
if (dbBreaker.opened) {
// 熔断状态,返回降级数据
return getFallbackData();
}
throw error;
}
}
// 降级数据(缓存的热门题目)
function getFallbackData() {
return [
{ id: 1, content: '备用题目1', difficulty: 3 },
{ id: 2, content: '备用题目2', difficulty: 2 }
];
}
3.3 数据一致性与事务管理
在分布式系统中,确保数据一致性至关重要。
分布式事务(Saga模式)
// 使用Saga模式处理跨服务事务
// 例如:创建题目 + 更新统计 + 发送通知
class QuestionSaga {
constructor() {
this.steps = [];
}
// 步骤1:创建题目
async createQuestion(questionData) {
const step = {
name: 'createQuestion',
action: async () => {
const [result] = await pool.query(
'INSERT INTO questions SET ?',
[questionData]
);
return result.insertId;
},
compensate: async (questionId) => {
await pool.query('DELETE FROM questions WHERE id = ?', [questionId]);
}
};
this.steps.push(step);
return this;
}
// 步骤2:更新统计
async updateStats(questionId, subjectId) {
const step = {
name: 'updateStats',
action: async () => {
await pool.query(
'UPDATE subject_stats SET question_count = question_count + 1 WHERE subject_id = ?',
[subjectId]
);
},
compensate: async () => {
await pool.query(
'UPDATE subject_stats SET question_count = question_count - 1 WHERE subject_id = ?',
[subjectId]
);
}
};
this.steps.push(step);
return this;
}
// 步骤3:发送通知
async sendNotification(userId, questionId) {
const step = {
name: 'sendNotification',
action: async () => {
// 调用通知服务
await fetch('https://notification.example.com/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId,
message: `题目 ${questionId} 已创建`
})
});
},
compensate: async () => {
// 通知无法撤销,但可以发送纠正消息
console.log(`补偿:发送纠正通知给用户 ${userId}`);
}
};
this.steps.push(step);
return this;
}
// 执行Saga
async execute() {
const results = [];
try {
for (const step of this.steps) {
const result = await step.action();
results.push({ name: step.name, result, success: true });
}
return { success: true, results };
} catch (error) {
// 执行补偿
console.error('Saga执行失败,开始补偿:', error);
for (let i = results.length - 1; i >= 0; i--) {
const step = this.steps[i];
if (results[i].success) {
try {
await step.compensate(results[i].result);
console.log(`补偿成功: ${step.name}`);
} catch (compError) {
console.error(`补偿失败: ${step.name}`, compError);
// 记录到补偿日志表,人工处理
await logCompensationFailure(step.name, results[i].result, compError);
}
}
}
return { success: false, error: error.message };
}
}
}
// 使用示例
const saga = new QuestionSaga()
.createQuestion({ content: '测试题目', subject_id: 101 })
.updateStats(/* 参数 */)
.sendNotification(123, 456);
const result = await saga.execute();
if (!result.success) {
// 处理失败
}
3.4 灾难恢复计划
灾难恢复脚本
#!/bin/bash
# disaster_recovery.sh
# 配置
BACKUP_S3_BUCKET="s3://question-bank-backups"
RECOVERY_DIR="/recovery"
DB_HOST="db-recovery.example.com"
# 步骤1:从S3拉取最新备份
echo "从S3拉取备份..."
aws s3 ls $BACKUP_S3_BUCKET/ | tail -1 | awk '{print $4}' > latest_backup.txt
LATEST_BACKUP=$(cat latest_backup.txt)
aws s3 cp $BACKUP_S3_BUCKET/$LATEST_BACKUP $RECOVERY_DIR/
# 步骤2:恢复数据库
echo "恢复数据库..."
gunzip -c $RECOVERY_DIR/$LATEST_BACKUP > $RECOVERY_DIR/restore.sql
mysql -h $DB_HOST -u root -p < $RECOVERY_DIR/restore.sql
# 步骤3:验证数据完整性
echo "验证数据..."
mysql -h $DB_HOST -u root -p -e "SELECT COUNT(*) FROM questions;" question_bank
# 步骤4:切换应用到恢复数据库
echo "更新应用配置..."
sed -i "s/db-primary.example.com/db-recovery.example.com/g" /app/config/database.js
# 步骤5:重启应用
echo "重启应用..."
systemctl restart question-bank-app
echo "灾难恢复完成"
灾难恢复演练计划
# 灾难恢复演练计划
## 演练频率
- 桌面推演:每季度一次
- 实际演练:每半年一次
## 演练场景
1. 主数据库完全宕机
2. 数据中心断电
3. 勒索软件攻击
4. 人为误删除数据
## 演练步骤
1. 模拟故障发生
2. 执行灾难恢复脚本
3. 验证数据完整性
4. 测试应用功能
5. 记录演练时间和问题
6. 优化恢复流程
## 成功标准
- RTO(恢复时间目标)< 1小时
- RPO(恢复点目标)< 15分钟
- 数据完整性100%
四、综合实施建议
4.1 分阶段实施路线图
第一阶段:基础安全与备份(1-2周)
- 配置数据库TDE加密
- 实施RBAC访问控制
- 设置自动化备份策略
- 配置HTTPS和数据库SSL
第二阶段:性能优化(2-3周)
- 分析查询日志,创建索引
- 部署Redis缓存
- 配置Nginx负载均衡
- 实现异步任务队列
第三阶段:高可用与监控(2-3周)
- 配置数据库主从复制
- 部署健康检查端点
- 集成Prometheus + Grafana
- 实现熔断器和重试机制
第四阶段:自动化与演练(1-2周)
- 编写故障转移脚本
- 实现Saga事务模式
- 执行灾难恢复演练
- 完善文档和应急预案
4.2 成本与效益分析
| 方案 | 成本 | 效益 | 优先级 |
|---|---|---|---|
| 数据库加密 | 低 | 高(合规性) | 高 |
| 自动化备份 | 低 | 极高(数据安全) | 高 |
| Redis缓存 | 中 | 高(性能提升) | 高 |
| 主从复制 | 中 | 高(可用性) | 中 |
| 负载均衡 | 中 | 中(扩展性) | 中 |
| 多因素认证 | 低 | 高(安全性) | 高 |
| 灾难恢复演练 | 低 | 极高(风险降低) | 高 |
4.3 关键指标监控(KPI)
- 可用性:> 99.9%
- 平均响应时间:< 200ms
- P95响应时间:< 500ms
- 数据丢失:0(RPO=0)
- 恢复时间:< 1小时(RTO)
- 安全事件:0
4.4 团队职责分工
- 开发团队:实现应用层安全、性能优化、健康检查
- 运维团队:配置基础设施、备份策略、监控告警
- 安全团队:安全审计、渗透测试、合规性检查
- DBA团队:数据库优化、复制配置、备份验证
结论
将题库部署在服务器上并确保其安全、流畅和可靠,需要系统性的方法和持续的努力。通过实施多层安全防御(加密、访问控制、审计)、性能优化策略(索引、缓存、负载均衡)和弹性架构设计(故障检测、自动恢复、灾难恢复),您可以构建一个企业级的题库服务系统。
记住,没有一劳永逸的解决方案。技术架构需要随着业务增长和威胁演变而持续优化。定期进行安全审计、性能测试和灾难恢复演练,是保持系统健康的关键。投资于自动化和监控工具,虽然初期成本较高,但长期来看将大幅降低运维成本和风险。
最后,文档和培训同样重要。确保团队成员都了解系统的架构、应急预案和最佳实践。在真正的危机时刻,清晰的流程和训练有素的团队比任何技术工具都更可靠。
