feat(cluster,httpapi): GET /cluster/balance 只读负载视图

## 为什么需要它

balancer 的判断原本只能从日志看,而且**只在"确实失衡 且 迟滞窗口已满"时才输出**。
运维问"这个集群均衡吗"——在健康状态下得到的是**沉默**,与"负载均衡没开"
完全无法区分。这个端点把它的判断做成可主动查询的。

## 关键性质:Report() 不扰动迟滞计时器

Report 只读,**不碰 overSince**。否则好奇的运维反复轮询这个端点就会不断重置
"失衡已持续多久"的计时,导致真正的迁移永远不会触发——一个"看一看就把它看没了"
的只读接口。测试 TestReportDoesNotDisturbHysteresis 正是钉住这点:轮询 5 次后
计时器必须原封不动,且随后满足窗口时迁移仍要发生。

## 只 leader 报计划

`wouldMigrate`/`overloadedSince` 仅在 `IsLeader()` 时给出。非 leader 仍返回各节点
Score(那是信息性的),但不报"会迁什么"——否则运维随便查一台节点会拿到与集群
实际行为不符的答案。TestReportOnFollowerHasNoPlan 钉住。

## 返回内容

    nodes[]           各节点 Forwards/MemPct/NetPct/Score
    isLeader          只有 leader 会行动
    overloadedSince   超载已持续多久(迟滞进度)
    wouldMigrate      窗口满足后会迁哪条、从哪到哪
    inFlight          两阶段迁移进行中(revoke 慢 vs 卡死可区分)
    dryRun / enabled  当前是观察还是真迁

## 接线

Balancer 在 NewBalancer 时就创建(HTTP 层需要),W4F_BALANCE_* 解析完再用
SetRatioAndSustain 应用;新增 SetDryRun / DryRun / SetRatioAndSustain,
避免初始化顺序耦合。Handler.Balancer 为 nil 时端点返回 404。

## 测试

新增 4 条:Report 不扰动迟滞(轮询后计时器不变、且迁移仍能触发)、
follower 无计划、窗口满足前后计划的有无、in-flight 可见。
go clean -testcache 后逐包实跑 7/7 全绿。
This commit is contained in:
JianFeeeee
2026-10-06 12:44:17 +08:00
parent 45a6a76621
commit 8176478269
4 changed files with 248 additions and 1 deletions

View File

@ -389,6 +389,11 @@ func main() {
}
})
// Load balancer is created here (before the Handler) so the HTTP layer can
// expose its read-only report. Mode/parameters are applied further down, once
// the W4F_BALANCE_* environment has been parsed.
balancer := cluster.NewBalancer(ring, 2, 1800)
h := &httpapi.Handler{
Store: st,
Process: pm,
@ -580,7 +585,7 @@ func main() {
if balInterval < time.Second {
balInterval = 10 * time.Second
}
balancer := cluster.NewBalancer(ring, balRatio, balSustain)
balancer.SetRatioAndSustain(balRatio, balSustain)
if balMode == "on" {
log.Printf("ring[%s] load balancing ENABLED (ratio=%.2f sustain=%ds interval=%s)",
selfID, balRatio, balSustain, balInterval)