优化后端的性能

This commit is contained in:
CN-JS-HuiBai
2026-04-24 15:46:21 +08:00
parent c254923bd6
commit 8e50437ed4
5 changed files with 126 additions and 63 deletions

View File

@@ -588,6 +588,11 @@
<option value="max">出入取大 (Max)</option>
</select>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="prometheusCacheTtlInput">数据自动刷新/同步间隔 (秒)</label>
<input type="number" id="prometheusCacheTtlInput" placeholder="例30" min="0" max="86400">
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">后端将按此频率主动从 Prometheus 抓取数据并缓存。设为 0 则禁用自动同步。建议值15-60s。</p>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="psFilingInput">公安备案号 (如:京公网安备 11010102000001号)</label>
<input type="text" id="psFilingInput" placeholder="请输入公安备案号">

View File

@@ -132,7 +132,8 @@
btnAddCustomMetric: document.getElementById('btnAddCustomMetric'),
btnSaveCustomMetrics: document.getElementById('btnSaveCustomMetrics'),
customDataContainer: document.getElementById('customDataContainer'),
cdnUrlInput: document.getElementById('cdnUrlInput')
cdnUrlInput: document.getElementById('cdnUrlInput'),
prometheusCacheTtlInput: document.getElementById('prometheusCacheTtlInput')
};
// ---- State ----
@@ -655,6 +656,7 @@
if (dom.logoUrlDarkInput) dom.logoUrlDarkInput.value = window.SITE_SETTINGS.logo_url_dark || '';
if (dom.faviconUrlInput) dom.faviconUrlInput.value = window.SITE_SETTINGS.favicon_url || '';
if (dom.showServerIpInput) dom.showServerIpInput.value = window.SITE_SETTINGS.show_server_ip ? "1" : "0";
if (dom.prometheusCacheTtlInput) dom.prometheusCacheTtlInput.value = window.SITE_SETTINGS.prometheus_cache_ttl !== undefined ? window.SITE_SETTINGS.prometheus_cache_ttl : 30;
// Apply security dependency
updateSecurityDependency();
@@ -2311,7 +2313,8 @@
ip_metric_name: dom.ipMetricNameInput ? dom.ipMetricNameInput.value.trim() : null,
ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address',
custom_metrics: getCustomMetricsFromUI(),
cdn_url: dom.cdnUrlInput ? dom.cdnUrlInput.value.trim() : ''
cdn_url: dom.cdnUrlInput ? dom.cdnUrlInput.value.trim() : '',
prometheus_cache_ttl: dom.prometheusCacheTtlInput ? parseInt(dom.prometheusCacheTtlInput.value) : 30
};
try {

View File

@@ -74,14 +74,15 @@ const SCHEMA = {
ip_label_name VARCHAR(100) DEFAULT 'address',
custom_metrics JSON DEFAULT NULL,
cdn_url VARCHAR(500) DEFAULT NULL,
prometheus_cache_ttl INT DEFAULT 30,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`,
seedSql: `
INSERT IGNORE INTO site_settings (
id, page_name, show_page_name, title, default_theme, show_95_bandwidth, p95_type, require_login_for_server_details
id, page_name, show_page_name, title, default_theme, show_95_bandwidth, p95_type, require_login_for_server_details, prometheus_cache_ttl
) VALUES (
1, '数据可视化展示大屏', 1, '数据可视化展示大屏', 'dark', 0, 'tx', 1
1, '数据可视化展示大屏', 1, '数据可视化展示大屏', 'dark', 0, 'tx', 1, 30
)
`,
columns: [
@@ -105,7 +106,8 @@ const SCHEMA = {
{ name: 'ip_metric_name', sql: "ALTER TABLE site_settings ADD COLUMN ip_metric_name VARCHAR(100) DEFAULT NULL AFTER show_server_ip" },
{ name: 'ip_label_name', sql: "ALTER TABLE site_settings ADD COLUMN ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name" },
{ name: 'custom_metrics', sql: "ALTER TABLE site_settings ADD COLUMN custom_metrics JSON DEFAULT NULL AFTER ip_label_name" },
{ name: 'cdn_url', sql: "ALTER TABLE site_settings ADD COLUMN cdn_url VARCHAR(500) DEFAULT NULL AFTER custom_metrics" }
{ name: 'cdn_url', sql: "ALTER TABLE site_settings ADD COLUMN cdn_url VARCHAR(500) DEFAULT NULL AFTER custom_metrics" },
{ name: 'prometheus_cache_ttl', sql: "ALTER TABLE site_settings ADD COLUMN prometheus_cache_ttl INT DEFAULT 30 AFTER cdn_url" }
]
},
traffic_stats: {

View File

@@ -22,6 +22,7 @@ const fs = require('fs');
const crypto = require('crypto');
let isDbInitialized = false;
let metricSyncTimer = null; // Background sync timer
const sessions = new Map(); // Fallback session store when Valkey is unavailable
const requestBuckets = new Map();
const SESSION_TTL_SECONDS = parseInt(process.env.SESSION_TTL_SECONDS, 10) || 86400;
@@ -152,7 +153,8 @@ function getPublicSiteSettings(settings = {}) {
ip_metric_name: settings.ip_metric_name || null,
ip_label_name: settings.ip_label_name || 'address',
custom_metrics: settings.custom_metrics || [],
cdn_url: settings.cdn_url || null
cdn_url: settings.cdn_url || null,
prometheus_cache_ttl: settings.prometheus_cache_ttl !== undefined ? parseInt(settings.prometheus_cache_ttl) : 30
};
}
@@ -342,7 +344,82 @@ async function checkDb() {
}
}
checkDb();
checkDb().then(() => {
if (isDbInitialized) {
startMetricSync();
}
});
/**
* Background Metric Synchronization Task
*/
async function startMetricSync() {
if (metricSyncTimer) {
clearInterval(metricSyncTimer);
metricSyncTimer = null;
}
try {
const [rows] = await db.query('SELECT prometheus_cache_ttl FROM site_settings WHERE id = 1');
const ttl = rows.length > 0 ? (parseInt(rows[0].prometheus_cache_ttl) || 0) : 30;
if (ttl <= 0) {
console.log('[MetricSync] Disabled (TTL=0)');
return;
}
console.log(`[MetricSync] Started with interval: ${ttl}s`);
// Immediate first run
runSync();
metricSyncTimer = setInterval(runSync, ttl * 1000);
} catch (err) {
console.error('[MetricSync] Failed to start:', err);
}
}
async function runSync() {
try {
const [sources] = await db.query('SELECT * FROM prometheus_sources WHERE type != "blackbox"');
if (sources.length === 0) return;
console.log(`[MetricSync] Syncing ${sources.length} sources at ${new Date().toLocaleTimeString()}`);
// 1. Sync individual source metrics (Overview & Detail)
await Promise.all(sources.map(async (source) => {
try {
const metrics = await prometheusService.getOverviewMetrics(source.url, source.name);
const enrichedMetrics = {
...metrics,
sourceName: source.name,
isOverview: !!source.is_overview_source,
isDetail: !!source.is_detail_source
};
const cacheKey = `source_metrics:${source.url}:${source.name}`;
await cache.set(cacheKey, enrichedMetrics, 86400); // Store for 24h, will be overwritten
} catch (err) {
console.error(`[MetricSync] Error syncing ${source.name}:`, err.message);
}
}));
// 2. Sync Network History (Optional but good for "real-time" feel)
const [historySources] = await db.query('SELECT * FROM prometheus_sources WHERE is_overview_source = 1 AND type != "blackbox"');
if (historySources.length > 0) {
const histories = await Promise.all(historySources.map(source =>
prometheusService.getNetworkHistory(source.url).catch(() => null)
));
const validHistories = histories.filter(h => h !== null);
if (validHistories.length > 0) {
const merged = prometheusService.mergeNetworkHistories(validHistories);
await cache.set('network_history_all', merged, 86400);
}
}
} catch (err) {
console.error('[MetricSync] Sync loop error:', err);
}
}
// --- Health API ---
app.get('/health', async (req, res) => {
@@ -953,7 +1030,7 @@ app.post('/api/settings', requireAuth, async (req, res) => {
page_name, show_page_name, title, logo_url, logo_url_dark, favicon_url,
default_theme, show_95_bandwidth, p95_type, require_login_for_server_details,
icp_filing, ps_filing, show_server_ip, ip_metric_name, ip_label_name, custom_metrics,
cdn_url
cdn_url, prometheus_cache_ttl
} = req.body;
// 3. Prepare parameters, prioritizing body but falling back to current
@@ -980,7 +1057,10 @@ app.post('/api/settings', requireAuth, async (req, res) => {
ip_metric_name: ip_metric_name !== undefined ? ip_metric_name : (current.ip_metric_name || null),
ip_label_name: ip_label_name !== undefined ? ip_label_name : (current.ip_label_name || 'address'),
custom_metrics: custom_metrics !== undefined ? JSON.stringify(custom_metrics) : (current.custom_metrics || '[]'),
cdn_url: cdn_url !== undefined ? cdn_url : (current.cdn_url || null)
cdn_url: cdn_url !== undefined ? cdn_url : (current.cdn_url || null),
prometheus_cache_ttl: prometheus_cache_ttl !== undefined
? Math.min(86400, Math.max(0, parseInt(prometheus_cache_ttl) || 0))
: (current.prometheus_cache_ttl !== undefined ? current.prometheus_cache_ttl : 30)
};
await db.query(`
@@ -989,8 +1069,8 @@ app.post('/api/settings', requireAuth, async (req, res) => {
default_theme, show_95_bandwidth, p95_type, require_login_for_server_details,
blackbox_source_id, latency_source, latency_dest, latency_target,
icp_filing, ps_filing, show_server_ip, ip_metric_name, ip_label_name,
custom_metrics, cdn_url
) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
custom_metrics, cdn_url, prometheus_cache_ttl
) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
page_name = VALUES(page_name),
show_page_name = VALUES(show_page_name),
@@ -1012,17 +1092,19 @@ app.post('/api/settings', requireAuth, async (req, res) => {
ip_metric_name = VALUES(ip_metric_name),
ip_label_name = VALUES(ip_label_name),
custom_metrics = VALUES(custom_metrics),
cdn_url = VALUES(cdn_url)`,
cdn_url = VALUES(cdn_url),
prometheus_cache_ttl = VALUES(prometheus_cache_ttl)`,
[
settings.page_name, settings.show_page_name, settings.title, settings.logo_url, settings.logo_url_dark, settings.favicon_url,
settings.default_theme, settings.show_95_bandwidth, settings.p95_type, settings.require_login_for_server_details,
settings.blackbox_source_id, settings.latency_source, settings.latency_dest, settings.latency_target,
settings.icp_filing, settings.ps_filing, settings.show_server_ip,
settings.ip_metric_name, settings.ip_label_name, settings.custom_metrics,
settings.cdn_url
settings.cdn_url, settings.prometheus_cache_ttl
]
);
startMetricSync();
res.json({ success: true, settings: getPublicSiteSettings(settings) });
} catch (err) {
console.error('Error updating settings:', err);
@@ -1049,30 +1131,17 @@ async function getOverview(force = false) {
servers: []
};
}
// If force is true, trigger an immediate sync run
if (force) {
await runSync();
}
// ONLY read from cache.
const allMetrics = await Promise.all(sources.map(async (source) => {
const cacheKey = `source_metrics:${source.url}:${source.name}`;
if (force) {
await cache.del(cacheKey);
} else {
const cached = await cache.get(cacheKey);
if (cached) return cached;
}
try {
const metrics = await prometheusService.getOverviewMetrics(source.url, source.name);
const enrichedMetrics = {
...metrics,
sourceName: source.name,
isOverview: !!source.is_overview_source,
isDetail: !!source.is_detail_source
};
await cache.set(cacheKey, enrichedMetrics, 15); // Cache for 15s
return enrichedMetrics;
} catch (err) {
console.error(`Error fetching metrics from ${source.name}:`, err.message);
return null;
}
const cached = await cache.get(cacheKey);
return cached || null;
}));
const validMetrics = allMetrics.filter(m => m !== null);
@@ -1234,31 +1303,11 @@ app.get('/api/metrics/network-history', async (req, res) => {
if (force) {
await cache.del(cacheKey);
} else {
const cached = await cache.get(cacheKey);
if (cached) return res.json(cached);
}
const cached = await cache.get(cacheKey);
if (cached) return res.json(cached);
const query = 'SELECT * FROM prometheus_sources WHERE is_overview_source = 1 AND type != "blackbox"';
const [sources] = await db.query(query);
if (sources.length === 0) {
return res.json({ timestamps: [], rx: [], tx: [] });
}
const histories = await Promise.all(sources.map(source =>
prometheusService.getNetworkHistory(source.url).catch(err => {
console.error(`Error fetching network history from ${source.name}:`, err.message);
return null;
})
));
const validHistories = histories.filter(h => h !== null);
if (validHistories.length === 0) {
return res.json({ timestamps: [], rx: [], tx: [] });
}
const merged = prometheusService.mergeNetworkHistories(validHistories);
await cache.set(cacheKey, merged, 300); // Cache for 5 minutes
res.json(merged);
// Fallback: If no cache, return empty instead of triggering Prometheus
return res.json({ timestamps: [], rx: [], tx: [] });
} catch (err) {
console.error('Error fetching network history history:', err);
res.status(500).json({ error: 'Failed to fetch network history history' });

View File

@@ -1,12 +1,15 @@
const axios = require('axios');
const http = require('http');
const https = require('https');
const cache = require('./cache'); // <-- ADD
const cache = require('./cache');
const crypto = require('crypto');
const QUERY_TIMEOUT = 10000;
// Reusable agents to handle potential redirect issues and protocol mismatches
const crypto = require('crypto');
function getCacheKey(type, baseUrl, expr, extra = '') {
return `prom_${type}:${crypto.createHash('md5').update(`${baseUrl}:${expr}:${extra}`).digest('hex')}`;
}
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });
@@ -951,5 +954,6 @@ module.exports = {
console.error(`[Prometheus] Error fetching latency for ${target}:`, err.message);
return null;
}
}
},
getCacheKey
};