hive 常驻集群:像 OSPF 一样在线,像 git 一样可信

面向:要在多台机器上长期跑 agent 的人。 读完你应该能回答三个问题:为什么还要一层集群?它什么时候说话算数?主节点挂了怎么办?


0. 一句话定位

git 仍然是任务状态的唯一真相;集群是「在线状态 + 进度通知」的快速控制面。

  • claim/<tid> 的原子性、res/<tid> 的审计、land 的合流,全部不动。
  • 集群负责:谁在线、谁在跑哪张卡、进度到哪一步、谁是主节点。
  • 建议每台跑 agent 的机器都起一个节点;不起也能用——只是跨机信息回到 「等下一次 git fetch」的旧速度,且失去主节点故障时的自动接管。
  • 分布式能消除单点(一台机器关机/重启,其他节点继续提供服务),但不是物理意义上的 「永不宕机」:所有节点同时断电、网络全断、或 Git 中心仓不可达时,服务仍会降级。

这条边界很重要:集群里的租约是建议性的,不是第二套锁。真正决定「这张卡归谁」的仍然是 git 分支。


1. 拓扑

 

每台跑 agent 的机器各起一个 hive cluster serve。它们彼此发现、注册、心跳; 其中一台是主节点(leader),其余是跟随者(follower)。主节点只做协调,不做代码合并。


2. 启动与接入

2.1 每台机器

以下命令在 hive 工作目录里跑(hive 仓 = ./hive);Windows 把 / 换成 \。

 

hive work 和 hive orchestrate 启动时会顺手 cluster ensure:本机没起就后台拉起。 不想自动起:HIVE_CLUSTER_AUTOSTART=0。

2.2 新机器加入

同一广播域内什么都不用配:新节点启动后发 UDP 发现,收到 announce 就向主节点注册。

跨网段、广播被禁、或机器在不同 VLAN 时,用静态 seed:

 

seed 会写进 ~/.hive/cluster/config.json,下次启动继续用。

2.3 端口与防火墙

端口

协议

用途

默认

8788

TCP

集群 HTTP API

HIVE_CLUSTER_PORT

8789

UDP

广播发现

HIVE_CLUSTER_DISCOVERY_PORT

只开 TCP 8788 也能工作(静态 seed 或手动 join);UDP 只是省掉手填地址。

端口默认值只在 hive_ports.py 定义一次;完整分配、预留段、 防火墙和冲突排查见 端口与服务规划。现场查看/体检:

 

2.4 共享 token(跨机建议必配)

 

设置后,/api/cluster/state、注册、心跳、投票、进度写入都要求 X-Hive-Cluster-Token 或 Authorization: Bearer ...。/api/cluster/health 保持公开,方便探活。


3. 发现与注册流程

 

注册成功后:

  • 新节点每 HIVE_CLUSTER_HEARTBEAT 秒(默认 1s)向主节点发心跳;
  • 主节点把成员变化、进度、租约、事件通过心跳响应/广播带回来;
  • 其他节点通过主节点或彼此的心跳逐步收敛到同一份视图。

4. 选主、任期、化身号、宽限期

4.1 任期(term)= fencing token

每次选举 term + 1。所有写入(进度、租约、投票)都带 term:

  • 旧主节点/旧进程带着旧 term 写入,新主节点返回 409 stale;
  • 节点看到更高 term,立即让位并把 voted_for 清空;
  • 同 term 的双主(网络分区恢复后)按 node id 大者优先,另一个主动降级。

这解决的是「僵尸主节点复活后继续发号施令」——仅靠心跳超时解决不了。

4.2 化身号(incarnation)

节点每次启动把 incarnation 推到 max(上次+1, 当前毫秒),持久化在 ~/.hive/cluster/node.json。其他节点只接受更高或相等化身号的记录:

  • 重启后的节点能干净覆盖关于自己的「已经死了」的旧传闻;
  • 旧进程即使还能发包,低化身号会被忽略,不会把新进程的状态顶掉。

4.3 选举规则

  • 跟随者超过 leader_lease(默认 5s)没收到主节点心跳 → 清空 leader;
  • 等一个错开的选举超时(election_min + rank × election_jitter,默认 3s 起);
  • 候选者 term+1,向当前可见的活节点拉票;
  • 多数门槛 = floor(活节点数/2)+1,单节点集群为 1;
  • 拿到多数 → 成为主节点,立即广播心跳。
  • 跟随者之间也会互发轻量心跳:主节点死后,B/C 仍能确认对方活着,多数门槛才是 2; 否则双方都可能把对方判死,各自以「1 节点多数」当选(早期实现踩过这个双主坑)。

