fix(startup): normal 模式配置损坏 → fail-fast, 加 health error 字段 + metric

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 (待用户)
This commit is contained in:
sun
2026-08-30 22:43:07 +08:00
parent a655f8ae0e
commit 9e38566952
4 changed files with 38 additions and 3 deletions
+10
View File
@@ -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 }
+4 -2
View File
@@ -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"`
}
+16 -1
View File
@@ -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)
+8
View File
@@ -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。