docs: README 补负载均衡(自导式释放)与集群页开关

## 新增

- **特性 → 集群模式**:负载均衡条目,点明**没有中心调度**及其原因。
- **运行 → 负载均衡参数**:`W4F_BALANCE_MODE` / `W4F_BALANCE_COOLDOWN` /
  `W4F_BALANCE_INTERVAL`,并写明集群页的开关**优先于**环境变量(页面上选过就以
  页面为准,重启不丢)。
- **集群模式 → 新小节「负载均衡(自导式释放)」**:规则本体、四条边界
  (自限到平摊值 / 不低于下限 / 不碰已停用转发 / **释放 ≠ 停止**)、三种模式、
  只读报告的字段。
- **API 表**:`GET /cluster/balance`(read)、`POST /cluster/balance/mode`(write)。
- **架构 / 视图 / 测试**:补上负载均衡;并修正一处失效引用
  `npm run type-check` → `npm run build`(web/package.json 的 scripts 里根本没有
  type-check,照原文执行会报 missing script)。

## 为什么这两个「为什么」要写进文档

1. **为什么只能自导**:负载信号是持有的转发数,而这个数只在**自己的拓扑条目消失**
   时才下降,条目又是本地簿记 —— 所以 leader 只能*看见*失衡,无法*消除*它;要消除
   就得伸手改另一个节点的簿记,而这正是这个环到处都在避免的跨节点写。
2. **为什么「释放」不能复用「停止」**:停止保留条目并标记链路停用;均衡释放必须
   **删除条目**且保持链路启用。复用停止的话,每个节点的重复认领守卫都会拒绝这条
   已是「有主」的转发,它会被永久搁死在刚放弃它的那台机器上。

这两条是设计约束而不是实现细节:后续任何人改这块,不知道原因就会改错。

## 默认 dry-run 的理由也写明了

释放会停掉该转发的 frpc worker,其上的**在途连接会断一次**(新节点认领后恢复),
所以默认只报告不动作;模式开关走 write 级权限。

go test ./... 全绿(go clean -testcache 后)。
This commit is contained in:
JianFeeeee
2026-10-06 15:59:28 +08:00
parent c90d4f3791
commit 1a850b4846

View File

