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

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。
相关推荐
王中阳Go12 分钟前
你那条三跳链路的单跳超时,是按压测的 p95 定的,还是拍出来的?
人工智能·后端·架构
方方洛14 分钟前
ray教程-27-附录F-RayClient与runtime_env与多语言
人工智能·分布式·机器学习
神鹰手哥_起飞版15 分钟前
用 Agent 越多,硬盘越满?我在隐藏目录里发现了 334 GB 的 .mimosa
人工智能
相玉17 分钟前
AI 开发工作流做成 Claude Code 插件:原理、集成与一致性保障
人工智能
一条鱼的星辰大海29 分钟前
大模型提示词工程:从提示词到提示词系统
人工智能
用户29914221968039 分钟前
GitHub 热榜项目:日榜(2026-11-07)
人工智能·开源·github
tellmewhoisi1 小时前
深度学习0:数学知识:正态分布3(积分,指数函数积分公式)
人工智能·深度学习
无忧_wy1 小时前
Claude-Mem 记忆增强系统新手部署与调用指南
人工智能
用户2991422196801 小时前
AI 日报(2026年11月7日)
人工智能