Files
dns-server/docs/日志加载优化方案.md
T
2026-04-03 10:04:07 +08:00

13 KiB
Raw Blame History

DNS 查询日志加载性能优化方案

问题分析

当前日志详情页面加载时间约 10 秒,主要性能瓶颈分析如下:

1. 主要性能瓶颈

1.1 同步 API 调用(最严重)

  • 位置: updateLogsTable() 函数第 1221 行
  • 问题: 对每条日志都调用 await isDomainInTrackerDatabase(log.domain)
  • 影响: 假设每页 10 条日志,每条日志检查跟踪器数据库需要 100-500ms,总计 1-5 秒
  • 代码:
const trackerInfo = await isDomainInTrackerDatabase(log.domain); // ❌ 每条都 await

1.2 跟踪器数据库加载

  • 位置: isDomainInTrackerDatabase() 函数
  • 问题: 每次检查都要等待数据库加载完成
  • 影响: 首次加载或数据库未加载完成时,每条日志都要等待

1.3 域名信息 API 调用

  • 位置: getDomainInfoFromAPI() 函数
  • 问题: 如果实现中调用 API,会产生网络延迟
  • 影响: 每个域名都可能产生一次 HTTP 请求

1.4 IP 地理位置查询

  • 位置: getIpGeolocation() 函数
  • 问题: 虽然有缓存,但首次查询仍需调用外部 API
  • 影响: 每个新 IP 地址需要 200-500ms

2. 性能测试数据(估算)

操作 单次耗时 每页 10 条总耗时 优化后耗时
跟踪器检查 100-500ms 1-5 秒 <100ms
IP 地理位置 200-500ms 2-5 秒 <500ms(缓存)
后端 API 查询 100-300ms 100-300ms 不变
DOM 渲染 50-100ms 50-100ms 不变
总计 10 秒 3-10 秒 <1 秒

优化方案

方案一:异步并行处理(推荐优先实施)

1.1 批量并行检查跟踪器

问题: 当前是串行等待每个域名的检查结果 解决: 使用 Promise.all() 并行处理所有域名的检查

// 优化前(串行)
for (const log of logs) {
    const trackerInfo = await isDomainInTrackerDatabase(log.domain);
    // ... 处理逻辑
}

// 优化后(并行)
const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
const trackerResults = await Promise.all(trackerChecks);

// 然后渲染表格
logs.forEach((log, index) => {
    const trackerInfo = trackerResults[index];
    // ... 渲染逻辑
});

预期效果: 从 1-5 秒降低到 100-500ms

1.2 预加载跟踪器数据库

问题: 每次检查都要等待数据库加载 解决: 页面初始化时立即加载,而不是延迟加载

// 在页面加载时立即执行
document.addEventListener('DOMContentLoaded', () => {
    loadTrackersDatabase(); // 提前加载,不等待日志显示
});

预期效果: 消除数据库加载等待时间


方案二:缓存优化

2.1 增强跟踪器检查缓存

问题: 相同的域名被重复检查 解决: 添加内存缓存,避免重复检查

// 添加缓存
const trackerCache = new Map();
const trackerCacheTimestamp = new Map();
const TRACKER_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 小时

async function isDomainInTrackerDatabase(domain) {
    // 检查缓存
    const cached = trackerCache.get(domain);
    const timestamp = trackerCacheTimestamp.get(domain);
    
    if (cached && timestamp && (Date.now() - timestamp) < TRACKER_CACHE_EXPIRY) {
        return cached;
    }
    
    // 执行实际检查...
    const result = // ... 检查逻辑
    
    // 缓存结果
    trackerCache.set(domain, result);
    trackerCacheTimestamp.set(domain, Date.now());
    
    return result;
}

预期效果: 重复域名检查从 100ms 降低到 <1ms

2.2 IP 地理位置缓存优化

问题: 相同 IP 被重复查询 解决: 已有 LRU 缓存,确保缓存命中率高

// 确保缓存大小足够(当前为 1000,建议保持)
const GEOLOCATION_CACHE_MAX_SIZE = 1000;

方案三:延迟渲染优化

3.1 先渲染后更新

问题: 等待所有数据获取完成才渲染表格 解决: 先渲染基本内容,再异步更新增强信息

