perf(postgres): 分离 API 池并降低连接预热

生产同构千并发请求在干净队列上复现 API Pool 31/32、idle=1 且 canceled acquire 持续增长,Worker Pool 仅使用少量连接,确认瓶颈位于 API 数据库连接预算而非 Worker 执行槽。

新增可配置的 AI_GATEWAY_DATABASE_MIN_IDLE_CONNS,生产仅预热 4 条连接;API Pool 独立固定为 48,Worker Pool 继续跟随 P24/P28/P32 的 32/36/40 档位。发布工具、Kubernetes 清单、验收门禁和文档同步更新。

验证:Go 全量测试、gofmt、bash -n、ShellCheck、kubectl kustomize 和 git diff --check 通过。
This commit is contained in:
2026-07-31 03:28:43 +08:00
parent 2606cc8223
commit 2529679b67
11 changed files with 78 additions and 23 deletions
+6
View File
@@ -59,6 +59,7 @@ type Config struct {
BillingEngineMode string
ProcessRole string
DatabaseMaxConns int
DatabaseMinIdleConns int
DatabaseIdleInTransactionTimeoutSeconds int
DatabaseLockTimeoutSeconds int
MediaRequestConcurrency int
@@ -122,6 +123,7 @@ func Load() Config {
BillingEngineMode: strings.ToLower(env("BILLING_ENGINE_MODE", "observe")),
ProcessRole: strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_PROCESS_ROLE", "all"))),
DatabaseMaxConns: envInt("AI_GATEWAY_DATABASE_MAX_CONNS", 0),
DatabaseMinIdleConns: envOptionalIntValidated("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", 0),
DatabaseIdleInTransactionTimeoutSeconds: envOptionalIntValidated(
"AI_GATEWAY_DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_SECONDS",
@@ -147,6 +149,10 @@ func (c Config) Validate() error {
if c.DatabaseMaxConns < 0 || c.DatabaseMaxConns > 1000 {
return errors.New("AI_GATEWAY_DATABASE_MAX_CONNS must be between 1 and 1000 when configured")
}
if c.DatabaseMinIdleConns < 0 || c.DatabaseMinIdleConns > 1000 ||
(c.DatabaseMaxConns > 0 && c.DatabaseMinIdleConns > c.DatabaseMaxConns) {
return errors.New("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS must be between 0 and AI_GATEWAY_DATABASE_MAX_CONNS")
}
if c.DatabaseIdleInTransactionTimeoutSeconds < 0 || c.DatabaseIdleInTransactionTimeoutSeconds > 3600 {
return errors.New("AI_GATEWAY_DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_SECONDS must be between 0 and 3600")
}
+15 -2
View File
@@ -144,9 +144,12 @@ func TestProcessRolePrecedenceAndCompatibility(t *testing.T) {
func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
cfg := Load()
if cfg.DatabaseIdleInTransactionTimeoutSeconds != 60 || cfg.DatabaseLockTimeoutSeconds != 30 {
if cfg.DatabaseMinIdleConns != 0 ||
cfg.DatabaseIdleInTransactionTimeoutSeconds != 60 ||
cfg.DatabaseLockTimeoutSeconds != 30 {
t.Fatalf(
"database transaction timeouts = %d/%d, want 60/30",
"database pool minimum/transaction timeouts = %d/%d/%d, want 0/60/30",
cfg.DatabaseMinIdleConns,
cfg.DatabaseIdleInTransactionTimeoutSeconds,
cfg.DatabaseLockTimeoutSeconds,
)
@@ -161,6 +164,16 @@ func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
t.Fatalf("Validate() error = %v, want invalid database max conns", err)
}
cfg.DatabaseMaxConns = 16
cfg.DatabaseMinIdleConns = 17
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "DATABASE_MIN_IDLE_CONNS") {
t.Fatalf("Validate() error = %v, want invalid database min idle conns", err)
}
cfg.DatabaseMinIdleConns = 4
t.Setenv("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", "not-an-integer")
if err := Load().Validate(); err == nil || !strings.Contains(err.Error(), "DATABASE_MIN_IDLE_CONNS") {
t.Fatalf("Validate() error = %v, want invalid non-integer database min idle conns", err)
}
t.Setenv("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", "4")
cfg.DatabaseIdleInTransactionTimeoutSeconds = 3601
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "IDLE_IN_TRANSACTION") {
t.Fatalf("Validate() error = %v, want invalid idle transaction timeout", err)
+2 -2
View File
@@ -27,11 +27,11 @@ type Store struct {
const (
postgresApplicationName = "easyai-ai-gateway"
postgresConnectTimeout = 5 * time.Second
postgresPoolWarmLimit = 64
)
type PostgresPoolOptions struct {
MaxConns int
MinIdleConns int
IdleInTransactionTimeout time.Duration
LockTimeout time.Duration
}
@@ -130,8 +130,8 @@ func postgresPoolConfigWithOptions(databaseURL string, options PostgresPoolOptio
}
if options.MaxConns > 0 {
config.MaxConns = int32(options.MaxConns)
config.MinIdleConns = int32(min(options.MaxConns, postgresPoolWarmLimit))
}
config.MinIdleConns = int32(min(max(options.MinIdleConns, 0), int(config.MaxConns)))
return config, nil
}
+11 -10
View File
@@ -27,7 +27,7 @@ func TestPostgresPoolConfigRejectsMalformedURL(t *testing.T) {
}
}
func TestPostgresPoolConfigWarmsBoundedIdleConnections(t *testing.T) {
func TestPostgresPoolConfigUsesExplicitMinimumIdleConnections(t *testing.T) {
config, err := postgresPoolConfig(
"postgresql://gateway:password@localhost:5432/gateway?sslmode=disable",
24,
@@ -35,23 +35,23 @@ func TestPostgresPoolConfigWarmsBoundedIdleConnections(t *testing.T) {
if err != nil {
t.Fatalf("parse PostgreSQL pool config: %v", err)
}
if config.MaxConns != 24 || config.MinIdleConns != 24 {
if config.MaxConns != 24 || config.MinIdleConns != 0 {
t.Fatalf(
"pool bounds max=%d minIdle=%d, want 24/24",
"pool bounds max=%d minIdle=%d, want 24/0",
config.MaxConns,
config.MinIdleConns,
)
}
small, err := postgresPoolConfig(
warmed, err := postgresPoolConfigWithOptions(
"postgresql://gateway:password@localhost:5432/gateway?sslmode=disable",
4,
PostgresPoolOptions{MaxConns: 24, MinIdleConns: 4},
)
if err != nil {
t.Fatalf("parse small PostgreSQL pool config: %v", err)
t.Fatalf("parse warmed PostgreSQL pool config: %v", err)
}
if small.MinIdleConns != 4 {
t.Fatalf("small pool minIdle=%d, want 4", small.MinIdleConns)
if warmed.MinIdleConns != 4 {
t.Fatalf("warmed pool minIdle=%d, want 4", warmed.MinIdleConns)
}
}
@@ -60,6 +60,7 @@ func TestPostgresPoolConfigSetsBoundedTransactionTimeouts(t *testing.T) {
"postgresql://gateway:password@localhost:5432/gateway?sslmode=disable",
PostgresPoolOptions{
MaxConns: 32,
MinIdleConns: 4,
IdleInTransactionTimeout: 60 * time.Second,
LockTimeout: 30 * time.Second,
},
@@ -73,8 +74,8 @@ func TestPostgresPoolConfigSetsBoundedTransactionTimeouts(t *testing.T) {
if got := config.ConnConfig.RuntimeParams["lock_timeout"]; got != "30000ms" {
t.Fatalf("lock timeout = %q, want 30000ms", got)
}
if config.MaxConns != 32 || config.MinIdleConns != 32 {
t.Fatalf("pool bounds max=%d minIdle=%d, want 32/32", config.MaxConns, config.MinIdleConns)
if config.MaxConns != 32 || config.MinIdleConns != 4 {
t.Fatalf("pool bounds max=%d minIdle=%d, want 32/4", config.MaxConns, config.MinIdleConns)
}
}