onesvm-browser-server/server/internal/proxymanager/health.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

311 lines
8.2 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.

// health.go:探活状态机(design §5.2 / proxy-probe §6.2–6.4)。
//
// 调度:活跃出口 30s/次、全池 5min/轮(并发 6、单节点 8s 超时,
// 经 mihomo delay API 逐节点测);连续 2 次失败摘除、1 次成功回候选;
// EWMA 延迟按区域组维护(design §5.2「按域名分」首版降为按区域组,
// 回执注明降级)。
//
// 状态机:candidate →(连续2败)→ removed →(1胜)→ candidate;
// active 出口失败立即切热备(由 manager 层处理重选)。
package proxymanager
import (
"context"
"io"
"log"
"math"
"sort"
"sync"
"time"
"onesvm.com/onesvm/browser-server/internal/config"
)
// 状态机参数(design §5.2 锁定值)。
const (
activeInterval = 30 * time.Second // 活跃出口探活频率
fullPoolInterval = 5 * time.Minute // 全池轮询频率
probeConcurrency = 6 // 全池并发
failThreshold = 2 // 连续失败摘除阈值
ewmaAlpha = 0.3 // EWMA 平滑系数(新样本权重)
ewmaInitSamples = 2 // 初始样本数后 EWMA 收敛
)
// 节点状态。
const (
stateCandidate = "candidate" // 候选(可被 P2C 选中)
stateRemoved = "removed" // 摘除(连续 2 败)
)
// nodeState 单节点探活状态。
type nodeState struct {
Name string
Region string
Pool string
State string // candidate | removed
ConsecFails int // 连续失败计数
LastDelayMs int // 最近一次延迟(0=从未成功)
LastProbeAt time.Time
}
// healthStat EWMA 统计(按区域组聚合维度之一)。
type healthStat struct {
ewma float64 // 区域组内平均延迟 EWMA
samples int
}
// HealthEngine 探活状态机(并发安全)。
type HealthEngine struct {
mu sync.RWMutex
nodes map[string]*nodeState // key=node name
byRegion map[string]*healthStat // EWMA 按区域组(首版降级,回执注明)
prober Prober
logger *log.Logger
}
// Prober 探针接口(生产= mihomo delay API;测试= stub)。
type Prober interface {
ProbeNode(ctx context.Context, name string) (delayMs int, err error)
}
// NewHealthEngine 构造探活引擎。
func NewHealthEngine(prober Prober, logger *log.Logger) *HealthEngine {
if logger == nil {
logger = log.New(io.Discard, "proxymanager/health ", 0)
}
return &HealthEngine{
nodes: map[string]*nodeState{},
byRegion: map[string]*healthStat{},
prober: prober,
logger: logger,
}
}
// ReplacePool 全量替换节点集(订阅重载后调用;保留同名节点既有状态机进度)。
func (h *HealthEngine) ReplacePool(metas []ProxyMeta) {
h.mu.Lock()
defer h.mu.Unlock()
next := make(map[string]*nodeState, len(metas))
for _, m := range metas {
if m.Pool != poolVless {
continue // 探活只覆盖可调度池
}
if old, ok := h.nodes[m.Name]; ok {
old.Region, old.Pool = m.Region, m.Pool
next[m.Name] = old
continue
}
next[m.Name] = &nodeState{Name: m.Name, Region: m.Region, Pool: m.Pool, State: stateCandidate}
}
h.nodes = next
// 区域统计重建(保留仍存在区域的历史 EWMA)。
nextRegion := map[string]*healthStat{}
for r := range h.byRegion {
for _, n := range next {
if n.Region == r {
nextRegion[r] = h.byRegion[r]
break
}
}
}
h.byRegion = nextRegion
}
// probeOne 探测单节点并推进状态机。返回本次是否成功。
func (h *HealthEngine) probeOne(ctx context.Context, n *nodeState) bool {
if h.prober == nil || n == nil {
return false
}
delay, err := h.prober.ProbeNode(ctx, n.Name)
h.mu.Lock()
defer h.mu.Unlock()
n.LastProbeAt = config.Now()
if err != nil {
n.ConsecFails++
if n.ConsecFails >= failThreshold && n.State == stateCandidate {
n.State = stateRemoved
h.logger.Printf("[health] 节点摘除 name=%s region=%s consec_fails=%d", n.Name, n.Region, n.ConsecFails)
}
return false
}
wasRemoved := n.State == stateRemoved
n.ConsecFails = 0
n.LastDelayMs = delay
n.State = stateCandidate // 1 次成功即回候选
if wasRemoved {
h.logger.Printf("[health] 节点恢复 name=%s region=%s delay_ms=%d", n.Name, n.Region, delay)
}
// EWMA 按区域组更新。
st := h.byRegion[n.Region]
if st == nil {
st = &healthStat{}
h.byRegion[n.Region] = st
}
if st.samples == 0 {
st.ewma = float64(delay)
} else {
st.ewma = ewmaAlpha*float64(delay) + (1-ewmaAlpha)*st.ewma
}
st.samples++
return true
}
// ProbeActive 探活活跃出口集合(30s 周期调用;单并发,延迟敏感)。
// 返回 (全部成功?, 失败节点名列表)。
func (h *HealthEngine) ProbeActive(ctx context.Context, activeNames []string) (bool, []string) {
var failed []string
for _, name := range activeNames {
h.mu.RLock()
n := h.nodes[name]
h.mu.RUnlock()
if n == nil {
continue
}
if !h.probeOne(ctx, n) {
failed = append(failed, name)
}
}
return len(failed) == 0, failed
}
// ProbeFullPool 全池轮询(5min 周期,并发 6)。返回 (候选数, 摘除数)。
func (h *HealthEngine) ProbeFullPool(ctx context.Context) (int, int) {
h.mu.RLock()
names := make([]*nodeState, 0, len(h.nodes))
for _, n := range h.nodes {
names = append(names, n)
}
h.mu.RUnlock()
var (
wg sync.WaitGroup
sem = make(chan struct{}, probeConcurrency)
mu sync.Mutex
alive int
removed int
)
for _, n := range names {
wg.Add(1)
go func(n *nodeState) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if h.probeOne(ctx, n) {
mu.Lock()
alive++
mu.Unlock()
} else {
mu.Lock()
if h.snapshotState(n.Name) == stateRemoved {
removed++
}
mu.Unlock()
}
}(n)
}
wg.Wait()
return alive, removed
}
// snapshotState 读取单节点状态(ProbeFullPool 内部用)。
func (h *HealthEngine) snapshotState(name string) string {
h.mu.RLock()
defer h.mu.RUnlock()
if n := h.nodes[name]; n != nil {
return n.State
}
return ""
}
// Candidates 返回候选节点名(candidate 状态,按区域+延迟排序稳定输出)。
func (h *HealthEngine) Candidates() []string {
h.mu.RLock()
defer h.mu.RUnlock()
var out []string
for _, n := range h.nodes {
if n.State == stateCandidate {
out = append(out, n.Name)
}
}
sort.Strings(out)
return out
}
// RegionEWMA 返回区域组 EWMA(毫秒;无样本区域不在 map 中)。
func (h *HealthEngine) RegionEWMA() map[string]float64 {
h.mu.RLock()
defer h.mu.RUnlock()
out := make(map[string]float64, len(h.byRegion))
for r, st := range h.byRegion {
out[r] = math.Round(st.ewma*100) / 100
}
return out
}
// NodeDelay 单节点最近延迟(无记录给 -1)。
func (h *HealthEngine) NodeDelay(name string) int {
h.mu.RLock()
defer h.mu.RUnlock()
if n := h.nodes[name]; n != nil {
return n.LastDelayMs
}
return -1
}
// AliveCount 池内存活(candidate)节点数。
func (h *HealthEngine) AliveCount() int {
h.mu.RLock()
defer h.mu.RUnlock()
c := 0
for _, n := range h.nodes {
if n.State == stateCandidate {
c++
}
}
return c
}
// TotalCount 池内节点总数(可调度池)。
func (h *HealthEngine) TotalCount() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.nodes)
}
// RegionOf 节点区域查询。
func (h *HealthEngine) RegionOf(name string) string {
h.mu.RLock()
defer h.mu.RUnlock()
if n := h.nodes[name]; n != nil {
return n.Region
}
return unknownRegion
}
// PoolAlive 池是否存活(至少 1 候选)。
func (h *HealthEngine) PoolAlive() bool { return h.AliveCount() > 0 }
// Snapshots 节点状态快照(/api/proxies 输出,脱敏)。
type NodeSnapshot struct {
Name string `json:"name"`
Type string `json:"type"`
Region string `json:"region"`
Alive bool `json:"alive"`
DelayMs int `json:"delay_ms"`
ConsecFails int `json:"consec_fails"`
State string `json:"state"`
}
// Snapshot 全量快照(按名称排序)。
func (h *HealthEngine) Snapshot() []NodeSnapshot {
h.mu.RLock()
defer h.mu.RUnlock()
out := make([]NodeSnapshot, 0, len(h.nodes))
for _, n := range h.nodes {
out = append(out, NodeSnapshot{
Name: n.Name, Region: n.Region, Alive: n.State == stateCandidate,
DelayMs: n.LastDelayMs, ConsecFails: n.ConsecFails, State: n.State,
})
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}