feat(monitor): NATS 集群 Raft 副本健康 + 基建 ping 延迟(监测组完善)

此前 /status 把 NATS 当一盏二元灯(连不上就 fatal 故恒真),看不出 3 节点集群里
哪个节点掉了、JetStream 持久流的 Raft 副本是否还齐(计费/状态/评测流不丢的关键)。
DB/Redis/MinIO 也只二元 ping、无延迟。

- bus.ClusterStatus:连的节点名 + 集群发现节点数(nc.Servers) + RTT(nc.RTT) + 6 条关键
  持久流(tasks/status/usage/eval/ingest/approvals)的 Raft 副本健康(leader + healthy/total,
  单节点部署记 1/1;某节点掉队 → healthy<total 记降级)。
- /status 新增 nats 集群对象 + NATS 灯改为'连接且无副本降级才绿'、detail 显示'N 节点·连 X·
  流副本齐全/降级'、latency=RTT;PG/Redis/MinIO 加 ping 往返耗时。
- admin StatusPage:新增 NATS 集群面板(节点/RTT/连接 + 各流 leader/副本健康/消息数表),
  基建行显示延迟。

sundynix-shared/gateway build+vet+test 绿;admin tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-21 16:15:22 +08:00
parent a42f30239f
commit 9b153871eb
5 changed files with 217 additions and 14 deletions
+72
View File
@@ -103,6 +103,78 @@ func waitConnected(nc *nats.Conn, d time.Duration) bool {
// IsConnected 报告 NATS 连接此刻是否真的可用(供 readiness 探针;非仅启动时连过)。
func (b *Bus) IsConnected() bool { return b.nc != nil && b.nc.IsConnected() }
// StreamHealth 是一条 JetStream 流的 Raft 副本健康。单节点部署 Cluster 为空 → 记 1/1。
type StreamHealth struct {
Name string `json:"name"`
Leader string `json:"leader"`
ReplicasHealthy int `json:"replicas_healthy"`
ReplicasTotal int `json:"replicas_total"`
Messages uint64 `json:"messages"`
}
// NATSClusterStatus 是 NATS 骨干网的实时集群视图(供监测面板:不再是一盏二元灯)。
type NATSClusterStatus struct {
Connected bool `json:"connected"`
ConnectedTo string `json:"connected_to"` // 当前连的节点名
KnownServers int `json:"known_servers"` // 集群发现到的节点数(3 节点集群应为 3)
RTTMillis int `json:"rtt_ms"` // 到当前节点的往返耗时
Streams []StreamHealth `json:"streams"` // 关键持久流的副本健康
Degraded int `json:"degraded"` // 副本未满(有节点掉队)的流数
}
// clusterStreams 是要体检的关键持久流(计费/状态/评测/任务/入库/审批都不能丢)。
var clusterStreams = []string{
contract.StreamTasks, contract.StreamStatus, contract.StreamUsage,
contract.StreamEval, contract.StreamIngest, contract.StreamApprovals,
}
// ClusterStatus 探 NATS 集群实时状态:连的节点、已知节点数、RTT,以及各关键流的 Raft 副本健康。
// 3 节点集群里某节点掉线 → 对应流 ReplicasHealthy < Total(仍有 quorum 可服务,但已降级须告警)。
func (b *Bus) ClusterStatus(ctx context.Context) NATSClusterStatus {
st := NATSClusterStatus{}
if b.nc == nil {
return st
}
st.Connected = b.nc.IsConnected()
st.ConnectedTo = b.nc.ConnectedServerName()
st.KnownServers = len(b.nc.Servers())
if rtt, err := b.nc.RTT(); err == nil {
st.RTTMillis = int(rtt.Milliseconds())
}
for _, name := range clusterStreams {
s, err := b.js.Stream(ctx, name)
if err != nil {
continue
}
info, err := s.Info(ctx)
if err != nil {
continue
}
sh := StreamHealth{Name: name, Messages: info.State.Msgs}
if info.Cluster != nil {
sh.Leader = info.Cluster.Leader
total := 1 + len(info.Cluster.Replicas) // leader + 副本
healthy := 0
if sh.Leader != "" {
healthy++ // leader 在即算健康 1 个
}
for _, p := range info.Cluster.Replicas {
if p.Current && !p.Offline {
healthy++
}
}
sh.ReplicasHealthy, sh.ReplicasTotal = healthy, total
} else {
sh.ReplicasHealthy, sh.ReplicasTotal, sh.Leader = 1, 1, st.ConnectedTo // 单节点
}
if sh.ReplicasHealthy < sh.ReplicasTotal {
st.Degraded++
}
st.Streams = append(st.Streams, sh)
}
return st
}
// Close 关闭底层连接。
func (b *Bus) Close() {
if b.nc != nil {