优化后端的性能

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

@@ -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' });