fix: ограничить пул Postgres и реже писать прогресс скана

- DB_MAX_OPEN_CONNS (25), DB_MAX_IDLE_CONNS (10), DB_CONN_MAX_LIFETIME_SEC (1800)

- Throttle UpdateScan прогресса в Runner (500ms), меньше одновременных запросов к БД

Устраняет FATAL too many clients при больших сканах и частом опросе GetScan

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 15:33:25 +10:00
parent 088be32ec5
commit 6ad59c9990
3 changed files with 73 additions and 10 deletions
+9
View File
@@ -57,6 +57,15 @@ func makeStore(cfg config.Config) (scans.Store, func()) {
log.Fatalf("db ping failed: %v", err) log.Fatalf("db ping failed: %v", err)
} }
db.SetMaxOpenConns(cfg.DBMaxOpenConns)
db.SetMaxIdleConns(cfg.DBMaxIdleConns)
if cfg.DBConnMaxLifetime > 0 {
db.SetConnMaxLifetime(cfg.DBConnMaxLifetime)
log.Printf("postgres pool: max_open=%d max_idle=%d conn_max_lifetime=%v", cfg.DBMaxOpenConns, cfg.DBMaxIdleConns, cfg.DBConnMaxLifetime)
} else {
log.Printf("postgres pool: max_open=%d max_idle=%d conn_max_lifetime=off", cfg.DBMaxOpenConns, cfg.DBMaxIdleConns)
}
if cfg.RunMigrations { if cfg.RunMigrations {
if err = store.RunMigrations(db); err != nil { if err = store.RunMigrations(db); err != nil {
_ = db.Close() _ = db.Close()
+44 -7
View File
@@ -2,7 +2,9 @@ package config
import ( import (
"os" "os"
"strconv"
"strings" "strings"
"time"
) )
type Config struct { type Config struct {
@@ -13,6 +15,10 @@ type Config struct {
SNMPEnabled bool SNMPEnabled bool
SNMPCommunity string SNMPCommunity string
ResetDBSecret string // NETTOPO_RESET_DB_SECRET — секрет для POST /api/admin/purge-scans ResetDBSecret string // NETTOPO_RESET_DB_SECRET — секрет для POST /api/admin/purge-scans
// Пул соединений к PostgreSQL (см. database/sql). По умолчанию ограничен, иначе при большом скане легко исчерпать max_connections на сервере.
DBMaxOpenConns int
DBMaxIdleConns int
DBConnMaxLifetime time.Duration
} }
func FromEnv() Config { func FromEnv() Config {
@@ -44,13 +50,44 @@ func FromEnv() Config {
snmpCommunity = "public" snmpCommunity = "public"
} }
maxOpen := intFromEnvMin("DB_MAX_OPEN_CONNS", 25, 1)
maxIdle := intFromEnvMin("DB_MAX_IDLE_CONNS", 10, 0)
if maxIdle > maxOpen {
maxIdle = maxOpen
}
connLife := 30 * time.Minute
if s := strings.TrimSpace(os.Getenv("DB_CONN_MAX_LIFETIME_SEC")); s != "" {
if sec, err := strconv.Atoi(s); err == nil {
if sec == 0 {
connLife = 0 // без ограничения времени жизни соединения
} else if sec > 0 {
connLife = time.Duration(sec) * time.Second
}
}
}
return Config{ return Config{
HTTPAddr: addr, HTTPAddr: addr,
DBDSN: dbDSN, DBDSN: dbDSN,
StoreBackend: storeBackend, StoreBackend: storeBackend,
RunMigrations: runMigrations, RunMigrations: runMigrations,
SNMPEnabled: snmpEnabled, SNMPEnabled: snmpEnabled,
SNMPCommunity: snmpCommunity, SNMPCommunity: snmpCommunity,
ResetDBSecret: strings.TrimSpace(os.Getenv("NETTOPO_RESET_DB_SECRET")), ResetDBSecret: strings.TrimSpace(os.Getenv("NETTOPO_RESET_DB_SECRET")),
DBMaxOpenConns: maxOpen,
DBMaxIdleConns: maxIdle,
DBConnMaxLifetime: connLife,
} }
} }
func intFromEnvMin(key string, def, min int) int {
s := strings.TrimSpace(os.Getenv(key))
if s == "" {
return def
}
n, err := strconv.Atoi(s)
if err != nil || n < min {
return def
}
return n
}
+20 -3
View File
@@ -163,11 +163,28 @@ func (r *Runner) run(ctx context.Context, job ScanJob) {
workCh := make(chan string) workCh := make(chan string)
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
pushProgress := func() { // Каждый UpdateScan в Postgres тянет GetScan+UPDATE; при сотнях воркеров без паузы исчерпывается пул соединений к БД.
const minProgressWriteInterval = 500 * time.Millisecond
var progWriteMu sync.Mutex
var lastProgressWrite time.Time
pushProgress := func(force bool) {
mu.Lock() mu.Lock()
q, f, u := quickDone, fullDone, up q, f, u := quickDone, fullDone, up
mu.Unlock() mu.Unlock()
prog := scanProgressBlend(q, f, total) prog := scanProgressBlend(q, f, total)
if !force {
progWriteMu.Lock()
now := time.Now()
if !lastProgressWrite.IsZero() && now.Sub(lastProgressWrite) < minProgressWriteInterval {
progWriteMu.Unlock()
return
}
lastProgressWrite = now
progWriteMu.Unlock()
}
_, _ = r.store.UpdateScan(job.ID, ScanUpdate{ _, _ = r.store.UpdateScan(job.ID, ScanUpdate{
Status: "running", Status: "running",
Progress: prog, Progress: prog,
@@ -207,7 +224,7 @@ func (r *Runner) run(ctx context.Context, job ScanJob) {
} }
quickDone++ quickDone++
mu.Unlock() mu.Unlock()
pushProgress() pushProgress(false)
if isUp && r.cfg.SNMPEnabled { if isUp && r.cfg.SNMPEnabled {
snmpRes, lldpRes, ifRes, portDevRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS) snmpRes, lldpRes, ifRes, portDevRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS)
@@ -232,7 +249,7 @@ func (r *Runner) run(ctx context.Context, job ScanJob) {
mu.Lock() mu.Lock()
fullDone++ fullDone++
mu.Unlock() mu.Unlock()
pushProgress() pushProgress(false)
} }
} }