fix(metrics): duplicate metrics collector registration panic(线上 CrashLoopBackOff)
=== 现象 ===
上一提交 b49677f 部署后平台直接 CrashLoopBackOff(5 次重启):
Initializing database...
panic: duplicate metrics collector registration attempted
lokistack/pkg/metrics.Register.func1() metrics.go:125
=== 根因 ===
prometheus/client_golang 在 init() 里已经把 GoCollector 与 ProcessCollector
注册进 **默认 registry**(prometheus.DefaultRegisterer)。我又对同一默认 registry
调用 MustRegister 注册了同一批 collector → 重复注册 → 运行时 panic。
致命点:go build / go vet / 普通单测**全都发现不了** ——
MustRegister 的重复检查只在运行期执行,只有真的调用 Register() 才会炸。
所以整个 P0 打磨在测试阶段是"全绿"的,一上线就挂。
=== 修法 ===
不再碰默认 registry,改用自有实例:
var registry = prometheus.NewRegistry() // 独立,不受 init() 影响
registry.MustRegister(...) // 注册全部指标
promhttp.HandlerFor(registry, ...) // 抓取读自有 registry
好处:完全掌控注册内容,未来再加 collector 不会与库的默认行为冲突。
=== 回归测试(这才是关键收获)===
新增 backend/pkg/metrics/metrics_test.go:
- TestRegisterIsIdempotent : 连续调用 Register 两次,必须不 panic
- TestHandlerExposesCustomMetrics : 断言抓取输出含 lokistack_* 与 go_goroutines
- TestRegisterConcurrent : 20 个 goroutine 并发调用不 panic
这三条在修复前必然失败,修复后全绿。以后再改指标定义会被它们拦住。
镜像:lghost/lokistack:latest + v0.1 = 2f8f3c85643e
This commit is contained in:
1 parent
b49677f6a6
commit
c44ffad61f
2 files changed
+79
-3
No files matched your search
@@ -116,13 +116,20 @@ var (
|
|||||||
[]string{"version", "go_version"},
|
[]string{"version", "go_version"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// registry 用独立实例,不用 prometheus.DefaultRegisterer。
|
||||||
|
// 原因:client_golang 在 init() 里已经把 GoCollector / ProcessCollector
|
||||||
|
// 注册进默认 registry,再用 MustRegister 注册同一批 collector 会
|
||||||
|
// **运行时 panic: duplicate metrics collector registration attempted**
|
||||||
|
// (编译期和单测都发现不了,只在进程启动时炸)。
|
||||||
|
registry = prometheus.NewRegistry()
|
||||||
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register 注册全部指标(幂等)。version 一般传 main.version。
|
// Register 注册全部指标(幂等)。version 一般传 main.version。
|
||||||
func Register(version string) {
|
func Register(version string) {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
prometheus.MustRegister(
|
registry.MustRegister(
|
||||||
HTTPRequestsTotal, HTTPRequestDuration, HTTPInFlight,
|
HTTPRequestsTotal, HTTPRequestDuration, HTTPInFlight,
|
||||||
DBSizeBytes, DBSaveDuration, DBSaveTotal, DBRecordsTotal,
|
DBSizeBytes, DBSaveDuration, DBSaveTotal, DBRecordsTotal,
|
||||||
DeploymentsTotal, CollectorsTotal,
|
DeploymentsTotal, CollectorsTotal,
|
||||||
@@ -135,8 +142,10 @@ func Register(version string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler 返回 Prometheus 抓取端点。
|
// Handler 返回 Prometheus 抓取端点(读自有 registry,不含默认 registry 的重复项)。
|
||||||
func Handler() http.Handler { return promhttp.Handler() }
|
func Handler() http.Handler {
|
||||||
|
return promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
|
||||||
|
}
|
||||||
|
|
||||||
// ── 埋点辅助(各层直接调) ──
|
// ── 埋点辅助(各层直接调) ──
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 回归:Register 必须幂等。
|
||||||
|
// 线上事故:client_golang 在 init() 里已把 GoCollector / ProcessCollector
|
||||||
|
// 注册进默认 registry,我们又 MustRegister 到默认 registry → 进程启动即
|
||||||
|
// panic: duplicate metrics collector registration attempted。
|
||||||
|
// 这个 bug 编译期和普通单测都发现不了(只有真的调用 Register 才炸),
|
||||||
|
// 所以这里必须实际调用两次。
|
||||||
|
func TestRegisterIsIdempotent(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Fatalf("Register 触发 panic(重复注册):%v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
Register("test")
|
||||||
|
Register("test") // 第二次必须安全返回
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抓取端点必须能输出我们自己定义的指标
|
||||||
|
func TestHandlerExposesCustomMetrics(t *testing.T) {
|
||||||
|
Register("test")
|
||||||
|
ObserveHTTP("GET", "/api/v1/deployments", 200, 0.01)
|
||||||
|
ObserveDBSave(0.002, true)
|
||||||
|
ObserveAlertEvaluation(0.3, true)
|
||||||
|
ObserveLokiQuery(true)
|
||||||
|
DBRecordsTotal.WithLabelValues("deployments").Set(3)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||||
|
Handler().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(rec.Body)
|
||||||
|
out := string(body)
|
||||||
|
for _, want := range []string{
|
||||||
|
"lokistack_http_requests_total",
|
||||||
|
"lokistack_db_save_duration_seconds",
|
||||||
|
"lokistack_alert_evaluations_total",
|
||||||
|
"lokistack_loki_queries_total",
|
||||||
|
"lokistack_db_records",
|
||||||
|
"go_goroutines", // GoCollector 生效
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Fatalf("抓取结果缺少指标 %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发调用不能 panic(sync.Once 保护)
|
||||||
|
func TestRegisterConcurrent(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
Register("test")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user