取舍:多数按「当前可见的活节点」算,而不是固定配置成员数。 2 节点集群里主节点退出后,剩下那台自己就是多数,所以能接管; 代价是网络分区时两边可能各自选出主节点。恢复后由 term 收敛。 这是家用/小团队局域网「可用性优先」的选择,不是 Raft 的强一致承诺。

4.3.1 人工指定主控(谁当家由人说了算)

自动选主看的是「谁先发起 + 谁的 term 高」,和「谁该当主控」无关。家里那台 常年开机的机器被笔记本抢走主控、之后再也回不去,是很常见的困扰。所以留了两条 人工通道 —— 偏好管长期,夺权管当下。

 

偏好(preferred_leader)的作用范围要说清楚:它是本机配置,只影响本机的 选举延迟 —— 被偏好的节点延迟为 0,其它节点让出一个选举窗口。所以想让 0.88 稳定当主控,就在 0.88 上配(或 start --leader)。它只在「当前没有主控」时生效: 主控还活着的时候,偏好不会把它顶下去。

夺权(promote)做了什么,按顺序:

  1. 先请现任主控 resign 主动下台 —— 它自己降级,比等它被更高任期压下去干净, 中间不会出现两边都以为自己是主控的窗口;
  1. 把本机 term 追到集群最高(/api/cluster/health 免 token 且带 term)。 这一步容易被忽略但很关键:重启过的节点 term 往往落后一整截, 直接 term+1 仍然比在位主控小,投票会被 stale_term 拒掉, 现象是「敲了 promote 什么也没发生」;
  1. term+1 发起选举,投票请求带 force: true。

force 放开了什么:原本 handle_vote 里有一道闸 —— 候选人的 store 落后于我就拒绝投票(防止选出数据落后的主节点)。人工夺权时这道闸会挡路, 因为重启节点的 store 几乎必然落后。放开它的依据是:hive 的 store 只是缓存, git 才是事实账,落后会在当上主控后由 StoreReconciler 从卡面补回来。 放行时会写一条 leader_forced 事件,注明落后多少条 —— 事后能分辨这次任期切换 是「按规矩选的」还是「人硬点的」。

不想放开的话:HIVE_CLUSTER_ALLOW_FORCE_PROMOTE=0(或 config.json 里 allow_forced_promote: false),语义变成「宁可没主控,也不接受落后节点」。

注意 promote 会打断当前主控:它有几百毫秒的不应期,正在跑的 orchestrate 那一轮可能落空(下一轮会重来)。别在关键任务收口时切。

4.3.2 主控优先级(OSPF 式,值大者优先)

preferred_leader 有个别扭的地方:它是本机配置,想让 0.88 当主控, 就得在 0.88 上单独配一条,换一台机器又要重配,而且别的节点看不见这个偏好。

所以照 OSPF 的 DR 选举补了一层优先级:每台节点在自己配置里声明「我该排第几」, 这个值随节点自述(_self_node()["priority"])广播给全集群,谁都能看到。 不用再在每台机器上互相约定「谁是首选」。

 

默认 1 —— 不改的话行为和以前完全一致(谁先发起谁当)。

四条语义,全部照抄 OSPF 的 DR 选举:

规则

行为

为什么这么定

值大者优先

优先级高的选举延迟更短,先发起、先当选

这才是「设定优先级」的本意

同值按 node_id 定序

相当于 OSPF 用 Router ID 打平手

保证顺序确定,避免同轮打平后反复重选

不抢占在位主控

高优先级节点后加入,不会把现任主控挤下来

否则设了 2 的机器每次重启都抢一次主控,而重启往往正发生在排障时

0 = 不参选

这台可以当从属、可以被投票,但自己永不主动发起选举

OSPF 里 priority 0 的接口永远不会成为 DR;给「只读副本 / 弱机器」用

全集群都是 0 时会退化成人人 1。不这么兜底的话没有任何节点愿意发起选举, 集群永远没有主控 —— 而「把优先级理解成越小越优先、全填 0」这种事一定会发生, 死锁比不理解更难查。

优先级只在当前没有主控时才起作用。想立刻换人,还是用 promote:

 

三者分工:priority 是默认机制(谁都能声明)、preferred_leader 是人工硬指定 (压过一切优先级,delay=0)、promote 是立刻动手。优先级排在 preferred 之后, 即被 preferred 指定的那台仍然最先。

