fix(store): 数据库连接池限 25 条并设语句与空闲事务超时,迁移锁固定在取锁的连接上释放,API 导出连接池指标

This commit is contained in:
Lemon-miaow committed 2026-09-25 13:25:45 +08:00
1 parent fde677c07e
commit 88d3dd7121
7 files changed
+348 -12

No files matched your search

+13 -3
View File
@@ -1,6 +1,7 @@
package api
import (
"database/sql"
"net/http"
"felis.lolicon.best/internal/metrics"
@@ -15,10 +16,12 @@ import (
// build reconcile loop lives in cmd/felis.reconcileBuilds), so this endpoint is
// that counter's sole scrape path; the operator's :8080 carries the fleet
// gauges instead.
var apiMetricsHandler = newAPIMetricsHandler()
var (
apiRegistry = prometheus.NewRegistry()
apiMetricsHandler = newAPIMetricsHandler(apiRegistry)
)
func newAPIMetricsHandler() http.Handler {
reg := prometheus.NewRegistry()
func newAPIMetricsHandler(reg *prometheus.Registry) http.Handler {
// A fresh registry cannot already hold a collector, so Register's
// AlreadyRegistered tolerance arm never triggers here.
_ = metrics.Register(reg)
@@ -31,6 +34,13 @@ func newAPIMetricsHandler() http.Handler {
return promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
}
// RegisterStorePool adds the store pool's go_sql_* series (db_name="felis"):
// connections open, in use and idle against the cap, and how often and how
// long requests waited for one, which is where a pool pinned at its cap shows.
func RegisterStorePool(db *sql.DB) error {
return apiRegistry.Register(collectors.NewDBStatsCollector(db, "felis"))
}
// handleMetrics is mounted as a Public route on the internal face (the same
// stance as the health probes): the internal listener is ClusterIP-only and a
// Prometheus scrape carries no token. The external face never serves metrics.
+26
View File
@@ -1,11 +1,14 @@
package api
import (
"database/sql"
"net/http"
"strings"
"testing"
"felis.lolicon.best/internal/metrics"
_ "github.com/jackc/pgx/v5/stdlib"
)
// TestMetricsEndpoint locks the scrape surface: the internal face serves the
@@ -42,3 +45,26 @@ func TestMetricsEndpoint(t *testing.T) {
t.Fatalf("external GET /metrics = %d, want 404 (metrics stay internal)", w.Code)
}
}
// The store pool's counts reach the internal scrape once registered.
func TestMetricsEndpointCarriesTheStorePool(t *testing.T) {
pool, err := sql.Open("pgx", "postgres://[email protected]:1/felis")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
pool.SetMaxOpenConns(7)
if err := RegisterStorePool(pool); err != nil {
t.Fatal(err)
}
w := do((&API{}).InternalHandler(), "GET", "/metrics", "", nil)
for _, want := range []string{
`go_sql_max_open_connections{db_name="felis"} 7`,
`go_sql_wait_count_total{db_name="felis"} 0`,
} {
if !strings.Contains(w.Body.String(), want) {
t.Errorf("metrics exposition missing %q", want)
}
}
}