Files
Blizzard da9c76f073 feat(prod): 实时就绪探针——gateway /readyz 真 ping + dispatcher/mcp-go 加 HTTP 探针(B2)
此前 gateway /readyz 用的是 Enabled() 启动期降级标志(反映不了运行中 PG 掉线,LB 会继续
往已不可用实例导流);dispatcher/mcp-go 干脆没有 HTTP 探针(只有 NATS ServeHealth,k8s/LB
够不着、只能靠 gateway 经 NATS 代探)。

- gateway /readyz 改用 db.Ping 实时探活。只把 **DB 当硬依赖门**:Redis 掉线仍可服务(限流有
  A5 进程内 fail-safe 兜底、SSE 回落 live NATS),一 blip 就把全部实例踢出轮转反而制造整站
  故障,故 Redis 只上报不 gate。NATS 启动即连(fatal)不单列。
- 新 sundynix-shared/health 包:Serve/Handler 提供 /healthz(恒 200 liveness) + /readyz(由
  ready() 决定 readiness)。dispatcher(:8091, DISPATCHER_HEALTH_ADDR)与 mcp-go(:8092,
  MCP_GO_HEALTH_ADDR)各起一个,readiness = NATS 连接可用(bus.IsConnected)。接入各自优雅停机。
- bus 加 IsConnected()(nc.IsConnected 实时);dispatcher Subscriber 透传。

四模块 build+vet+test 绿;health 带 Handler 单测。探针端口仅内部用(对外仍只暴露 gateway)。
compose/k8s 的 healthcheck 编排属 C 层 ops,另做。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:50:31 +08:00

55 lines
2.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package health 提供轻量 HTTP 健康探针,给无 HTTP 端口的后端服务(dispatcher / mcp-go
// 补上 k8s/LB 能直接探的 /healthz(liveness) 与 /readyz(readiness)。
//
// 此前这两个服务只有 NATS ServeHealth 应答器,编排器无法对它们做 HTTP 探测、只能靠 gateway
// 经 NATS 代探。这里给它们各起一个极小的 HTTP 服务。
package health
import (
"context"
"log"
"net/http"
"time"
)
// Serve 起一个健康探针 HTTP 服务:
// - /healthz 恒 200liveness:进程能应答即存活);
// - /readyz 由 ready() 决定 200/503readiness:依赖就绪才导流)。
//
// addr 为空则不启动、返回 no-op(本地无端口需求时)。返回 shutdown 供优雅停机调用。
func Serve(service, addr string, ready func() bool) func(context.Context) {
if addr == "" {
return func(context.Context) {}
}
srv := &http.Server{Addr: addr, Handler: Handler(service, ready), ReadHeaderTimeout: 5 * time.Second}
go func() {
log.Printf("[%s] health probe on %s (/healthz /readyz)", service, addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("[%s] health probe listen: %v", service, err)
}
}()
return func(ctx context.Context) { _ = srv.Shutdown(ctx) }
}
// Handler 返回探针路由(/healthz 恒 200/readyz 由 ready 决定),便于单测与自定义挂载。
func Handler(service string, ready func() bool) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, `{"status":"ok","service":"`+service+`"}`)
})
mux.HandleFunc("/readyz", func(w http.ResponseWriter, _ *http.Request) {
if ready == nil || ready() {
writeJSON(w, http.StatusOK, `{"status":"ready"}`)
return
}
writeJSON(w, http.StatusServiceUnavailable, `{"status":"not_ready"}`)
})
return mux
}
func writeJSON(w http.ResponseWriter, code int, body string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_, _ = w.Write([]byte(body))
}