配置持久化在 ~/.hive/cluster/config.json 的 leader_priority,也可以用环境变量 HIVE_CLUSTER_PRIORITY 或启动参数 --priority N 给。

4.4 主节点退出

  • 优雅停止(hive cluster stop / Ctrl-C):先向同伴发 resign,同伴立即进入选举, 通常 1~2 秒内出新主。
  • 崩溃/断电:同伴靠 leader_lease 超时发现,通常 5~8 秒内出新主。
  • 新主上任会发 leader_elected 事件,hive cluster watch / 看板徽标能看到。

4.5 租约恢复宽限期

新主节点上任后,会把已知的、未释放的租约标记 grace_until = now + 120s (--lease-grace 可调)。宽限期内:

  • 集群不会因为「换主了」就建议重派;
  • 原 worker 若还在跑,继续续租即可;
  • 宽限期过后租约才按正常 TTL 判过期。

真正的回收仍然由 hive watchdog 根据 git 里的 claim 决定,集群只提供更早的提示。


5. 集群里有什么

GET /api/cluster/state(带 token)返回:

字段

含义

term / role / leader

当前任期、本机角色、主节点记录

nodes[]

已知节点:id/name/host/port/url/incarnation/last_seen/status(role)

alive / known / quorum

在线数、已知数、当前多数门槛

events[]

最近 500 条事件(join/leave/elected/progress/lease/repo.updated…)

progress{}

每张卡的实时进度(state/progress/step/message/updated_at/term/gen)

leases{}

建议性租约(agent/lease_until/term/gen/grace_until/released)

generation

单调递增的状态代数,供长轮询判断「有没有新东西」

5.1 HTTP API

方法

路径

作用

GET

/api/cluster/health

探活(免 token)

GET

/api/cluster/state

完整快照

GET

/api/cluster/task-status

只取 progress + leases

GET

/api/cluster/events?since=N

事件增量

GET

/api/cluster/wait?since=N&timeout=20

长轮询,状态变了就返回

GET

/api/cluster/sync

本机 SyncAgent 游标/最近同步状态

POST

/api/cluster/register

注册节点

POST

/api/cluster/heartbeat

心跳 / 状态交换

POST

/api/cluster/vote

拉票

POST

/api/cluster/resign

主节点主动让位

POST

/api/cluster/leave

节点退出

POST

/api/cluster/push

上报任务进度

POST

/api/cluster/lease

上报/续租建议性租约

POST

/api/cluster/release

释放租约

POST

/api/cluster/seed

动态加 seed 并尝试注册

POST

/api/cluster/discover

立即触发一轮发现

POST

/api/cluster/event

控制面事件(P0:repo.updated)

GET

/api/cluster/store/state

本机 store 快照(tasks/claims/results)

GET

/api/cluster/store/snapshot

store 全量快照(压缩后追平用)

POST

/api/cluster/store/claim

leader CAS 领取(P2a)

POST

/api/cluster/store/lease

续租

POST

/api/cluster/store/release

释放/退回

POST

/api/cluster/store/result

结果 accepted

POST

/api/cluster/store/assign

指派

POST

/api/cluster/store/close

关闭并清 claim

POST

/api/cluster/op

leader→follower 的 op prepare(内部)

POST

/api/cluster/op/commit

leader 确认多数后提交 op(内部)

POST

/api/cluster/op/truncate

新主截断未提交尾部(内部)

GET

/api/cluster/op/since?since=N

已提交 op 追平(内部)

5.2 与 worker 的接线

  • Worker.claim 抢到 git 锁后,向本机集群发 lease;
  • _refresh_lease 续租时同步刷新集群租约;
  • run_task 结束(成功/失败/异常)撤销集群租约;
  • agent 每次 push_status:先试 HIVE_PUSH_URL(老看板直推),再退到本机集群 /api/cluster/push;
  • P0 起 status/<tid> 默认不再 force push:HIVE_STATUS_GIT=auto(默认)只在 里程碑且集群不可用时兜底;always 恢复旧行为;never 永不写 Git;
  • 看板 /api/status 会把集群进度合并进任务行(push_source=cluster);
  • hive cluster status/peers/events/watch 给人和脚本用;
  • orchestrate leader gate:有集群时只有 leader 跑 watchdog/dispatch/land; follower/no-leader 跳过写动作;--no-elect 的节点只做 observer。
Logo

更多推荐