perf(gateway): 为 dashboard 接口添加短时响应缓存

新增 DashboardResponseCache,对 stats/daily_stats/provider_status
三个接口按用户维度缓存 15-30 秒,减少重复数据库查询。
This commit is contained in:
fawney19
2026-04-15 03:03:20 +08:00
parent 98ad1172b0
commit 67a2ca6e31
6 changed files with 118 additions and 8 deletions

View File

@@ -0,0 +1,28 @@
use std::time::Duration;
use aether_cache::ExpiringMap;
const MAX_ENTRIES: usize = 256;
#[derive(Debug)]
pub(crate) struct DashboardResponseCache {
entries: ExpiringMap<String, Vec<u8>>,
}
impl Default for DashboardResponseCache {
fn default() -> Self {
Self {
entries: ExpiringMap::new(),
}
}
}
impl DashboardResponseCache {
pub(crate) fn get(&self, key: &str, ttl: Duration) -> Option<Vec<u8>> {
self.entries.get_fresh(&key.to_string(), ttl)
}
pub(crate) fn insert(&self, key: String, value: Vec<u8>, ttl: Duration) {
self.entries.insert(key, value, ttl, MAX_ENTRIES);
}
}

View File

@@ -1,10 +1,12 @@
mod auth_api_key_last_used;
mod auth_context;
mod dashboard_response;
mod direct_plan_bypass;
mod scheduler_affinity;
pub(crate) use auth_api_key_last_used::AuthApiKeyLastUsedCache;
pub(crate) use auth_context::AuthContextCache;
pub(crate) use dashboard_response::DashboardResponseCache;
pub(crate) use direct_plan_bypass::DirectPlanBypassCache;
pub(crate) use scheduler_affinity::{
SchedulerAffinityCache, SchedulerAffinitySnapshotEntry, SchedulerAffinityTarget,

View File

@@ -565,6 +565,27 @@ pub(super) async fn handle_dashboard_stats_get(
Err(response) => return response,
};
let is_admin = dashboard_role_is_admin(&auth.user.role);
let cache_identity = if is_admin {
"admin"
} else {
auth.user.id.as_str()
};
let query_string = request_context
.request_query_string
.as_deref()
.unwrap_or("");
let cache_key = format!("stats:{cache_identity}:{query_string}");
let cache_ttl = std::time::Duration::from_secs(15);
if let Some(cached) = state.dashboard_response_cache.get(&cache_key, cache_ttl) {
return Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "application/json")
.body(Body::from(cached))
.unwrap_or_else(|_| http::StatusCode::INTERNAL_SERVER_ERROR.into_response());
}
let query = request_context.request_query_string.as_deref();
let summary_range = match dashboard_parse_stats_range(query) {
Ok(value) => value,
@@ -723,7 +744,7 @@ pub(super) async fn handle_dashboard_stats_get(
},
"token_breakdown": token_breakdown,
});
return Json(payload).into_response();
return dashboard_cached_json_response(state, cache_key, cache_ttl, &payload);
}
let wallet = if state.has_wallet_data_reader() {
@@ -801,7 +822,7 @@ pub(super) async fn handle_dashboard_stats_get(
"token_breakdown": token_breakdown,
"monthly_cost": dashboard_round_f64(period_totals.total_cost_usd, 4),
});
Json(payload).into_response()
dashboard_cached_json_response(state, cache_key, cache_ttl, &payload)
}
fn dashboard_daily_aggregate_record(
@@ -841,6 +862,27 @@ pub(super) async fn handle_dashboard_daily_stats_get(
Err(response) => return response,
};
let is_admin = dashboard_role_is_admin(&auth.user.role);
let cache_identity = if is_admin {
"admin"
} else {
auth.user.id.as_str()
};
let query_string = request_context
.request_query_string
.as_deref()
.unwrap_or("");
let cache_key = format!("daily:{cache_identity}:{query_string}");
let cache_ttl = std::time::Duration::from_secs(30);
if let Some(cached) = state.dashboard_response_cache.get(&cache_key, cache_ttl) {
return Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "application/json")
.body(Body::from(cached))
.unwrap_or_else(|_| http::StatusCode::INTERNAL_SERVER_ERROR.into_response());
}
let query = request_context.request_query_string.as_deref();
let range = match dashboard_parse_daily_range(query) {
Ok(value) => value,
@@ -1030,7 +1072,7 @@ pub(super) async fn handle_dashboard_daily_stats_get(
if let Some(provider_summary_payload) = provider_summary_payload {
payload["provider_summary"] = json!(provider_summary_payload);
}
Json(payload).into_response()
dashboard_cached_json_response(state, cache_key, cache_ttl, &payload)
}
pub(super) async fn handle_dashboard_recent_requests_get(
@@ -1134,6 +1176,23 @@ pub(super) async fn handle_dashboard_provider_status_get(
Ok(value) => value,
Err(response) => return response,
};
let cache_identity = if dashboard_role_is_admin(&auth.user.role) {
"admin"
} else {
auth.user.id.as_str()
};
let cache_key = format!("provider:{cache_identity}");
let cache_ttl = std::time::Duration::from_secs(20);
if let Some(cached) = state.dashboard_response_cache.get(&cache_key, cache_ttl) {
return Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "application/json")
.body(Body::from(cached))
.unwrap_or_else(|_| http::StatusCode::INTERNAL_SERVER_ERROR.into_response());
}
let providers = match state.list_provider_catalog_providers(true).await {
Ok(value) => value,
Err(err) => {
@@ -1209,7 +1268,8 @@ pub(super) async fn handle_dashboard_provider_status_get(
entries.truncate(limit);
}
Json(json!({ "providers": entries })).into_response()
let payload = json!({ "providers": entries });
dashboard_cached_json_response(state, cache_key, cache_ttl, &payload)
}
fn dashboard_parse_limit(
@@ -1248,6 +1308,23 @@ fn dashboard_bad_request_response(detail: String) -> Response<Body> {
.into_response()
}
fn dashboard_cached_json_response(
state: &AppState,
cache_key: String,
cache_ttl: std::time::Duration,
payload: &serde_json::Value,
) -> Response<Body> {
let bytes = serde_json::to_vec(payload).unwrap_or_default();
state
.dashboard_response_cache
.insert(cache_key, bytes.clone(), cache_ttl);
Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "application/json")
.body(Body::from(bytes))
.unwrap_or_else(|_| http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
fn dashboard_backend_unavailable_response(detail: &'static str) -> Response<Body> {
(
http::StatusCode::SERVICE_UNAVAILABLE,

View File

@@ -6,7 +6,8 @@ use aether_runtime::{ConcurrencyGate, DistributedConcurrencyGate};
use super::super::async_task::{VideoTaskPollerConfig, VideoTaskService};
use super::super::cache::{
AuthApiKeyLastUsedCache, AuthContextCache, DirectPlanBypassCache, SchedulerAffinityCache,
AuthApiKeyLastUsedCache, AuthContextCache, DashboardResponseCache, DirectPlanBypassCache,
SchedulerAffinityCache,
};
use super::super::data::GatewayDataState;
use super::super::fallback_metrics;
@@ -35,6 +36,7 @@ pub struct AppState {
pub(crate) oauth_refresh: Arc<provider_transport::LocalOAuthRefreshCoordinator>,
pub(crate) direct_plan_bypass_cache: Arc<DirectPlanBypassCache>,
pub(crate) scheduler_affinity_cache: Arc<SchedulerAffinityCache>,
pub(crate) dashboard_response_cache: Arc<DashboardResponseCache>,
pub(crate) fallback_metrics: Arc<fallback_metrics::GatewayFallbackMetrics>,
pub(crate) frontdoor_cors: Option<Arc<FrontdoorCorsConfig>>,
pub(crate) frontdoor_user_rpm: Arc<FrontdoorUserRpmLimiter>,

View File

@@ -22,8 +22,8 @@ use super::super::async_task::{
spawn_video_task_poller, VideoTaskPollerConfig, VideoTaskService, VideoTaskTruthSourceMode,
};
use super::super::cache::{
AuthApiKeyLastUsedCache, AuthContextCache, DirectPlanBypassCache, SchedulerAffinityCache,
SchedulerAffinitySnapshotEntry, SchedulerAffinityTarget,
AuthApiKeyLastUsedCache, AuthContextCache, DashboardResponseCache, DirectPlanBypassCache,
SchedulerAffinityCache, SchedulerAffinitySnapshotEntry, SchedulerAffinityTarget,
};
use super::super::data::{GatewayDataConfig, GatewayDataState};
use super::super::fallback_metrics;
@@ -144,6 +144,7 @@ impl AppState {
oauth_refresh: Arc::new(provider_transport::LocalOAuthRefreshCoordinator::new()),
direct_plan_bypass_cache: Arc::new(DirectPlanBypassCache::default()),
scheduler_affinity_cache: Arc::new(SchedulerAffinityCache::default()),
dashboard_response_cache: Arc::new(DashboardResponseCache::default()),
fallback_metrics: Arc::new(fallback_metrics::GatewayFallbackMetrics::default()),
frontdoor_cors: None,
frontdoor_user_rpm: Arc::new(FrontdoorUserRpmLimiter::new(