From 9e3856695239d42d467b227df28971398bf23e60 Mon Sep 17 00:00:00 2001 From: "3371392206@qq.com" <3371392206@qq.com> Date: Sun, 30 Aug 2026 22:43:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(startup):=20normal=20=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8D=9F=E5=9D=8F=20=E2=86=92=20fail-fast,?= =?UTF-8?q?=20=E5=8A=A0=20health=20error=20=E5=AD=97=E6=AE=B5=20+=20metric?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 报告 (用户排查后提出): - main.go:61-68 在 normal 模式下 LoadRuntimeConfig 失败只打 log.Printf - TTSConfigErr 被设, /v1/audio/speech 返 503 - 但服务进程继续跑, 监听端口 - 运维看到"服务在跑", 排查成本高 - /health 虽然 503 + body 有 config_error:true, 但普通用户不会看 body - 没 metric 计数, 监控告警配不出来 修法 (P0+P1+P2 一起): P0 - main.go: mode 区分 - setup mode + 失败 = WARN (没装正常) - normal mode + 失败 = log.Fatalf (进程退出, K8s 拉起, 触发告警) P1 - dto/health.go + controller/tts.go: - ConfigStatusResponse 加 Error 字段 (omitempty) - 错误时填 setting.TTSConfigErr.Error() - /health body 运维一眼看出原因 P2 - metrics/metrics.go: - 加 tts_config_load_failures_total counter, labeled by mode - 不管 fail-fast 还是 warn 都计数, 但 mode 区分 - Prometheus 告警: rate(tts_config_load_failures_total{mode="normal"}[5m]) > 0 e2e 验证 (本机, 3 场景): - 空 DB / setup 模式 → /health 200 + body error 字段 + 进程不退出 ✓ - 装好 OK / normal 模式 → /health 200, body 无 error 字段 ✓ - 装完 + default_resource_id='' (损坏) / normal 模式 → log 出现 [FATAL] "service cannot start in normal mode..." 进程退出, K8s 拉起会循环触发, 直到 DB 修好 ✓ 副作用: - fail-fast 后, 自愈回退逻辑不变 (installer.Detect 仍把损坏 db 备份 + 转 setup mode), 但用户已正常装过的 db 不会被自愈删除 - 不影响启动日志, LogStartupSummary() 仍照常打印 - 不影响 setup 模式, 该 WARN 还是 WARN 未 push (待用户) --- controller/tts.go | 10 ++++++++++ dto/health.go | 6 ++++-- main.go | 17 ++++++++++++++++- metrics/metrics.go | 8 ++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/controller/tts.go b/controller/tts.go index ec5fa09..4b103f3 100644 --- a/controller/tts.go +++ b/controller/tts.go @@ -305,6 +305,7 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) { ConfigStatus: dto.ConfigStatusResponse{ AllRequiredVarsSet: allRequired, ConfigError: setting.TTSConfigErr != nil, + Error: configErrorMessage(setting.TTSConfigErr), }, Installed: mode == installer.ModeNormal, Mode: mode.String(), @@ -312,6 +313,15 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(resp) } +// configErrorMessage 把 setting.TTSConfigErr 安全地转成可对外暴露的字符串。 +// 仅在 normal 模式且有错时调用, error 为 nil 时返 "" (被 omitempty 跳过)。 +func configErrorMessage(err error) string { + if err == nil { + return "" + } + return err.Error() +} + var startTime time.Time func SetStartTime(t time.Time) { startTime = t } diff --git a/dto/health.go b/dto/health.go index cdc7bd4..8236887 100644 --- a/dto/health.go +++ b/dto/health.go @@ -18,6 +18,8 @@ type HealthResponse struct { } type ConfigStatusResponse struct { - AllRequiredVarsSet bool `json:"all_required_vars_set"` - ConfigError bool `json:"config_error"` + AllRequiredVarsSet bool `json:"all_required_vars_set"` + ConfigError bool `json:"config_error"` + // Error 仅在 ConfigError=true 时填充, 给出原始 error message, 方便运维 / curl 一眼看出原因 + Error string `json:"error,omitempty"` } diff --git a/main.go b/main.go index 0120ac3..5612b46 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/volcano-tts/tts-api/middleware" "github.com/volcano-tts/tts-api/router" "github.com/volcano-tts/tts-api/setting" + "github.com/volcano-tts/tts-api/telemetry" ) // ttsDBPath 返回数据库/lock 所在路径;空时落到当前目录的 tts.db。 @@ -58,9 +59,23 @@ func main() { // 4) M3: 从 store 加载运行时 TTS 配置(替代原来的 env-based InitTTSConfig) // 必须在 LogStartupSummary 之前,这样日志显示的是真实状态(API key 已从 DB 加载,不再读 env) + // + // 模式区分: + // - setup 模式 + 失败 = 正常(还没装), 警告即可 + // - normal 模式 + 失败 = 致命(已装但配置坏), fail-fast 让 K8s/进程管理器拉起 if st != nil { if err := setting.LoadRuntimeConfig(st); err != nil { - log.Printf("[main] TTS 运行时配置加载失败:%v", err) + mode := installer.GetMode() + modeName := "setup" + if mode == installer.ModeNormal { + modeName = "normal" + } + metrics.ConfigLoadFailures.Inc(telemetry.Labels{"mode": modeName}) + if mode == installer.ModeNormal { + log.Printf("[main][FATAL] TTS 运行时配置加载失败 (normal mode, 服务无法启动): %v", err) + log.Fatalf("service cannot start in normal mode without valid config: %v", err) + } + log.Printf("[main][WARN] TTS 运行时配置加载失败 (setup mode, 需先 /setup): %v", err) } else { log.Printf("[main] TTS 运行时配置已加载(api_key=***, speaker=%s, resource=%s, format=%s)", setting.TTSOptions.Speaker, setting.TTSOptions.ResourceID, setting.TTSOptions.Format) diff --git a/metrics/metrics.go b/metrics/metrics.go index 7fbd623..35828cb 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -34,6 +34,9 @@ var ( ConcurrencyRejected *telemetry.Counter RateLimitRejected *telemetry.Counter AuthFailed *telemetry.Counter + + // 启动期配置加载失败(仅 normal 模式可能触发,触发后服务 fail-fast) + ConfigLoadFailures *telemetry.Counter ) // Init 初始化所有指标。在 main 启动期调用一次。 @@ -107,6 +110,11 @@ func Init() { "tts_auth_failed_total", "Requests rejected due to invalid/missing API key.", ) + ConfigLoadFailures = m.NewCounter( + "tts_config_load_failures_total", + "TTS runtime config load failures during startup. labeled by mode (normal/setup).", + "mode", + ) } // AdapterRecorder 把 telemetry 指标适配为 volcano.MetricsRecorder。