feat: per-forward worker 模型 — 每条转发独立 frpc 配置+独立进程

核心改动(ring 协议不变,Task 本来就是 {Local,Remote,Link} 三元组):
- process.WorkerKey: worker 键改为 'local~remote~port' 三元组
- renderForward: 每条 forward 渲染只含 1 个 proxy 的独立 frpc 配置
  (替代 renderRemote 把该 remote 全部 forwards 塞进一个进程)
- ClaimFn/RevokeFn: 认领/撤销只操作这一条 forward 自己的进程
- SyncWorkers/auto-start: 只拉起 localOnly 转发的独立进程,
  集群转发由 ring 认领节点拉起 (消除三台争抢 proxy already exists)
- RemoteStatus/StartRemote/StopRemote/RestartRemote: remote 级聚合辅助,
  保持 /profiles API 形状不变, 前端零改动
- handleStatus/logs: 按 worker key 解析真实 remote 名

效果: 三台节点的 frpc 各自只注册自己拥有的 proxy, 单条转发故障不再波及兄弟
This commit is contained in:
JianFeeeee
2026-08-24 01:42:58 +08:00
parent 2292ee7f3a
commit e3a978888a
9 changed files with 340 additions and 101 deletions

View File

@ -257,7 +257,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
binary = h.BinaryPath()
}
for _, rv := range remotes {
st, has := h.Process.Status(rv.Name)
st, has := h.Process.RemoteStatus(rv.Name)
fwd, _ := h.Store.LinksForRemote(rv.Name)
p := profileStatus{
Name: rv.Name, Enabled: rv.Enabled, Status: st, HasProc: has, Forwards: fwd,
@ -271,10 +271,12 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
}
}
// Prefer admin-derived conn state; fall back to log inference.
// In the per-forward model multiple workers may exist for one remote;
// use the first worker's log when available.
if p.AdminEnabled && len(p.ProxyStates) > 0 {
p.ConnState = connStateAdmin(p.ProxyStates)
} else {
p.ConnState = connStateOf(h.Process.LogPath(rv.Name), rv.Enabled, st)
p.ConnState = connStateOf(firstWorkerLog(h.Process, rv.Name), rv.Enabled, st)
}
profiles = append(profiles, p)
}
@ -300,11 +302,17 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
}
ts := make([]localTargetStatus, 0, len(targets))
for _, tg := range targets {
st, _ := h.Process.Status(tg.Remote)
// Per-forward model: this target's worker is the specific
// (local,remote,port) forward; active = its own process running.
st, has := h.Process.Status(process.WorkerKey(l.Name, tg.Remote, tg.RemotePort))
state := "stopped"
if has {
state = st.State
}
ts = append(ts, localTargetStatus{
Remote: tg.Remote,
RemotePort: tg.RemotePort,
WorkerState: st.State,
WorkerState: state,
})
}
localStatuses = append(localStatuses, localStatus{Local: l, Targets: ts})
@ -346,7 +354,8 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
if loc.LocalOnly {
fs.Kind = "local"
fs.OwnerID = selfID
if st, has := h.Process.Status(ln.Remote); has {
// Per-forward model: check this exact forward's worker process.
if st, has := h.Process.Status(process.WorkerKey(ln.Local, ln.Remote, ln.RemotePort)); has {
fs.Active = !ln.Disabled && st.State == "running"
}
} else {
@ -727,6 +736,16 @@ func frpcVersionOf(binPath string) string {
return ""
}
// firstWorkerLog returns the log path of the first per-forward worker of a
// remote (worker keys are "local~remote~port"), or "" when none exist. Used
// by status aggregation to infer frps connectivity from worker logs.
func firstWorkerLog(pm *process.Manager, remote string) string {
for _, key := range pm.RemoteWorkers(remote) {
return pm.LogPath(key)
}
return ""
}
// connStateOf derives the frps connection state from the local worker process
// state plus a probe of its log. We never control the remote frps: the worker is
// our local frpc; "connected" means frpc has successfully logged in to frps.