perf(db): add statement_timeout; skip retry on pool Timeout errors

- 连接配置 statement_timeout(默认 30s,STATEMENT_TIMEOUT_SECS 可调),
  防慢查询长时间占用连接拖垮池(L6)
- get_conn 对 Timeout(池满)错误立即返回不再 sleep 重试,避免雪崩;
  仅 Backend/Postgres 错误才退避重试
This commit is contained in:
xfy 2026-06-18 13:35:33 +08:00
parent 179897ba6f
commit 1e2e3c9332
2 changed files with 22 additions and 5 deletions

View File

@ -42,6 +42,7 @@ RATE_LIMIT_UPLOAD_BURST=15
RATE_LIMIT_IMAGE_PER_SEC=10 RATE_LIMIT_IMAGE_PER_SEC=10
RATE_LIMIT_IMAGE_BURST=50 RATE_LIMIT_IMAGE_BURST=50
DB_POOL_SIZE=20 # database connection pool size DB_POOL_SIZE=20 # database connection pool size
STATEMENT_TIMEOUT_SECS=30 # per-query timeout; slow queries are canceled to protect the pool
SSR_CACHE_SECS=3600 # incremental SSR cache TTL SSR_CACHE_SECS=3600 # incremental SSR cache TTL
``` ```

View File

@ -15,10 +15,19 @@ use tokio_postgres::NoTls;
/// 最大连接数可通过 `DB_POOL_SIZE` 环境变量调整,默认 20。 /// 最大连接数可通过 `DB_POOL_SIZE` 环境变量调整,默认 20。
pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| { pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set");
let pg_cfg = db_url let mut pg_cfg = db_url
.parse::<tokio_postgres::Config>() .parse::<tokio_postgres::Config>()
.expect("Invalid DATABASE_URL format"); .expect("Invalid DATABASE_URL format");
// statement_timeout防止单条慢查询如全表扫搜索长时间占用连接拖垮池。
// 默认 30s可由 STATEMENT_TIMEOUT_SECS 覆盖L6
let statement_timeout_secs = std::env::var("STATEMENT_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(30);
// 通过 libpq options 传递 GUCtokio-postgres 在建连时执行。
pg_cfg.options(format!("-c statement_timeout={}", statement_timeout_secs * 1000));
// 使用 Fast 回收策略:归还连接时不额外发 SELECT 1 验证,直接复用。 // 使用 Fast 回收策略:归还连接时不额外发 SELECT 1 验证,直接复用。
// Verified 在高并发下会为每次 get() 增加一次往返Fast 依赖 tokio-postgres // Verified 在高并发下会为每次 get() 增加一次往返Fast 依赖 tokio-postgres
// 在使用时自然报错,由 get_conn 的重试层兜底。 // 在使用时自然报错,由 get_conn 的重试层兜底。
@ -44,8 +53,9 @@ pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
/// 从全局连接池获取一个数据库连接,失败时按指数退避 + jitter 重试。 /// 从全局连接池获取一个数据库连接,失败时按指数退避 + jitter 重试。
/// ///
/// 退避策略见 `retry::backoff_for`。jitter 使用 `rand` 生成 [0,1) 随机数, /// 退避策略见 `retry::backoff_for`。仅对 Backend/Postgres 错误DB 不可达)重试;
/// 避免多请求同步重试形成惊群。若所有尝试均失败,返回最后一次的 PoolError。 /// Timeout池满直接返回让上层限流兜底避免雪崩L6
/// 若所有重试均失败,返回最后一次的 PoolError。
pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::PoolError> { pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::PoolError> {
use rand::Rng; use rand::Rng;
@ -54,17 +64,23 @@ pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::
match DB_POOL.get().await { match DB_POOL.get().await {
Ok(conn) => return Ok(conn), Ok(conn) => return Ok(conn),
Err(e) => { Err(e) => {
// Timeout池满不重试快速失败让上层限流兜底避免雪崩。
// Backend/PostgresDB 不可达)才退避重试。
let is_timeout = matches!(e, deadpool_postgres::PoolError::Timeout(_));
last_err = Some(e); last_err = Some(e);
if attempt < crate::db::retry::MAX_RETRIES { if !is_timeout && attempt < crate::db::retry::MAX_RETRIES {
let jitter = rand::thread_rng().gen::<f64>(); let jitter = rand::thread_rng().gen::<f64>();
let delay = crate::db::retry::backoff_for(attempt, jitter); let delay = crate::db::retry::backoff_for(attempt, jitter);
tracing::warn!( tracing::warn!(
"DB connection attempt {} failed, retrying in {:?}: {:?}", "DB connection attempt {} failed (backend error), retrying in {:?}: {:?}",
attempt + 1, attempt + 1,
delay, delay,
last_err.as_ref().unwrap(), last_err.as_ref().unwrap(),
); );
tokio::time::sleep(delay).await; tokio::time::sleep(delay).await;
} else if is_timeout {
// 池满:立即返回,不再 sleep。
break;
} }
} }
} }