async function updateLogsTable(logs) {
    // 第一步:立即渲染基本表格(不等待跟踪器检查)
    renderBasicTable(logs);
    
    // 第二步:异步获取跟踪器信息并更新
    const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
    Promise.all(trackerChecks).then(results => {
        updateTrackerIcons(results); // 只更新图标部分
    });
    
    // 第三步:异步获取 IP 地理位置
    logs.forEach(log => {
        getIpGeolocation(log.clientIP).then(location => {
            updateLocationElement(log.clientIP, location);
        });
    });
}

预期效果: 用户感知加载时间从 10 秒降低到 500ms 以内

3.2 虚拟滚动(针对大量数据)

问题: 如果用户选择每页 100 条,DOM 节点过多 解决: 只渲染可见区域的日志

// 使用虚拟滚动库或自定义实现
// 只渲染可视区域的 20-30 条,滚动时动态加载

预期效果: 渲染时间从 100ms 降低到 20ms


方案四:后端优化

4.1 后端批量检查跟踪器

问题: 前端检查每条日志的跟踪器状态 解决: 后端在返回日志时直接包含跟踪器信息

// 后端代码修改
type QueryLog struct {
    // ... 现有字段
    IsTracker bool   `json:"isTracker"`
    TrackerName string `json:"trackerName,omitempty"`
}

// 在返回日志前批量检查
func (s *Server) enrichLogsWithTrackerInfo(logs []QueryLog) {
    for i := range logs {
        logs[i].IsTracker, logs[i].TrackerName = s.checkTracker(logs[i].Domain)
    }
}

预期效果: 完全消除前端跟踪器检查时间

4.2 后端缓存优化

问题: 后端查询数据库慢 解决: 增加 Redis 或内存缓存

// 使用内存缓存查询结果
var queryCache = lru.New(100)

预期效果: 后端查询时间从 300ms 降低到 50ms


方案五:数据库索引优化

5.1 添加数据库索引

问题: 日志查询慢 解决: 为常用查询字段添加索引

-- 为时间戳添加索引(加速时间范围查询)
CREATE INDEX idx_timestamp ON query_logs(timestamp DESC);

-- 为域名添加索引(加速域名搜索)
CREATE INDEX idx_domain ON query_logs(domain);

-- 为客户端 IP 添加索引
CREATE INDEX idx_client_ip ON query_logs(client_ip);

-- 组合索引(加速组合查询)
CREATE INDEX idx_timestamp_result ON query_logs(timestamp DESC, result);

预期效果: 后端查询时间从 500ms 降低到 50ms


实施优先级

第一阶段(立即实施,效果最明显)

  1. 方案 1.1: 并行处理跟踪器检查
  2. 方案 2.1: 添加跟踪器检查缓存
  3. 方案 1.2: 预加载跟踪器数据库

预期效果: 10 秒 → 2-3 秒

第二阶段(本周内实施)

  1. 方案 3.1: 先渲染后更新
  2. 方案 4.1: 后端批量检查跟踪器

预期效果: 2-3 秒 → 1 秒以内

第三阶段(长期优化)

  1. 方案 5.1: 数据库索引优化
  2. 方案 4.2: 后端缓存
  3. 方案 3.2: 虚拟滚动

预期效果: 1 秒 → 500ms 以内


具体代码修改

修改 1: logs.js - 并行处理跟踪器检查

// 在 updateLogsTable() 函数中,替换第 1155-1353 行

async function updateLogsTable(logs) {
    const tableBody = document.getElementById('logs-table-body');
    if (!tableBody) return;
    
    // 清空表格
    tableBody.innerHTML = '';
    
    if (logs.length === 0) {
        // 显示空状态
        tableBody.innerHTML = `
            <tr>
                <td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
                    <i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
                    <div>暂无查询日志</div>
                </td>
            </tr>
        `;
        return;
    }
    
    // 检测是否为移动设备
    const isMobile = window.innerWidth <= 768;
    
    // 【优化】并行处理所有跟踪器检查
    const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
    const trackerResults = await Promise.all(trackerChecks);
    
    // 预创建文档片段,减少 DOM 操作
    const fragment = document.createDocumentFragment();
    
    // 填充表格
    for (let i = 0; i < logs.length; i++) {
        const log = logs[i];
        const trackerInfo = trackerResults[i];
        const isTracker = trackerInfo !== null;
        
        const row = document.createElement('tr');
        row.className = `border-b border-gray-100 hover:bg-gray-50 transition-colors ${
            log.result === 'blocked' ? 'bg-red-50' : ''
        }`;
        
        // ... 渲染逻辑(保持不变,但移除 await)
        
        fragment.appendChild(row);
    }
    
    // 一次性添加到 DOM
    tableBody.appendChild(fragment);
    
    // 【优化】异步更新 IP 地理位置(不阻塞渲染)
    logs.forEach(log => {
        if (!isPrivateIP(log.clientIP)) {
            getIpGeolocation(log.clientIP).then(location => {
                const locationEl = document.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
                if (locationEl) {
                    locationEl.textContent = location;
                }
            });
        }
    });
}

