5 节点边缘冗余方案(上):基于 aiRaft 的物联网高可用控制面设计
在物联网边缘,最贵的不是算力,而是确定性。
设备分散、网络抖动、供电不稳、现场无人值守,这些是常态。中心云再强,也救不了一个已经断网的边缘现场。于是,越来越多系统开始把关键控制面下沉到边缘:本地选主、本地故障切换、本地状态同步、本地进程托管。
但边缘高可用不能只靠"多部署几个节点"。节点放错位置,一次机房断网就能全灭;没有维护余量,坏一台之后连升级都不敢做;没有人工干预窗口,自动化系统可能在异常时做出不可逆决策。
本文给出一个基于 aiRaft 的 5 节点边缘冗余方案:跨故障域部署、独立供电供网、独立 etcd、Learner 入网、人工确认门、带外管理,目标是在物联网边缘构建一个既能自动切换、又能为人为干预留出"时空间隙"的高可用控制面。
下篇将聚焦坏节点处理流程、日常运维与适合场景。
一、为什么是 5 节点?
Raft 集群的可用性取决于多数派。节点数不是越多越好,3、5、7 是常见选择。
| 集群规模 | 多数派 quorum | 可容忍故障 | 坏 1 台后剩余 | 还能再停几台维护 |
|---|---|---|---|---|
| 3 节点 | 2 | 1 | 2 | 0 |
| 5 节点 | 3 | 2 | 4 | 1 |
| 7 节点 | 4 | 3 | 6 | 2 |
3 节点能扛一台,但坏一台后只剩刚好够多数派,没有任何维护余量。此时再停一台做升级,集群立刻不可用。
5 节点则不同:坏 1 台后还剩 4 台,quorum=3,你还能再停 1 台做更换、修复或滚动升级。坏 2 台后剩 3 台,刚好 quorum,服务还能跑,但不能再动第三台。
所以,如果希望"坏一台还能从容更换或修复",5 节点是可靠性、容错窗口和成本之间比较均衡的选择。
二、理论失效率:5 节点为什么明显更稳?
假设只有 voter 节点计入 quorum,单节点失效率为 (p),节点故障相互独立。集群失效率近似为:
P_{\\text{fail}} \\approx \\binom{N}{f+1}p\^{f+1}
其中 (f) 是可容忍故障数。于是:
- 3 节点约 (3p^2)
- 5 节点约 (10p^3)
- 7 节点约 (35p^4)
下表是独立故障假设下的理论失效率:
| voter 数 | 容忍故障 | (p=0.1%) | (p=1%) | (p=5%) | (p=10%) |
|---|---|---|---|---|---|
| 3 | 1 | 0.0003% | 0.0298% | 0.725% | 2.8% |
| 5 | 2 | 约 0.000001% | 0.000985% | 0.1158% | 0.856% |
| 7 | 3 | 约 0 | 0.000034% | 0.0194% | 0.273% |
换算成年不可用时间,假设一年 8760 小时:
| voter 数 | (p=0.1%) | (p=1%) | (p=5%) | (p=10%) |
|---|---|---|---|---|
| 3 | 1.58 分钟 | 2.61 小时 | 63.5 小时 | 245 小时 |
| 5 | 0.315 秒 | 5.18 分钟 | 10.15 小时 | 75.0 小时 |
| 7 | 0.0011 秒 | 10.8 秒 | 1.70 小时 | 23.9 小时 |
从理论上看,5 节点把失效率从 (p^2) 级降到 (p^3) 级,提升非常明显。7 节点更低,但收益递减,且 Raft 复制、选举延迟和运维复杂度都明显上升。
但必须强调:以上全部建立在"节点故障相互独立"的假设上。
如果 5 台机器放在同一个机房、同一路电、同一交换机、同一出口,那么一次断网或断电就能让它们同时失效。此时 3、5、7 节点没有本质区别。
所以,5 节点的可靠性有一个前提:跨故障域,并且独立供电、独立供网。
三、跨故障域与独立供电供网
推荐布局:2 + 2 + 1,跨三个故障域。
#mermaid-svg-GgKgXyW5L96GsdyV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GgKgXyW5L96GsdyV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GgKgXyW5L96GsdyV .error-icon{fill:#552222;}#mermaid-svg-GgKgXyW5L96GsdyV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GgKgXyW5L96GsdyV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GgKgXyW5L96GsdyV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GgKgXyW5L96GsdyV .marker.cross{stroke:#333333;}#mermaid-svg-GgKgXyW5L96GsdyV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GgKgXyW5L96GsdyV p{margin:0;}#mermaid-svg-GgKgXyW5L96GsdyV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GgKgXyW5L96GsdyV .cluster-label text{fill:#333;}#mermaid-svg-GgKgXyW5L96GsdyV .cluster-label span{color:#333;}#mermaid-svg-GgKgXyW5L96GsdyV .cluster-label span p{background-color:transparent;}#mermaid-svg-GgKgXyW5L96GsdyV .label text,#mermaid-svg-GgKgXyW5L96GsdyV span{fill:#333;color:#333;}#mermaid-svg-GgKgXyW5L96GsdyV .node rect,#mermaid-svg-GgKgXyW5L96GsdyV .node circle,#mermaid-svg-GgKgXyW5L96GsdyV .node ellipse,#mermaid-svg-GgKgXyW5L96GsdyV .node polygon,#mermaid-svg-GgKgXyW5L96GsdyV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GgKgXyW5L96GsdyV .rough-node .label text,#mermaid-svg-GgKgXyW5L96GsdyV .node .label text,#mermaid-svg-GgKgXyW5L96GsdyV .image-shape .label,#mermaid-svg-GgKgXyW5L96GsdyV .icon-shape .label{text-anchor:middle;}#mermaid-svg-GgKgXyW5L96GsdyV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GgKgXyW5L96GsdyV .rough-node .label,#mermaid-svg-GgKgXyW5L96GsdyV .node .label,#mermaid-svg-GgKgXyW5L96GsdyV .image-shape .label,#mermaid-svg-GgKgXyW5L96GsdyV .icon-shape .label{text-align:center;}#mermaid-svg-GgKgXyW5L96GsdyV .node.clickable{cursor:pointer;}#mermaid-svg-GgKgXyW5L96GsdyV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GgKgXyW5L96GsdyV .arrowheadPath{fill:#333333;}#mermaid-svg-GgKgXyW5L96GsdyV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GgKgXyW5L96GsdyV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GgKgXyW5L96GsdyV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GgKgXyW5L96GsdyV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GgKgXyW5L96GsdyV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GgKgXyW5L96GsdyV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GgKgXyW5L96GsdyV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GgKgXyW5L96GsdyV .cluster text{fill:#333;}#mermaid-svg-GgKgXyW5L96GsdyV .cluster span{color:#333;}#mermaid-svg-GgKgXyW5L96GsdyV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GgKgXyW5L96GsdyV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GgKgXyW5L96GsdyV rect.text{fill:none;stroke-width:0;}#mermaid-svg-GgKgXyW5L96GsdyV .icon-shape,#mermaid-svg-GgKgXyW5L96GsdyV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GgKgXyW5L96GsdyV .icon-shape p,#mermaid-svg-GgKgXyW5L96GsdyV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GgKgXyW5L96GsdyV .icon-shape .label rect,#mermaid-svg-GgKgXyW5L96GsdyV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GgKgXyW5L96GsdyV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GgKgXyW5L96GsdyV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GgKgXyW5L96GsdyV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 人为干预通道
故障域 C
故障域 B
故障域 A
独立 etcd 集群
etcd-1
etcd-2
etcd-3
节点 1
aiRaft voter
节点 2
aiRaft voter
节点 3
aiRaft voter
节点 4
aiRaft voter
节点 5
aiRaft voter
带外管理 BMC / 串口 / 4G
LeaderServer 扩展 API
/pause /resume /approve /yield
这个布局下:
- 挂掉故障域 A(2 台),还剩 3 台,quorum=3,集群可用;
- 挂掉故障域 C(1 台),还剩 4 台,集群可用,还有维护余量;
- 每个故障域独立供电、独立供网,避免共因故障;
- etcd 也跨故障域部署,避免 aiRaft 的 leader key、成员注册、Lease 租约、watch 兜底被单点拖死;
seed_endpoints跨域配置,新节点 join 时不会因为某个域断网而找不到集群。
"独立供电、独立供网"不等于同机房双路电、双网线。真正的独立应尽量做到:
| 层级 | 能防什么 | 防不了什么 |
|---|---|---|
| 双电源、双网卡 | 单电源、单网卡、单端口 | 同一 PDU、同一 UPS、同一交换机 |
| 双 PDU、双交换机 | 单 PDU、单交换机 | 同一 UPS、同一核心交换机、同一出口 |
| 独立 UPS、独立运营商 | 单 UPS、单运营商、单上行 | 同一机房总电、总出口、火灾、水淹 |
| 独立市电、独立光缆路由、独立运营商 | 单路市电、单运营商、单物理路由 | 同一建筑、同一机房、地震、消防、误操作 |
| 跨机房 / 跨 AZ | 机房级断电、断网、火灾、水淹 | 区域级灾难、同版本软件 bug、全局误操作 |
所以,5 节点 + 真独立供电 + 真独立供网 + 跨故障域,才能接近理论可靠性。同机房 5 节点只能防单机、单机架、单路电,防不了机房整体断电、断网、火灾、水淹。
四、方案总览
这个方案的控制面由 aiRaft 承担。aiRaft 是基于 5d-Raft / NuRaft 的分布式高可用管理守护进程,为 Kthena 等服务提供 Leader 选举、故障切换、健康检查与进程管理能力。
核心组件:
| 组件 | 作用 |
|---|---|
| RaftManager | 封装 5d-raft / NuRaft,负责选主、禅让、AddPeer/RemovePeer、Learner 提权 |
| ClusterMembership | 自动入网/注销、租约心跳、死节点收割、Learner 提权看门狗 |
| ProcessManager | fork/exec 受管子进程,监控、崩溃重启、SIGTERM 优雅退出 |
| HealthChecker | Lua 5.5 驱动,采集 loadavg + meminfo,计算健康分,触发禅让 |
| EtcdClient | etcd v3 HTTP API:KV、watch、Lease 租约 |
| LeaderServer | HTTP API:GET / 查 Leader,POST /probe 探测,POST /join 报名,POST /leave 注销 |
| libalgo.so | 健康检查算法动态库,运行时 dlopen 载入,与主程序解耦 |
在边缘场景中,aiRaft 负责回答三个问题:
- 现在谁是 Leader?
- 某个节点掉线了,要不要剔除?
- 新节点加入后,什么时候能获得竞选权?
关键特性包括:
- Raft 共识选主:多节点自动选举 Leader;
- 启动防双主:bootstrap 节点启动时探测其他 peers 的 LeaderServer,若已有 Leader 则不自举;
- 预测性禅让:健康分连续低于阈值,Leader 主动让权,避免故障节点拖垮集群;
- Learner 入网:新节点先以学习者身份加入,只同步日志、不投票不参选;日志追平后才提权;
- 自动入网 / 注销 :上线
POST /join,下线POST /leave;异常崩溃靠 etcd Lease 超时自动注销; - 子进程管理:仅 Leader 节点启动受管业务进程,崩溃自动重启,SIGTERM 优雅退出;
- etcd 双重保障:Leader 信息写入 etcd,watch 作为 Raft 回调的兜底;
- Lua 健康检查 :采集
/proc/loadavg与/proc/meminfo计算健康分,支持热更新脚本。
五、Learner 入网:换机不扰动选举
边缘现场最典型的运维动作是换机。旧设备坏了,新设备上线。如果新节点空日志直接成为 voter,可能扰动选举,甚至拖慢集群。
aiRaft 的 Learner 机制正好解决这个问题:
- 新节点以
mode=join启动,配置seed_endpoints; - 自动向 Leader 发起
POST /join; - Leader 调
addLearner,新节点以 Learner 身份加入; - Learner 只从 Leader 同步日志,不投票、不参选;
- 日志追平 Leader 后,Leader 自动
flip_learner_flag提权; - 新节点获得投票和竞选权,正式成为 voter。
旧节点如果还能优雅退出,走 POST /leave;如果已经坏了,靠 etcd Lease 租约超时自动删除成员键,Leader 看门狗连续 reap_miss_count 次探测不到就自动 remove_srv 剔除。
这套机制让"坏一台,换一台"变得平滑:新节点先追日志,再提权,不打扰现有集群。
六、人为干预的"时空间隙"
边缘系统不能只靠自动化。有些决策需要人确认:切换、回滚、维护、剔除节点、重启关键进程。问题是,人需要时间,而系统不能一直等。
5 节点冗余 + 跨故障域,本质上是在制造"时空间隙":
| 维度 | 来源 | 作用 |
|---|---|---|
| 时间间隙 | Raft 选举超时、quorum 容忍、健康检查连续阈值、预测性禅让、etcd Lease TTL、进程重启上限、优雅退出超时 | 故障后系统不瞬间崩溃,留出缓冲期 |
| 空间间隙 | 跨故障域、独立供电供网、多路径网络、带外管理、本地操作口 | 某些点失效时,其他点仍可服务,人还能到达可操作节点 |
在这个窗口里,系统可以进入"待人工确认"模式,而不是直接做不可逆动作。
可以在 LeaderServer 基础上扩展人工干预 API:
POST /pause:暂停关键写操作,进入只读/待确认模式;POST /resume:人工确认后恢复;POST /approve:对高风险动作做二次确认;POST /yield:人工触发 Leader 禅让;POST /maintenance:进入维护模式,禁止自动提权/自动重启;POST /rollback:触发回滚或恢复快照。
配套要求:
- 强认证、权限控制、审计日志、防重放;
- 超时自动退出干预模式,避免人为忘记恢复;
- 带外通道:BMC、串口、本地 Console、独立 4G/5G 链路;
- 安全关键动作仍要自动兜底,不能全等人工。
一个典型闭环:
- 边缘节点检测异常:负载、内存、网络、温度、电源;
- 健康分下降,连续 N 次低于阈值;
- Leader 预测性禅让,或进入"待人工确认"模式;
- 系统仍保持多数派,基本服务降级运行;
- 通知运维,人工通过带外或 LeaderServer 介入;
- 人工确认切换、修复、回滚或替换节点;
- 新节点以 Learner 加入,日志追平后提权;
- 集群恢复,退出维护模式。
核心原则是:人工干预不能绕过 quorum,不能制造双主,不能破坏一致性。
人工只能在共识安全边界内暂停、确认、放行或回滚。
七、配置示例
节点 1 可以是 bootstrap 节点,节点 2~5 使用 mode=join,通过 seed_endpoints 跨域联系。
json
{
"node_id": 1,
"node_name": "edge-node1",
"raft": {
"mode": "bootstrap",
"host": "0.0.0.0",
"port": 9000,
"peers": [
{"id": 1, "endpoint": "10.0.1.11:9000"},
{"id": 2, "endpoint": "10.0.1.12:9000"},
{"id": 3, "endpoint": "10.0.2.11:9000"},
{"id": 4, "endpoint": "10.0.2.12:9000"},
{"id": 5, "endpoint": "10.0.3.11:9000"}
]
},
"membership": {
"enabled": true,
"lease_ttl_sec": 10,
"heartbeat_sec": 3,
"reap_miss_count": 3,
"member_key_prefix": "/arkos/kthena/members"
},
"leader_server": { "port": 9100 },
"process": {
"enabled": true,
"name": "edge_controller",
"binary_path": "/usr/local/bin/edge_controller",
"args": ["--config", "/etc/edge/config.yaml"],
"health_check_interval_s": 5,
"auto_restart": true,
"max_restart_count": 3
},
"health": {
"check_interval_ms": 1000,
"threshold_switch": 70,
"consecutive_required": 3,
"sources": ["loadavg", "meminfo"],
"lua_script_path": "scripts/health_score.lua"
},
"etcd": {
"endpoints": [
"http://10.0.1.21:2379",
"http://10.0.2.21:2379",
"http://10.0.3.21:2379"
],
"leader_key": "/arkos/kthena/leader",
"snapshot_key": "/arkos/kthena/state/last_snapshot",
"standby_key": "/arkos/kthena/standby_nodes"
},
"log": {
"level": "info",
"path": "/var/log/airaft.log"
}
}
节点 2~5 使用类似配置,但 mode 改为 join,并提供 seed_endpoints:
json
"raft": {
"mode": "join",
"host": "0.0.0.0",
"port": 9000,
"seed_endpoints": [
"10.0.1.11:9100",
"10.0.2.11:9100",
"10.0.3.11:9100"
]
}
注意:aiRaft 默认只让 Leader 启动受管子进程。如果希望每个边缘点都运行本地采集或预处理模块,不能只靠 process 段。更合理的是:
- 本地采集/预处理模块独立运行;
- aiRaft 只托管"主处理/协调进程";
- 或扩展 ProcessManager,支持
role: leader / follower / all。
下篇将详细展开:坏节点处理流程、日常运维流程、适合场景、风险与边界,以及最终结论。