Files
Volcano-Engine-TTS-UI/main.go
T
sun 9e38566952 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 (待用户)
2026-08-30 22:43:07 +08:00

140 lines
4.5 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/volcano-tts/tts-api/controller"
"github.com/volcano-tts/tts-api/installer"
"github.com/volcano-tts/tts-api/metrics"
"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。
func ttsDBPath() string {
if p := os.Getenv("TTS_DB_PATH"); p != "" {
return p
}
return "tts.db"
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.SetPrefix("[TTS-Server] ")
// 1) 加载引导环境变量(PORT / TTS_ADMIN_KEY / OPENAI_TTS_API_KEY 兜底)
// 注: Auth.APIKeys 在 InitAllConfigs 里先读 env,后被 LoadRuntimeConfig 覆盖为 DB 值。
setting.InitAllConfigs()
metrics.Init()
middleware.InitRateLimiter()
// 2) 启动期关键步骤:打开/建库 → 检测 lock → 判定模式
dbPath := ttsDBPath()
if err := installer.EnsureDBDir(dbPath); err != nil {
log.Fatalf("FATAL: cannot create db dir: %v", err)
}
st, res, err := installer.Detect(dbPath)
if err != nil {
log.Fatalf("FATAL: installer detect failed: %v", err)
}
if res.Corrupted {
log.Printf("[main] 注意: 启动时检测到 db 损坏并已自愈回退(备份=%s)", res.BackupTo)
}
// 3) 注入 setup/admin 控制器需要的句柄(M1+M2)
controller.SetSetupState(st, dbPath)
controller.SetAdminStore(st)
controller.SetMetricsTextWriter(func(w http.ResponseWriter) error {
metrics.Meter.Handler().ServeHTTP(w, &http.Request{})
return nil
})
// 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 {
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)
}
}
// 5) 启动摘要日志(此时 Auth.APIKeys 已是 DB 值,日志反映真实状态)
setting.LogStartupSummary()
log.Printf("[main] 当前模式: %s (db=%s lock=%s)", res.Mode, dbPath, res.LockPath)
controller.InitController()
controller.SetStartTime(time.Now())
r := router.Setup()
server := &http.Server{
Addr: ":" + setting.Server.Port,
Handler: middleware.CORS(r),
ReadTimeout: 30 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 60 * time.Second,
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
if installer.GetMode() == installer.ModeSetup {
log.Printf("Starting TTS Server in SETUP mode")
log.Printf("Open browser to http://localhost:%s/setup to install", setting.Server.Port)
} else {
log.Printf("Starting ByteDance TTS to OpenAI API Adapter Server")
log.Printf("Listening on port: %s", setting.Server.Port)
log.Printf("OpenAI TTS endpoint: http://localhost:%s/v1/audio/speech", setting.Server.Port)
log.Printf("Admin WebUI: http://localhost:%s/admin", setting.Server.Port)
}
log.Printf("Health check: http://localhost:%s/health", setting.Server.Port)
log.Printf("Metrics: http://localhost:%s/metrics", setting.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
<-quit
log.Println("Shutting down server...")
// 关闭 db 连接(仅当 st 非 nil 时)
if st != nil {
_ = st.Close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
} else {
log.Println("Server exited gracefully")
}
}