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>
This commit is contained in:
@@ -100,6 +100,9 @@ func waitConnected(nc *nats.Conn, d time.Duration) bool {
|
||||
return nc.Status() == nats.CONNECTED
|
||||
}
|
||||
|
||||
// IsConnected 报告 NATS 连接此刻是否真的可用(供 readiness 探针;非仅启动时连过)。
|
||||
func (b *Bus) IsConnected() bool { return b.nc != nil && b.nc.IsConnected() }
|
||||
|
||||
// Close 关闭底层连接。
|
||||
func (b *Bus) Close() {
|
||||
if b.nc != nil {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 恒 200(liveness:进程能应答即存活);
|
||||
// - /readyz 由 ready() 决定 200/503(readiness:依赖就绪才导流)。
|
||||
//
|
||||
// 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))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// /healthz 恒 200;/readyz 随 ready() 翻 200/503。
|
||||
func TestHandler(t *testing.T) {
|
||||
ready := false
|
||||
h := Handler("test", func() bool { return ready })
|
||||
|
||||
do := func(path string) int {
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
return w.Code
|
||||
}
|
||||
|
||||
if c := do("/healthz"); c != 200 {
|
||||
t.Fatalf("/healthz 应恒 200,得 %d", c)
|
||||
}
|
||||
if c := do("/readyz"); c != 503 {
|
||||
t.Fatalf("未就绪时 /readyz 应 503,得 %d", c)
|
||||
}
|
||||
ready = true
|
||||
if c := do("/readyz"); c != 200 {
|
||||
t.Fatalf("就绪后 /readyz 应 200,得 %d", c)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user