优化后端的性能

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> <option value="max">出入取大 (Max)</option>
</select> </select>
</div> </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;"> <div class="form-group" style="margin-top: 15px;">
<label for="psFilingInput">公安备案号 (如:京公网安备 11010102000001号)</label> <label for="psFilingInput">公安备案号 (如:京公网安备 11010102000001号)</label>
<input type="text" id="psFilingInput" placeholder="请输入公安备案号"> <input type="text" id="psFilingInput" placeholder="请输入公安备案号">

View File

@@ -132,7 +132,8 @@
btnAddCustomMetric: document.getElementById('btnAddCustomMetric'), btnAddCustomMetric: document.getElementById('btnAddCustomMetric'),
btnSaveCustomMetrics: document.getElementById('btnSaveCustomMetrics'), btnSaveCustomMetrics: document.getElementById('btnSaveCustomMetrics'),
customDataContainer: document.getElementById('customDataContainer'), customDataContainer: document.getElementById('customDataContainer'),
cdnUrlInput: document.getElementById('cdnUrlInput') cdnUrlInput: document.getElementById('cdnUrlInput'),
prometheusCacheTtlInput: document.getElementById('prometheusCacheTtlInput')
}; };
// ---- State ---- // ---- State ----
@@ -655,6 +656,7 @@
if (dom.logoUrlDarkInput) dom.logoUrlDarkInput.value = window.SITE_SETTINGS.logo_url_dark || ''; 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.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.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 // Apply security dependency
updateSecurityDependency(); updateSecurityDependency();
@@ -2311,7 +2313,8 @@
ip_metric_name: dom.ipMetricNameInput ? dom.ipMetricNameInput.value.trim() : null, ip_metric_name: dom.ipMetricNameInput ? dom.ipMetricNameInput.value.trim() : null,
ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address', ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address',
custom_metrics: getCustomMetricsFromUI(), 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 { try {

View File

@@ -74,14 +74,15 @@ const SCHEMA = {
ip_label_name VARCHAR(100) DEFAULT 'address', ip_label_name VARCHAR(100) DEFAULT 'address',
custom_metrics JSON DEFAULT NULL, custom_metrics JSON DEFAULT NULL,
cdn_url VARCHAR(500) DEFAULT NULL, cdn_url VARCHAR(500) DEFAULT NULL,
prometheus_cache_ttl INT DEFAULT 30,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`, `,
seedSql: ` seedSql: `
INSERT IGNORE INTO site_settings ( 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 ( ) VALUES (
1, '数据可视化展示大屏', 1, '数据可视化展示大屏', 'dark', 0, 'tx', 1 1, '数据可视化展示大屏', 1, '数据可视化展示大屏', 'dark', 0, 'tx', 1, 30
) )
`, `,
columns: [ 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_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: '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: '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: { traffic_stats: {

View File

@@ -22,6 +22,7 @@ const fs = require('fs');
const crypto = require('crypto'); const crypto = require('crypto');
let isDbInitialized = false; let isDbInitialized = false;
let metricSyncTimer = null; // Background sync timer
const sessions = new Map(); // Fallback session store when Valkey is unavailable const sessions = new Map(); // Fallback session store when Valkey is unavailable
const requestBuckets = new Map(); const requestBuckets = new Map();
const SESSION_TTL_SECONDS = parseInt(process.env.SESSION_TTL_SECONDS, 10) || 86400; 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_metric_name: settings.ip_metric_name || null,
ip_label_name: settings.ip_label_name || 'address', ip_label_name: settings.ip_label_name || 'address',
custom_metrics: settings.custom_metrics || [], 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 --- // --- Health API ---
app.get('/health', async (req, res) => { 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, 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, 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, icp_filing, ps_filing, show_server_ip, ip_metric_name, ip_label_name, custom_metrics,
cdn_url cdn_url, prometheus_cache_ttl
} = req.body; } = req.body;
// 3. Prepare parameters, prioritizing body but falling back to current // 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_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'), 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 || '[]'), 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(` 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, default_theme, show_95_bandwidth, p95_type, require_login_for_server_details,
blackbox_source_id, latency_source, latency_dest, latency_target, blackbox_source_id, latency_source, latency_dest, latency_target,
icp_filing, ps_filing, show_server_ip, ip_metric_name, ip_label_name, icp_filing, ps_filing, show_server_ip, ip_metric_name, ip_label_name,
custom_metrics, cdn_url custom_metrics, cdn_url, prometheus_cache_ttl
) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
page_name = VALUES(page_name), page_name = VALUES(page_name),
show_page_name = VALUES(show_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_metric_name = VALUES(ip_metric_name),
ip_label_name = VALUES(ip_label_name), ip_label_name = VALUES(ip_label_name),
custom_metrics = VALUES(custom_metrics), 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.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.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.blackbox_source_id, settings.latency_source, settings.latency_dest, settings.latency_target,
settings.icp_filing, settings.ps_filing, settings.show_server_ip, settings.icp_filing, settings.ps_filing, settings.show_server_ip,
settings.ip_metric_name, settings.ip_label_name, settings.custom_metrics, 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) }); res.json({ success: true, settings: getPublicSiteSettings(settings) });
} catch (err) { } catch (err) {
console.error('Error updating settings:', err); console.error('Error updating settings:', err);
@@ -1049,30 +1131,17 @@ async function getOverview(force = false) {
servers: [] 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 allMetrics = await Promise.all(sources.map(async (source) => {
const cacheKey = `source_metrics:${source.url}:${source.name}`; const cacheKey = `source_metrics:${source.url}:${source.name}`;
if (force) { const cached = await cache.get(cacheKey);
await cache.del(cacheKey); return cached || null;
} 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 validMetrics = allMetrics.filter(m => m !== null); const validMetrics = allMetrics.filter(m => m !== null);
@@ -1234,31 +1303,11 @@ app.get('/api/metrics/network-history', async (req, res) => {
if (force) { if (force) {
await cache.del(cacheKey); await cache.del(cacheKey);
} else { } else {
const cached = await cache.get(cacheKey); const cached = await cache.get(cacheKey);
if (cached) return res.json(cached); if (cached) return res.json(cached);
}
const query = 'SELECT * FROM prometheus_sources WHERE is_overview_source = 1 AND type != "blackbox"'; // Fallback: If no cache, return empty instead of triggering Prometheus
const [sources] = await db.query(query); return res.json({ timestamps: [], rx: [], tx: [] });
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);
} catch (err) { } catch (err) {
console.error('Error fetching network history history:', err); console.error('Error fetching network history history:', err);
res.status(500).json({ error: 'Failed to fetch network history history' }); res.status(500).json({ error: 'Failed to fetch network history history' });

View File

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