Skip to content

第二十二章 性能优化与调优

作者

谭策 — 独立开发者 | AIOps 领域探索者

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 性能配置详解:

typescript
// 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_modeDELETEWAL读写并发,写入不阻塞读取
synchronousFULLFULL数据安全优先(不改为 NORMAL)
cache_size-2000 (2MB)-64000 (64MB)减少磁盘 I/O
mmap_size01GB大查询直接内存访问
busy_timeout05000ms避免锁竞争立即失败
temp_storeDEFAULTMEMORY临时表在内存中

WAL 模式工作原理:

传统模式 (DELETE):                    WAL 模式:

  ┌─────────┐                          ┌─────────┐     ┌─────────┐
  │  DB File │                          │  DB File │     │ WAL File │
  │          │                          │ (主数据)  │◀───│ (预写日志)│
  │          │                          └─────────┘     └─────────┘
  └─────────┘                               │               │
       ▲                                    │               │
       │ 写入时:                              │ 读取:         │ 写入:
       │ 1. 锁整个文件                        │ 读主文件      │ 写入 WAL
       │ 2. 写入修改                          │ + WAL 文件    │ 文件
       │ 3. 检查点                            │ (不阻塞)      │
       │ 4. 删除日志                          │               │
       │ (阻塞所有读取)                       │ 检查点时合并  │
                                            │ 到主文件      │
                                            └───────────────┘

优势: 读取者不被写入者阻塞,适合读多写少的场景

预编译语句(Prepared Statements):

项目中所有数据库操作均使用预编译语句,避免重复 SQL 解析:

typescript
// 预编译一次,多次执行
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, ...);
});

索引策略:

项目为高频查询字段创建了针对性索引:

sql
-- 服务器相关
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);

索引查询优化示例:

typescript
// 有索引: 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 请求。

typescript
// 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):

typescript
// 修改数据后使相关缓存失效
const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: (data) => api.post('/api/servers', data),
  onSuccess: () => {
    // 使服务器列表缓存失效,触发重新请求
    queryClient.invalidateQueries({ queryKey: ['servers'] });
  },
});

页面级缓存配置示例:

typescript
// 服务器列表 - 适中缓存 (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 状态等)。

认证状态管理:

typescript
// 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 会话,连接管理和资源清理至关重要。

typescript
// 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 服务端配置:

typescript
// 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 配置中的优化策略:

typescript
// 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 侧的缓存优化:

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 配置容器资源限制:

yaml
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 健康检查与监控端点

健康检查配置:

yaml
# 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   # 启动宽限期

健康检查端点实现:

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

本章小结

本章从六个维度系统讲解了项目的性能优化策略:

  1. 数据库层:WAL 模式、64MB 缓存、1GB 内存映射、预编译语句、针对性索引
  2. 前端缓存层:React Query 的 staleTime/gcTime 策略、按需失效
  3. 状态管理层:Zustand 选择器优化、持久化中间件
  4. 连接管理层:WebSocket 指数退避重连、组件卸载资源清理
  5. 构建优化层:Vite ESBuild、Nginx 分级缓存、代码分割
  6. 基础设施层:Docker 资源限制、健康检查、日志轮转

这些优化相互协作,确保了平台在中等负载下的流畅体验。理解这些优化原理,将帮助你根据实际场景进行针对性调优。

本章练习

基础练习

  1. 数据库索引分析:使用 EXPLAIN QUERY PLAN 分析项目中 3 个常用查询的执行计划,确认是否使用了索引。针对没有使用索引的查询,设计并添加合适的索引。

  2. React Query 缓存调优:在 Settings 页面中,将 QAnything 配置查询的 staleTime 设置为 30 分钟,并在保存配置后使缓存失效。观察 Network 面板中的请求变化。

  3. WebSocket 重连测试:在 Web 终端页面,通过 docker restart itops-backend 模拟后端重启,观察前端的重连行为。验证指数退避策略是否生效。

进阶练习

  1. 数据库性能监控:在 database.ts 中添加慢查询日志功能。当查询执行时间超过 100ms 时,记录 SQL 语句和执行时间。设计一个定期分析慢查询的脚本。

  2. 实现请求去重:使用 React Query 的 queryClient.ensureQueryData() 或自定义 Hook 实现防抖请求,避免用户在短时间内多次点击导致的重复 API 请求。

  3. 构建分析:使用 rollup-plugin-visualizer 分析前端构建产物,找出体积最大的依赖包。尝试通过动态导入(import())或替换为更轻量的替代方案来减小体积。

思考题

  1. SQLite 是单文件数据库,在容器化环境中,WAL 模式会在数据库文件旁边生成 -wal-shm 两个辅助文件。如果在备份时只复制了 .db 文件而遗漏了这两个文件,可能会导致什么问题?如何确保备份的完整性?

  2. 项目中 React Query 的全局 staleTime 设置为 5 分钟。对于一个有 50 个页面的大型应用,统一的全局配置和页面级局部配置各有什么优缺点?你会如何设计分层缓存策略?

延伸阅读

基于 MPL-2.0 许可证发布