mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-15 12:14:37 +00:00
feat(cluster,webapi): 集群页可切换负载均衡模式(每节点一个开关)
## 为什么之前没有开关(以及为什么现在这么加) 模式原本只读一次环境变量 `W4F_BALANCE_MODE`,UI 只显示「观察模式 / 已启用」。 把开关加进来时有两个坑必须一起解决,否则开关是个摆设: 1. **不持久化就等于骗人**:只用 env,页面上翻一下、重启一次就回到默认, 而页面已经显示成功了。 2. **不能做成集群级开关**:每个成员只释放自己的转发,根本没有一个动作能把它们 一起翻过来。做成集群开关就是撒谎。 所以:**每节点一个开关**,写的是本节点自己的设置。 ## 持久化与优先级 - 新增 `settings` 表的 `balance_mode` 列 + `Store.BalanceMode()`。**单独一列**, 不是塞进 settings JSON:那个 JSON 是整份重写的,而这是会被各节点同时写、必须 各自收敛的状态,跟 `nodeKey`/`settings` 同一类处理。 - **存了就以存的为准**,env 只作首次启动的默认值;`Settings()` 读回时带上它, 但 `UpdateSettings` 只在非空时镜像到列,所以一次无关的设置保存不会把它清掉。 - `SetModeFromUI` **先落盘再应用**:写失败就什么都不改,否则节点会运行一个重启后 复现不出来的模式——开关就成了谎言。 - 认不出的模式一律拒绝(400),不猜。 ## 报告新增两个字段 - `modeSource`: `env` | `stored` —— 说清当前模式是启动参数给的还是有人在这里选的。 - `canControl`: 由**调用者的权限**决定,只读身份不会看到一个点了会 403 的开关。 `PUT /cluster/balance/mode` 走 **write** 级鉴权:释放会停 worker、断活连接。 ## 两处我自己抓到并修掉的错 1. **`ModeSource()` 一开始写成「能存就算 stored」**(判据是 `persist != nil`), 于是**每个有 store 的节点都自称 stored**,哪怕从没人设过。canary 第一轮就露了: `mode=dry-run source=stored`。改成看「是否真的存过值」。 2. **启动日志打的是 env 变量而不是生效值**,存过 `on` 之后仍打 `DRY-RUN`—— 上一行的 `loaded from settings` 还在,两行自相矛盾,正好会让人以为释放是关的 而它其实开着。 ## 测试 新增 6 条:存的模式压过 env、没存就回落 env、落盘失败则模式不变、乱值被拒、 UI 切换同时落盘并生效、无持久化能力时不宣称可控制。 canary 实跑 6 步:初始 `dry-run/env` → 设 `on` → 复查 `on/stored` → 乱值 400 → 乱值后模式不变 → **重启后仍 `on/stored`**,启动日志 `load balancing ON (source=stored …)`。 vue-tsc 干净;go build / vet / go test ./... 全绿。
This commit is contained in:
@ -117,6 +117,7 @@ func NewServeMux(h *Handler) (http.Handler, error) {
|
||||
mux.HandleFunc(apiPrefix+"/cluster/cache", h.auth("read")(h.handleClusterCache)) // GET read; POST write-checked inside
|
||||
mux.HandleFunc(apiPrefix+"/cluster/ring", h.auth("read")(h.handleClusterRing))
|
||||
mux.HandleFunc(apiPrefix+"/cluster/balance", h.auth("read")(h.handleClusterBalance))
|
||||
mux.HandleFunc(apiPrefix+"/cluster/balance/mode", h.auth("write")(h.handleClusterBalanceMode))
|
||||
mux.HandleFunc(apiPrefix+"/node/logs", h.auth("read")(h.handleNodeLogs))
|
||||
mux.HandleFunc(apiPrefix+"/cluster/logs/export", h.auth("read")(h.handleClusterLogsExport))
|
||||
|
||||
@ -655,6 +656,43 @@ func (h *Handler) handleClusterBalance(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "load balancer not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
rep := h.Balancer.Report()
|
||||
// CanControl is decided by the caller's own level rather than by the
|
||||
// presence of a stored value, so a read-only principal is never shown a
|
||||
// switch that would 403.
|
||||
rep.CanControl = hasLevel(r, "write")
|
||||
writeJSON(w, http.StatusOK, rep)
|
||||
}
|
||||
|
||||
// handleClusterBalanceMode switches load balancing on/off at runtime.
|
||||
//
|
||||
// It is per-node on purpose. Every member decides about its OWN forwards, so the
|
||||
// switch belongs to each node: an operator may well want to turn releasing on
|
||||
// everywhere, but the server should not pretend those are one atomic action.
|
||||
// (A cluster-wide control can be layered on top later; the primitive here is the
|
||||
// one that actually has an owner.)
|
||||
//
|
||||
// Write-level auth: releasing stops a worker and drops a live connection.
|
||||
func (h *Handler) handleClusterBalanceMode(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
if h.Balancer == nil {
|
||||
http.Error(w, "load balancer not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "parse json: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.Balancer.SetModeFromUI(req.Mode); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, h.Balancer.Report())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user