修改 2: logs.js - 添加跟踪器缓存

// 在全局变量部分添加
const trackerCache = new Map();
const trackerCacheTimestamp = new Map();
const TRACKER_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 小时

// 修改 isDomainInTrackerDatabase 函数
async function isDomainInTrackerDatabase(domain) {
    // 检查缓存
    const cached = trackerCache.get(domain);
    const timestamp = trackerCacheTimestamp.get(domain);
    
    if (cached !== undefined && timestamp && (Date.now() - timestamp) < TRACKER_CACHE_EXPIRY) {
        return cached;
    }
    
    // 确保数据库已加载
    if (!trackersDatabase || !trackersLoaded) {
        await loadTrackersDatabase();
    }
    
    if (!trackersDatabase || !trackersDatabase.trackers) {
        trackerCache.set(domain, null);
        trackerCacheTimestamp.set(domain, Date.now());
        return null;
    }
    
    // 检查逻辑(保持不变)
    let result = null;
    
    if (trackersDatabase.trackers.hasOwnProperty(domain)) {
        result = trackersDatabase.trackers[domain];
    } else {
        for (const trackerKey in trackersDatabase.trackers) {
            const tracker = trackersDatabase.trackers[trackerKey];
            if (tracker && tracker.url) {
                try {
                    const trackerUrl = new URL(tracker.url);
                    if (trackerUrl.hostname === domain) {
                        result = tracker;
                        break;
                    }
                } catch (e) {
                    // 忽略无效 URL
                }
            }
        }
    }
    
    // 缓存结果
    trackerCache.set(domain, result);
    trackerCacheTimestamp.set(domain, Date.now());
    
    // 限制缓存大小(LRU 简单实现)
    if (trackerCache.size > 1000) {
        const oldestKey = trackerCache.keys().next().value;
        trackerCache.delete(oldestKey);
        trackerCacheTimestamp.delete(oldestKey);
    }
    
    return result;
}

修改 3: index.html - 预加载跟踪器数据库

<!-- 在页面底部,logs.js 加载后立即执行 -->
<script>
    // 页面加载时立即预加载跟踪器数据库
    document.addEventListener('DOMContentLoaded', function() {
        // 异步加载,不阻塞页面
        if (typeof loadTrackersDatabase === 'function') {
            loadTrackersDatabase();
        }
    });
</script>

性能监控

添加性能日志

// 在 loadLogs() 函数中添加性能监控
async function loadLogs() {
    const startTime = performance.now();
    console.log('🚀 开始加载日志...');
    
    // ... 加载逻辑
    
    const endTime = performance.now();
    const duration = endTime - startTime;
    console.log(`✅ 日志加载完成,耗时:${duration.toFixed(2)}ms`);
    
    // 分解各阶段耗时
    console.log('📊 性能分析:', {
        API 请求apiTime,
        跟踪器检查trackerTime,
        DOM 渲染renderTime,
        IP 查询ipTime
    });
}

预期最终效果

优化阶段 加载时间 改善幅度
优化前 10 秒 -
第一阶段 2-3 秒 70-80%
第二阶段 1 秒以内 90%
第三阶段 500ms 以内 95%

风险与注意事项

  1. 内存使用: 缓存会增加内存占用,建议限制缓存大小
  2. 缓存一致性: 跟踪器数据库更新后需要清理缓存
  3. 浏览器兼容性: Promise.all() 在老旧浏览器可能不支持
  4. 并发限制: 如果日志数量过多(>100),考虑分批处理

总结

最优先实施的优化:

  1. 并行处理跟踪器检查(方案 1.1
  2. 添加跟踪器检查缓存(方案 2.1
  3. 先渲染后更新(方案 3.1

这三项优化可以将加载时间从 10 秒降低到 1 秒以内,且实施成本低,风险小。