@ -32,6 +32,14 @@
- 主负载信号:当前节点拥有的转发数(权重 ×100)
- 次级信号:**NIC 网络接口利用率**(从 `/proc/net/dev` 增量 + `/sys/class/net/*/speed` 计算,排除了 lo/docker/veth 等虚拟接口)
- 同转发数节点间由内存占用 + 网络饱和度打破平局
- **负载均衡(自导式)**:每个节点对**自己的**转发跑同一条规则 —— 持有数超过
`ceil(总转发数 / 存活节点数)` 且至少 2 条时,自行撤销一条、把任务丢回环上,
再由环按「最闲者优先」重新认领
- **没有中心调度**:一节点的负载只有它自己的拓扑条目消失时才会下降,而条目是
本地簿记,没有对端能代它写,所以只能由持有者自己动手
- 规则自限:撤到平摊值即停(否则会把转发卸光),且低于下限永不撤、不会卸空节点
- 每节点默认 10 分钟冷却,限制抖动频率;默认 **dry-run**(只报告不释放)
- 集群页每节点一个开关:关闭 / 观察模式 / 启用,可持久化
- **per-forward 独立进程**:每条转发拥有独立的 frpc 配置文件和 frpc 进程(worker 键 = `local~remote~port`),单条转发故障不影响兄弟,消除旧模型的多节点 `proxy already exists` 冲突
- **容错自愈**:leader / 非 leader 节点宕机 → 自动检测 → 环跳过死亡节点、任务重挂;leader 宕机 → 上邻居晋升为新 leader
- **宕机自动重联**:节点宕机重启后通过缓存的集群拓扑(对端地址 + 准入密钥)自动重联,显式脱离集群的节点不自动重联
@ -160,6 +168,19 @@ sha256sum -c SHA256SUMS --ignore-missing
环境变量 `W4F_HOST` 可覆盖节点对外可达的主机名(Docker DNS / 通配监听场景用)。
### 负载均衡参数
每节点独立生效;集群页的开关**优先于**这里的默认值(在页面上选过就以页面为准,重启不丢)。
| 环境变量 | 默认 | 说明 |
| --- | --- | --- |
| `W4F_BALANCE_MODE` | `dry-run` | `off` 不参与 / `dry-run` 只报告 / `on` 真正释放 |
| `W4F_BALANCE_COOLDOWN` | `600` | 同一节点两次释放之间的最小秒数 |
| `W4F_BALANCE_INTERVAL` | `10` | 检查间隔(秒) |
**释放会停掉该转发的 frpc worker,其上的在途连接会断一次**,由新节点认领后恢复;
因此默认是 `dry-run`。集群页的开关走 write 级权限。
### 启动行为
节点启动时的集群引导优先级:
@ -191,10 +212,10 @@ webui4frpc(单二进制,无 frp 依赖,前端 go:embed)
│ ├── render 渲染 frpc JSON 配置(per-forward 单 proxy + 高级参数 + LB/健康检查)
│ ├── process worker 进程管理(spawn/stop/restart/自愈/日志轮转,worker 键 = local~remote~port)
│ ├── install 一键下载官方 frpc(GitHub Releases,含解压安全防护)
│ ├── cluster 令牌环协议(ring engine / leader 选举 / 容错 / 宕机重联 / 二进制分发 / NIC 负载采样)
│ ├── cluster 令牌环协议(ring engine / leader 选举 / 容错 / 宕机重联 / 二进制分发 / NIC 负载采样 / 自导式负载均衡)
│ └── httpapi REST API + 静态资源(auth/session / canvas / forwards / cluster / users / audit 导出)
└── web/ Vue 3 SPA(Element Plus + VueFlow 画布 + Pinia)
└── src/views/ 状态总览 / 连接配置(画布)/ 集群 / 设置 / 账号与密钥
└── src/views/ 状态总览 / 连接配置(画布)/ 集群(含负载均衡开关)/ 设置 / 账号与密钥
```
## API
@ -220,6 +241,8 @@ webui4frpc(单二进制,无 frp 依赖,前端 go:embed)
| | GET | `/binary/status` | read | frpc 路径 |
| | POST | `/binary/install` | write | 安装 frpc |
| **集群** | GET | `/cluster/ring` | read | 令牌环快照 |
| | GET | `/cluster/balance` | read | 本节点负载均衡视图(只读,不变更状态) |
| | POST | `/cluster/balance/mode` | write | 切换本节点负载均衡模式(持久化) |
| | POST | `/cluster/create` | write | 创建独立集群 |
| | POST | `/cluster/join-ring` | write | 加入集群(本节点发起) |
| | POST | `/cluster/join` | write | 接收新节点加入 |
@ -254,8 +277,45 @@ webui4frpc(单二进制,无 frp 依赖,前端 go:embed)
- **任务分发**:在任意节点画布上配置 `localOnly=false` 的转发并保存 → 画布差异判断生成命令 → 令牌携带 → 负载最低节点摘取 → 拉起 frpc worker
- **容错**:节点宕机 → 自动检测 → 环自愈(跳过死亡节点 / leader failover);重启 → 自动重联
- **退出集群**:在集群页「退出集群」→ 本节点脱离为 standalone,不自动重联
- **负载均衡**:见下节
集群页展示:环拓扑(self/leader 标记)、令牌轮次、节点负载、待办任务、活跃转发拓扑(含 owner 归属)、增量事件日志。
集群页展示:环拓扑(self/leader 标记)、令牌轮次、节点负载、待办任务、活跃转发拓扑(含 owner 归属)、负载均衡(本机份额与释放计划)、增量事件日志。
### 负载均衡(自导式释放)
**为什么不是 leader 统一调度**:一节点的负载信号是它持有的转发数,而这个数只在
**它自己的拓扑条目消失**时才下降 —— 条目是本地簿记。所以 leader 从外面只能*看见*
失衡,无法*消除*它;要消除就得伸手改另一个节点的簿记,而这正是这个环到处都在避免
的跨节点写(撤销、重启同样都是定向往 owner)。
于是规则由每个节点对自己的转发本地执行:
```
当 持有转发数 >= 2 且 持有转发数 > ceil(总转发数 / 存活节点数)
则 撤销其中一条 → 删除条目 → 以启用状态重新发布到环 → 由最闲节点认领
```
- **自限**:撤到平摊值即停。3 条分 3 台时,持有 2 条的那台卸一条后变成 1 条,
`1 > 1` 不成立 → 停手。没有这个上界,owner 会把转发全部卸光。
- **不会卸空**:低于下限 2 条永不撤销。
- **不碰已停用的转发**:停用条目的 entry 是用户「停用」决定的载体,释放它等于把
一条**已停**的转发当活干交给别人。
- **释放 ≠ 停止**:用户停止保留条目并标记链路为停用;均衡释放**删除条目**且链路保持
启用。两者不能混用 —— 复用停止的话,每个节点的重复认领守卫都会拒绝这条已是「有主」
的转发,它会被永久搁死在刚放弃它的那台机器上。
三种模式(集群页每节点一个开关,也可用 `W4F_BALANCE_MODE` 设默认值):
| 模式 | 行为 |
| --- | --- |
| `off` | 不参与均衡 |
| `dry-run` | 只报告「将释放哪条」,不动任何东西(默认) |
| `on` | 真正释放 |
只读接口 `GET /cluster/balance` 返回本节点视角:`selfForwards` / `share` /
`totalForwards` / `aliveNodes` / `selfOverShare`、`wouldRelease`、`releasing`、
`blockedBy`(为何暂不动的机器原因)、`mode` / `modeSource` / `canControl`。
该接口**不改变任何状态** —— 反复轮询不会启动、延迟或取消一次释放。
## 测试
@ -263,6 +323,9 @@ webui4frpc(单二进制,无 frp 依赖,前端 go:embed)
# 后端
go test ./...
# 单包(负载均衡规则与状态机)
go test ./internal/cluster/ -run TestShare -v
# 前端类型检查
cd web && npm run type-check
cd web && npm run build
```