第二十二章 性能优化与调优
作者
谭策 — 独立开发者 | AIOps 领域探索者
- 🌐 项目官网:ITOpsAgentinfo
- 📝 博客:zjzwfw.cloud
- 📧 邮箱:huawei_network@foxmail.com
- 💬 微信公众号:IT Online

许可证
MPL-2.0 © 谭策
本章导读
性能是用户体验的系统性保障。ITOps Agent Platform 在数据库查询、前端缓存、状态管理、WebSocket 连接、构建优化等多个层面进行了系统性性能优化。本章将从后端数据库到前端渲染,从连接管理到构建产物,全方位解析项目中的性能优化策略和调优方法。
学习目标
- 掌握 SQLite 性能优化配置(WAL 模式、忙等待超时、内存映射)
- 理解 React Query 缓存策略(staleTime、gcTime、invalidation)
- 掌握 Zustand Store 优化技巧
- 理解 WebSocket 连接管理与资源清理机制
- 掌握前端构建优化(Vite 配置、代码分割)
- 学会配置 Docker 资源限制和健康检查
- 能够系统性诊断和解决性能问题
核心内容
22.1 数据库查询优化
项目使用 better-sqlite3(同步 SQLite 驱动),通过一系列 PRAGMA 指令优化数据库性能。
SQLite 性能配置详解:
// backend/src/models/database.ts
const db = new Database(DB_PATH);
// 1. WAL 模式 - 写入不阻塞读取
db.pragma('journal_mode = WAL');
// 2. 外键约束 - 保证数据一致性
db.pragma('foreign_keys = ON');
// 3. 忙等待超时 - 锁竞争时等待 5 秒而非立即失败
db.pragma('busy_timeout = 5000');
// 4. 同步模式 - FULL 确保崩溃时事务不丢失
db.pragma('synchronous = FULL');
// 5. 临时表存储 - 使用内存提升排序性能
db.pragma('temp_store = MEMORY');
// 6. 内存映射 - 1GB 直接内存访问
db.pragma('mmap_size = 1073741824');
// 7. 页面缓存 - 64MB 缓存
db.pragma('cache_size = -64000');
// 8. WAL 自动检查点 - 每 1000 页
db.pragma('wal_autocheckpoint = 1000');
// 9. 缓存溢出 - 允许溢出到磁盘
db.pragma('cache_spill = ON');
// 10. 日志大小限制 - WAL 文件最大 100MB
db.pragma('journal_size_limit = 104857600');
// 11. 锁定模式 - 允许共享锁
db.pragma('locking_mode = NORMAL');
// 12. 自动索引 - 允许创建临时索引
db.pragma('automatic_index = ON');关键 PRAGMA 对比:
| PRAGMA | 默认值 | 优化值 | 效果 |
|---|---|---|---|
| journal_mode | DELETE | WAL | 读写并发,写入不阻塞读取 |
| synchronous | FULL | FULL | 数据安全优先(不改为 NORMAL) |
| cache_size | -2000 (2MB) | -64000 (64MB) | 减少磁盘 I/O |
| mmap_size | 0 | 1GB | 大查询直接内存访问 |
| busy_timeout | 0 | 5000ms | 避免锁竞争立即失败 |
| temp_store | DEFAULT | MEMORY | 临时表在内存中 |
WAL 模式工作原理:
传统模式 (DELETE): WAL 模式:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ DB File │ │ DB File │ │ WAL File │
│ │ │ (主数据) │◀───│ (预写日志)│
│ │ └─────────┘ └─────────┘
└─────────┘ │ │
▲ │ │
│ 写入时: │ 读取: │ 写入:
│ 1. 锁整个文件 │ 读主文件 │ 写入 WAL
│ 2. 写入修改 │ + WAL 文件 │ 文件
│ 3. 检查点 │ (不阻塞) │
│ 4. 删除日志 │ │
│ (阻塞所有读取) │ 检查点时合并 │
│ 到主文件 │
└───────────────┘
优势: 读取者不被写入者阻塞,适合读多写少的场景预编译语句(Prepared Statements):
项目中所有数据库操作均使用预编译语句,避免重复 SQL 解析:
// 预编译一次,多次执行
const insertAgent = db.prepare(`
INSERT INTO agents (id, name, avatar, role, system_prompt, model, temperature, is_preset, enabled, category, description)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// 高效执行
presetAgents.forEach(agent => {
insertAgent.run(agent.id, agent.name, ...);
});索引策略:
项目为高频查询字段创建了针对性索引:
-- 服务器相关
CREATE INDEX idx_servers_enabled ON servers(enabled);
CREATE INDEX idx_cmd_history_server_id ON server_command_history(server_id);
CREATE INDEX idx_cmd_history_executed_at ON server_command_history(executed_at);
-- Agent 相关
CREATE INDEX idx_agent_executions_agent_id ON agent_executions(agent_id);
CREATE INDEX idx_agent_executions_created_at ON agent_executions(created_at);
CREATE INDEX idx_agents_category ON agents(category);
CREATE INDEX idx_agents_usage ON agents(usage_count);
-- 告警相关
CREATE INDEX idx_alerts_status ON alerts(status);
CREATE INDEX idx_alerts_created_at ON alerts(created_at);
CREATE INDEX idx_alerts_severity ON alerts(severity);
-- 报告相关
CREATE INDEX idx_reports_created_at ON reports(created_at DESC);
CREATE INDEX idx_reports_task_id ON reports(task_id);索引查询优化示例:
// 有索引: O(log n) - 直接通过索引定位
const recentAlerts = db.prepare(`
SELECT * FROM alerts
WHERE status = 'new'
ORDER BY created_at DESC
LIMIT 50
`).all();
// 使用 idx_alerts_status + idx_alerts_created_at
// 无索引: O(n) - 全表扫描
const slowQuery = db.prepare(`
SELECT * FROM alerts
WHERE content LIKE '%disk%' -- LIKE 前缀通配符无法使用索引
ORDER BY created_at DESC
`).all();22.2 React Query 缓存策略
前端使用 @tanstack/react-query 管理服务端状态,通过精细的缓存配置减少不必要的 API 请求。
// frontend/src/App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 分钟内数据视为"新鲜",不重新请求
gcTime: 10 * 60 * 1000, // 10 分钟后清理无用缓存 (原 cacheTime)
refetchOnWindowFocus: false, // 窗口聚焦不自动重新请求
retry: 1, // 失败重试 1 次
},
mutations: {
retry: 0, // 写操作不重试
},
},
});React Query 缓存生命周期:
请求数据
│
▼
┌─────────────────────┐
│ Fresh (新鲜期) │ ← staleTime 内 (5分钟)
│ 直接使用缓存 │ 不再发请求
└─────────┬───────────┘
│ staleTime 到期
▼
┌─────────────────────┐
│ Stale (过期) │ ← 下次使用时后台重新请求
│ 先用缓存再更新 │ (后台静默刷新)
└─────────┬───────────┘
│ 组件卸载
▼
┌─────────────────────┐
│ Inactive (非活跃) │ ← gcTime 内 (10分钟)
│ 保留在缓存中 │ 组件重新挂载可复用
└─────────┬───────────┘
│ gcTime 到期
▼
┌─────────────────────┐
│ Garbage Collected │ ← 从内存中移除
│ (垃圾回收) │
└─────────────────────┘缓存失效策略(Invalidation):
// 修改数据后使相关缓存失效
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data) => api.post('/api/servers', data),
onSuccess: () => {
// 使服务器列表缓存失效,触发重新请求
queryClient.invalidateQueries({ queryKey: ['servers'] });
},
});页面级缓存配置示例:
// 服务器列表 - 适中缓存 (5分钟)
useQuery({
queryKey: ['servers'],
queryFn: () => api.get('/api/servers').then(r => r.data),
// 使用默认 staleTime: 5min
});
// 用户信息 - 长缓存 (30分钟)
useQuery({
queryKey: ['user'],
queryFn: () => api.get('/api/auth/me').then(r => r.data),
staleTime: 30 * 60 * 1000,
});
// 实时数据 - 短缓存 (10秒)
useQuery({
queryKey: ['tasks', 'active'],
queryFn: () => api.get('/api/tasks?status=running').then(r => r.data),
staleTime: 10 * 1000,
refetchInterval: 30 * 1000, // 每 30 秒轮询
});22.3 Zustand Store 优化
前端使用 Zustand 管理客户端状态(认证、UI 状态等)。
认证状态管理:
// frontend/src/contexts/AuthContext.tsx
interface AuthState {
user: User | null;
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
login: (token: string, user: User, refreshToken: string) => void;
logout: () => void;
}
// 持久化到 localStorage
const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
login: (token, user, refreshToken) => set({
token, user, refreshToken, isAuthenticated: true
}),
logout: () => set({
token: null, user: null, refreshToken: null, isAuthenticated: false
}),
}),
{ name: 'auth-storage' }
)
);Zustand 性能优化要点:
| 优化策略 | 说明 | 适用场景 |
|---|---|---|
| 选择器 (Selector) | useStore(s => s.user) 只在 user 变化时重渲染 | 大 Store 中仅使用部分状态 |
| shallow 比较 | useStore(s => s.items, shallow) 避免引用变化导致重渲染 | 数组/对象状态 |
| 持久化 | persist() 中间件 | 认证 Token、用户偏好 |
| 拆分 Store | 按领域拆分多个 Store | 独立领域状态 |
| 避免内联对象 | 不在 selector 中创建新对象 | useStore(s => ({ a: s.a })) 会导致每次重渲染 |
22.4 WebSocket 连接管理与资源清理
Web 终端功能通过 WebSocket 实现实时 SSH 会话,连接管理和资源清理至关重要。
// frontend/src/components/WebTerminal.tsx
const connect = useCallback(() => {
const socket = io(import.meta.env.VITE_API_URL || 'http://localhost:3001', {
auth: { token },
transports: ['websocket']
});
socketRef.current = socket;
// 终端数据监听
const terminalDataHandler = (data: { sessionId: string; data: string }) => {
if (data.sessionId === sessionIdRef.current && xtermRef.current) {
xtermRef.current.write(data.data);
}
};
socket.on('terminal:data', terminalDataHandler);
socket.on('connect', () => {
reconnectCountRef.current = 0;
socket.emit('terminal:open', { serverId, cols: term.cols, rows: term.rows }, (result) => {
if (result.error) {
setStatus('error');
setError(result.error);
return;
}
sessionIdRef.current = result.sessionId;
setStatus('connected');
});
});
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
socket.disconnect();
setStatus('disconnected');
return;
}
// 指数退避重连
if (reconnectCountRef.current < maxReconnectAttempts) {
setStatus('connecting');
reconnectCountRef.current++;
reconnectTimerRef.current = setTimeout(() => {
socket.connect();
}, Math.min(1000 * Math.pow(2, reconnectCountRef.current), 5000));
} else {
setStatus('disconnected');
setError('Terminal connection lost');
}
});
}, []);
// 组件卸载时清理所有资源
useEffect(() => {
return () => {
// 清理重连定时器
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
// 关闭 WebSocket 连接
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
// 清理 xterm 实例
if (xtermRef.current) {
xtermRef.current.dispose();
xtermRef.current = null;
}
};
}, []);WebSocket 资源清理清单:
组件卸载 (useEffect cleanup)
│
├── 清除 reconnectTimer (setTimeout)
│ └── 防止组件销毁后仍然触发重连
│
├── socket.disconnect()
│ └── 关闭 WebSocket 连接,释放服务器端 session
│
└── xterm.dispose()
└── 销毁 xterm.js 实例,释放 DOM 和内存指数退避重连策略:
第 1 次重连: 1000 * 2^1 = 2s
第 2 次重连: 1000 * 2^2 = 4s
第 3 次重连: 1000 * 2^3 = 8s → capped at 5s
第 4 次重连: 5s (上限)
第 5 次重连: 5s (上限) → 达到 maxReconnectAttempts,放弃Socket.IO 服务端配置:
// backend/src/app.ts
const io = new SocketIOServer(httpServer, {
cors: {
origin: env.ALLOWED_ORIGINS,
methods: ['GET', 'POST']
},
maxHttpBufferSize: 1e6, // 1MB 最大消息
pingTimeout: 60000, // 60 秒无响应判定断开
pingInterval: 25000 // 25 秒发送一次 ping
});22.5 前端构建优化
Vite 配置中的优化策略:
// frontend/vite.config.ts
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: process.env.DOCKER_MODE ? 'http://backend:3001' : 'http://localhost:3001',
changeOrigin: true,
},
'/socket.io': {
target: process.env.DOCKER_MODE ? 'http://backend:3001' : 'http://localhost:3001',
ws: true,
},
},
},
});Vite 默认优化(开箱即用):
| 特性 | 说明 | 效果 |
|---|---|---|
| ESBuild | 使用 Go 编写的构建器 | 比 Webpack 快 10-100 倍 |
| 代码分割 | 按路由自动分割 | 首屏加载更小 |
| Tree Shaking | 移除未使用代码 | 减小产物体积 |
| 预加载 | 自动注入 <link rel="modulepreload"> | 加速后续路由加载 |
| HMR | 热模块替换 | 开发时即时更新 |
Nginx 侧的缓存优化:
# JS/CSS 文件:1 年缓存 + immutable
location ~* \.(js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable" always;
}
# HTML 文件:不缓存
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}构建产物分析:
dist/
├── index.html ~1KB (入口,不缓存)
├── assets/
│ ├── index-abc123.js ~200KB (主 bundle,1年缓存)
│ ├── vendors-def456.js ~150KB (第三方库,1年缓存)
│ ├── xterm-ghi789.js ~80KB (终端组件,懒加载)
│ └── index-jkl012.css ~20KB (样式,1年缓存)
└── ...22.6 Docker 资源限制
通过 Docker Compose 的 deploy.resources 配置容器资源限制:
services:
backend:
deploy:
resources:
limits: # 硬限制(不能超过)
cpus: '2.0'
memory: 2G
reservations: # 软保证(最低分配)
cpus: '0.5'
memory: 512M
frontend:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M资源分配分析:
┌─────────────────────────────────────────────────────┐
│ 宿主机资源分配 │
│ │
│ ┌───────────────────────────────┐ │
│ │ Backend Container │ │
│ │ Reservation: 0.5 CPU / 512MB │ │
│ │ Limit: 2.0 CPU / 2GB │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ Frontend Container │ │
│ │ Reservation: 0.25/128MB│ │
│ │ Limit: 1.0/512MB │ │
│ └───────────────────────┘ │
│ │
│ 剩余资源供宿主机和其他进程使用 │
└─────────────────────────────────────────────────────┘22.7 健康检查与监控端点
健康检查配置:
# docker-compose.yml
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})\""]
interval: 30s # 每 30 秒检查
timeout: 10s # 超时 10 秒
retries: 3 # 连续 3 次失败标记 unhealthy
start_period: 30s # 启动宽限期健康检查端点实现:
// backend/src/app.ts
app.get('/health', async (_req, res) => {
try {
// 检查数据库连接
db.prepare('SELECT 1').get();
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
database: 'connected'
});
} catch (error) {
res.status(503).json({
status: 'error',
database: 'disconnected'
});
}
});健康检查状态流转:
容器启动
│
▼
starting (start_period: 30s)
│ 健康检查不计数
│
▼
healthy? ── Yes ──▶ healthy
│
No
│
等待 interval (30s)
│
▼
healthy? ── Yes ──▶ healthy
│
No (连续 3 次)
│
▼
unhealthy ──▶ 触发 restart: unless-stopped本章小结
本章从六个维度系统讲解了项目的性能优化策略:
- 数据库层:WAL 模式、64MB 缓存、1GB 内存映射、预编译语句、针对性索引
- 前端缓存层:React Query 的 staleTime/gcTime 策略、按需失效
- 状态管理层:Zustand 选择器优化、持久化中间件
- 连接管理层:WebSocket 指数退避重连、组件卸载资源清理
- 构建优化层:Vite ESBuild、Nginx 分级缓存、代码分割
- 基础设施层:Docker 资源限制、健康检查、日志轮转
这些优化相互协作,确保了平台在中等负载下的流畅体验。理解这些优化原理,将帮助你根据实际场景进行针对性调优。
本章练习
基础练习
数据库索引分析:使用
EXPLAIN QUERY PLAN分析项目中 3 个常用查询的执行计划,确认是否使用了索引。针对没有使用索引的查询,设计并添加合适的索引。React Query 缓存调优:在 Settings 页面中,将 QAnything 配置查询的
staleTime设置为 30 分钟,并在保存配置后使缓存失效。观察 Network 面板中的请求变化。WebSocket 重连测试:在 Web 终端页面,通过
docker restart itops-backend模拟后端重启,观察前端的重连行为。验证指数退避策略是否生效。
进阶练习
数据库性能监控:在
database.ts中添加慢查询日志功能。当查询执行时间超过 100ms 时,记录 SQL 语句和执行时间。设计一个定期分析慢查询的脚本。实现请求去重:使用 React Query 的
queryClient.ensureQueryData()或自定义 Hook 实现防抖请求,避免用户在短时间内多次点击导致的重复 API 请求。构建分析:使用
rollup-plugin-visualizer分析前端构建产物,找出体积最大的依赖包。尝试通过动态导入(import())或替换为更轻量的替代方案来减小体积。
思考题
SQLite 是单文件数据库,在容器化环境中,WAL 模式会在数据库文件旁边生成
-wal和-shm两个辅助文件。如果在备份时只复制了.db文件而遗漏了这两个文件,可能会导致什么问题?如何确保备份的完整性?项目中 React Query 的全局
staleTime设置为 5 分钟。对于一个有 50 个页面的大型应用,统一的全局配置和页面级局部配置各有什么优缺点?你会如何设计分层缓存策略?
延伸阅读
- SQLite 官方性能优化指南: https://www.sqlite.org/speed.html
- TanStack Query 文档: https://tanstack.com/query/latest/docs/react/overview
- Zustand 最佳实践: https://docs.pmnd.rs/zustand/guides/practice-with-no-store-actions
- Vite 性能优化: https://vitejs.dev/guide/performance.html
- Socket.IO 文档: https://socket.io/docs/v4/
