☰
Raft一致性算法在物联网海量设备管理协调方案中的工程实践
2026/10/7 4:37:07 网站建设 项目流程

凌晨两点,线上告警响了。设备离线数量突然从几十台跳到六千多台,我蹲在工位上一边翻日志一边怀疑是哪个网关又挂了。后来发现事情没这么简单——单点协调服务所在的节点内存溢出了,整个设备注册、状态上报、配置下发全部卡死。那会儿我们平台的设备量刚过两万台,就已经被这个架构折腾得够呛。后来痛定思痛,查了一堆资料,最终把目光停在了 Raft 一致性算法上。“Raft与物联网:大数据海量设备管理协调方案”这个大标题听起来很大,其实落地下来就是一句话:给设备管理协调这一层找一个能自我恢复、能保持一致的底座。这篇文章就是想把这些折腾出来的经验完整记录下来,给那些正在做物联网平台、又恰好被海量设备元数据管理问题卡住的朋友一些参考。

设备量小的时候,MySQL 加一张设备表就够用了,网关上报数据直接往里写,偶尔做个缓存,一切都很美好。但设备量一旦跨越某个临界点,事情就开始变得诡异:状态不一致、注册重复、指令丢失、某个节点挂了就全局瘫痪。这个临界点通常比你想象得低,我们实际踩下来,单机协调服务能稳定撑住的设备元数据写入大概在两万台左右,再多就要开始拆东墙补西墙了。所以核心问题变成了:当物联网的海量设备涌进来时,协调方案到底应该怎么设计,才能既扛得住大数据规模,又不会动不动就翻车。

这篇文章我会按自己的实操路径来讲,先解释为什么物联网设备管理这一层需要 Raft,然后把 Raft 在里面的角色拆开,再给出一套可落地的架构方案,最后把我踩过的坑和压测运维经验一并倒出来。内容偏向架构设计和工程落地,不搞纯理论,但你会从中看到不少“双击666”级别的方向性问题。

1. 设备从千台到十万台,管理协调才真正成了大问题

1.1 单点协调服务的“最后一根稻草”

我早期对“管理协调”的理解非常土,就是一个中心服务,维护设备在线状态,收到心跳就更新 last_seen 时间,收到上报就转发到存储。接口层面也简单,设备注册、下线、属性上报、指令下发,全走同一个服务。

这套架构一直活到设备量破万,中间出过几次内存告警,都被我用扩容扛过去了。真正压垮它的是一个很常见的场景:某个工厂内部网段抖动,几千台设备同时重连,同一时间发起注册。注册流程里需要做设备ID生成、校验、分配网关、写库,这一大批并发请求全部打到协调服务上,数据库连接池瞬间耗尽。然后协调服务自身也出现假死,心跳检查线程超时,把在线设备全标记成了离线。设备端看到连接断开,又发起重连,于是雪崩。

到这里我才意识到,管理协调这一层必须要有“多副本 + 自动选主 + 数据一致”的能力。而且这个能力不能再靠外部数据库去凑,因为数据库本身也是单点,或者需要额外的高可用方案。Raft 的价值就是把这些能力内聚到服务本身。

1.2 控制面与数据面分离:Raft该管什么

好多刚接触物联网架构的人,一听“海量设备管理 + Raft”,第一反应是“那设备上报的每秒几百万条数据是不是都要走 Raft”?千万别这么干。设备上报的时序数据是数据面,量大、价值密度低、允许少量丢失;设备的注册信息、配置版本、在线状态、指令回执,才是控制面。

Raft 要管的只是控制面元数据,比如设备ID与网关的绑定关系、设备期望配置的版本号、固件升级任务状态、设备所属项目组。这些数据的特点是:单条很小、更新频繁、一旦错乱代价极高。你可以把它理解成一个大饭店的等位叫号系统,客人(设备)很多,但你只需要知道每桌坐了几个人、下一桌轮到谁,真正的厨房出菜数据(业务时序数据)走的是另一条通道。

我们在架构里把两者彻底分开:控制面走协调服务(Raft组),数据面走消息队列和流处理平台。这样既保住了 Raft 的写入效率,又不会让大数据链路被一致性算法拖着走不动。

1.3 为什么不是Paxos,不是ZooKeeper自带协议

Paxos 当然更早、更通用,但它难理解——这句话不是玩笑,是真实成本。团队里来了新人,让他去看 Paxos 论文,大概率一周都搞不清楚怎么在工程里改 bug。Raft 的最大优势是“可读性”,整个算法把问题拆成 Leader 选举、日志复制、安全性三大块,每一个模块都能独立讲清楚,也更容易用代码实现和故障复盘。

ZooKeeper 的 ZAB 协议其实和 Raft 同源,ZooKeeper 本身也是成熟方案。但我们在物联网场景里没选它,主要原因是部署形态太重。ZooKeeper 有自己的一套 session 机制、Watcher 机制,和我们的设备管理元数据模型并不是一一对应。而且那个年代客户端对多语言的支持也没现在这么丝滑。相比之下,etcd 直接基于 Raft,提供 gRPC 接口和 lease 机制,做设备心跳租约、配置存储非常顺手。所以我们后来的方案就是:控制面元数据放进 etcd,复杂的业务协调逻辑在 etcd 之上再包一层服务,这层服务本身就相当于一个自定义状态机。

2. Raft在设备管理场景里的角色拆解:Leader、日志、多数派

2.1 一台Leader统管所有设备元数据?先想想分片

Raft 的经典逻辑是“一个集群只有一个 Leader,所有写请求都走 Leader”。但设备管理场景里,如果你把所有设备元数据都放在一个 Raft 组里,那这个 Leader 就是瓶颈,而且集群规模会限制你的设备量。Raft 的写性能其实并不高,单组 etcd 在普通硬件上也就每秒几千次写,如果用事务和同步刷盘,数字还会更低。

所以海量设备管理的第一步,不是直接套 Raft,而是先做分片。把设备按项目、区域、或者设备 ID 哈希分成多个逻辑组,每一组对应一个独立的 Raft 组。设备注册时根据当前组的状态路由到一个具体的 Raft 组。这样每个 Raft 组只承担一部分设备的元数据变更,各组之间的写操作互不影响。

这个思路和数据库分库分表是一样的。但难点在于路由规则本身也必须一致。所以在我们的方案里,分组路由表也放在一个全局 etcd 集群里,也就是“先找组,再找组内 Leader”。这个两层设计帮我解决了很大规模的设备扩容问题。

2.2 设备注册与心跳写入的日志复制链路

设备注册的核心流程,可以当成状态机的一次日志复制。协调服务接收到设备注册请求后,先校验请求合法性,然后生成设备ID并构造一条日志记录,内容大概是“设备 ID 为 xxxx、所属项目组 yyyy、绑定网关 zzzz、配置版本 v1”。

Leader 把这条日志追加到自己的日志里,然后并行发给组内所有 Follower。当超过半数的节点把这个日志持久化成功后,Leader 就认为这条日志已经提交,把结果应用到状态机,然后给客户端返回成功。设备注册成功后,接下来每次心跳实际上也可以作为一次小的日志写入,但如果每条心跳都走完整同步,对 Raft 组压力过大。这时可以用 etcd 的 lease 机制——设备在网关侧上报心跳,协调服务只负责续租,不需要每跳都写 Raft。

指令下发则是另一类日志写入。平台下发一条“固件升级到 v2”的指令,协调服务把它作为一条日志写进去,等日志提交后再推送给对应网关。这样做的好处是,如果多个管理员同时操作同一台设备,指令顺序被日志顺序天然管理,不会出现你先下发 A 我后下发 B,结果 B 被覆盖的情况。

2.3 线性读机制:如何避免Follower读到过期配置

Raft 解决方案里写操作必须走 Leader,但读操作如果也让 Leader 扛,读写混合时会很吃力。很多团队图省事,直接在 Follower 上读配置,结果读到旧数据,给设备下发过期的配置,导致大量设备行为异常。这里推荐的就是 Raft 里的线性一致性读。

做法是:Follower 收到读请求后,先向 Leader 确认当前 Leader 还是自己所在组的合法 Leader,然后等待一个心跳周期,确保自己已经追上了所有提交日志,再读取本地状态机。这个流程的开销比写操作小得多,也能保证读到的是“在一个时间点上一致”的配置。

我们在网关查询设备配置时,就强制走协调服务提供的线性读接口。刚开始我觉得这个操作拖慢了网关启动速度,后来发现比起“读到旧配置导致控制混乱”,这点延迟完全值得。

3. 一套可落地的协调方案:从网关注册到数据大屏的完整链路

3.1 整体架构:设备接入层、协调层、大数据层各自做什么

这套方案最终落地后,整个链路变得非常清晰。

设备端通过网关接入,网关用 FreeRTOS/STM32 这类设备跑着轻量级通信协议,定期上报心跳和传感器数据。网关本身不直接和协调服务建立长连接,而是通过接入层(一个无状态的 API 网关)做协议转换和流量整形。接入层背后才是协调服务集群,协调服务内部用 Raft 保证元数据一致。

再往下是大数据层。设备上报的原始数据不进协调服务,而是通过接入层直接投递到 Kafka/Pulsar 这类消息队列。Flink 或 Spark Streaming 消费这些数据做清洗、聚合,最后落到 Hive 表或数据分析服务里。数据大屏需要展示实时的设备总数、在线率、告警数,这些指标由流处理任务实时计算,写进 Redis 或 ClickHouse,大屏直接拉取就行。

这样分层以后,Raft 协调服务只服务“控制面”,不会成为数据链路的瓶颈。我们曾经在 10 万台设备同时上线的情况下,协调服务的峰值写并发只有几千 QPS,完全在可控范围。

3.2 协调服务的数据模型和接口设计

协调服务里的核心数据结构需要仔细设计。设备元数据大致包含:

  • 设备基本信息:设备ID、名称、类型、项目ID、创建时间。
  • 设备状态:在线/离线、最后心跳时间、当前 IP/网关信息。
  • 配置与版本:期望配置、当前配置版本。
  • 指令记录:最近下发指令、指令状态、超时重试次数。

在 etcd 里的存储结构可以按 key 前缀来组织,例如/devices/{deviceId}/meta、/devices/{deviceId}/status、/commands/{deviceId}/{commandSeq}。用 lease 绑定在线状态 key,设备心跳续租,租约过期 key 自动删除,再配合 watch 机制触发离线通知。

接口层面提供注册、注销、上报状态、查询配置、下发指令、指令回执确认。每个接口都需要带上幂等键,比如设备注册时用网关生成的全局唯一请求 ID,Leader 对相同请求 ID 只执行一次,避免网络重试导致重复注册。

3.3 指令下发与状态回执的一致性保证

指令下发是物联网里最容易出问题的一环。协调服务把指令写入 Raft 日志之后,推送给网关,网关执行完毕回执,整个链路才算闭环。

但指令下发往往有延迟,网关可能离线,或者执行到一半失败。所以协调服务不能简单“发完就不管”。我给每个指令维护一个状态机:待确认、已推送、执行中、成功、失败、超时。网关执行成功或失败都要带上原指令的 ID 回报。协调服务根据回报更新状态机;超时的指令进入重试队列,规定时间内重试次数设上限。

这里要注意一点:下发指令和上报状态是两条并发的通信链路,它们的先后顺序并不一定等于实际执行顺序。协调服务用指令 ID 和每个设备维护的指令序号做排序,只有当前序号匹配的指令才会被推送给网关。这样可以避免“重复下发的旧指令覆盖新指令”的经典错乱。

3.4 大数据平台配合:时序数据走消息队列,元数据走Raft

大数据平台与协调服务的配合,核心在于数据边界划分。这是我在这个项目里反思最多的一点。

最开始我们很想把“设备全部数据”都放进协调服务里,然后让大数据平台直接从协调服务拉数据。结果发现时序数据量级对 Raft 而言根本扛不住,而且也没必要,因为大数据分析需要的是原始 sensor 值,这些数据不需要强一致,甚至允许了一定的重复或延迟。

后来定下的规则很简单:

  • 所有高频、大规模、可丢失的时序数据,走 Kafka/Pulsar 异步链路。
  • 所有低频、关键、必须准确的元数据和指令数据,走 Raft 协调服务。
  • 大数据平台需要元数据做维度关联时,通过协调服务的读接口查询,或者把元数据定期导出到 Hive 表,与设备上报数据做 join。

这样大数据平台既拿得到全局元数据,又能享受流式处理的大吞吐,两边互不拖累。

4. 选型与部署:etcd、自研Raft,或者直接依赖云服务

4.1 三种路线对比

做 Raft 落地方案时,摆在面前的路线无非三条:直接用 etcd、自研 Raft 状态机、依赖云厂商托管服务。我重点对比一下各自的实际体验。

路线优点缺点适合场景
基于 etcd成熟稳定、接口友好、有 lease/watch元数据模型可能和业务不匹配,需要包一层服务设备量中等,团队时间紧
自研 Raft 状态机可深度定制、按业务精简开发量大、Bug 风险高、要自己处理快照和变更需要极低延迟和定制语义,团队有人精通共识算法
云托管(如腾讯云上的 etcd 托管、或云原生配置中心)不用关心运维,稳定性好可能会有网络隔离限制、费用高私有化程度低、合规要求允许

我们最终选了第一种,基于 etcd 自建了一套协调服务。原因很现实:自研 Raft 是一个看起来性感但耗时巨大的事,尤其在物联网项目里,上下游都在等你上线,你不可能花两个月去实现一个新算法。etcd 本身已经足够稳定,而且值得强调的是,etcd 的 Raft 实现是经过生产环境验证的,我不需要在“共识算法可能藏着死锁”这件事上赌运气。

4.2 单集群还是多Raft组:根据设备区域做隔离

前面说了分片,但工程上还必须考虑“物理隔离级别”。同一个 etcd 集群如果部署在同一个机房,那区域故障依然会全挂。所以我们在多区域部署时,把设备按区域再切一层,每个区域都有自己的协调服务集群,区域之间通过上层路由中心互通。

举个例子,华东设备只访问华东协调集群,华南设备只访问华南协调集群。每台设备注册时绑定一个区域标识,路由层根据区域标识转发请求。这样某个区域故障时,其他区域完全不受影响。

当然这带来了新问题:跨区域查询设备总数时,需要聚合多个协调集群的数据。我们的解决办法是各区域定期把元数据摘要同步到大数据层,大屏和设备统计统一在大数据层查询,而不是实时查询 Raft 集群。

4.3 集群规模、心跳与网络分区参数的调优经验

Raft 集群规模不是越大越好。三个节点是经典配置,容忍一个节点故障;五个节点容忍两个节点故障。我们线上一般用五节点,节点越多,日志复制同步的放大效应越明显。如果你拿三个节点和五个节点对比写性能,五个节点的写延迟会明显上升,这是多数派通信的基本数学问题。

心跳间隔要根据网络环境调整。etcd 默认的 heartbeat 是 100ms,election timeout 在 1000ms 左右。如果物联网网关和协调服务部署的机房之间有跨地域链路,过短的心跳间隔会频繁触发选举,反而增加脑裂风险。我们在跨地域场景下把 heartbeat 调到了 500ms,election timeout 调到 3000ms,节点稳定性提升非常明显。

网络分区是最危险的情况。分区后少数派节点会失去 Leader,设备管理流程会异常。解决思路是把 Raft 节点部署在容灾拓扑中,尽量让同一个 Raft 组的节点分布在不同故障域,同时把设备的“写请求强制走多数派所在区域”。这一点在设计阶段就要定好,否则一旦分区,设备恢复时间会很难看。

5. 踩过的坑:网络抖动、脑裂、设备僵尸注册

5.1 网络抖动引起的Leader重选风暴

有一个真实教训。我们把节点都放在同一个机房的同一个机柜里,某天机柜内交换机出现了微突发丢包,三个节点之间互相丢包。由于心跳超时,Follower 开始发起新一轮选举,好不容易选出一个新 Leader,但网络仍在抖动,新 Leader 还没稳定,又触发下一轮选举。集群在这几十秒里完全不可用,而设备端的重连全部失败,算是一起小事故。

事后我们做了几件事:第一,把节点打散到不同机柜,避免单点网络设备故障;第二,调大选举超时时间,降低误判概率;第三,开启 etcd 的 pre-vote 机制,让节点在真正发起选举前先确认自己还能与其他节点通信。这三招叠加以后,再遇到网络抖动,集群基本能平稳度过,不会再出现重选风暴。

5.2 脑裂后旧Leader继续响应请求的防护办法

Raft 理论上能避免脑裂,但工程实现里还是会出现“旧 Leader 以为自己还是 Leader”的情况。尤其是网络分区下,旧 Leader 所在的少数派分区内,它依然持续处理请求,但写操作永远不会被多数派确认。问题在于,客户端的请求已经到旧 Leader,旧 Leader 会一直超时,这可能拖垮业务。

防护做法不复杂:客户端必须优先从协调服务发现接口里获取当前 Leader 地址,而不是把 Leader 地址缓存死。收到超时或异常响应后,要立刻重新发现 Leader。另外,etcd 客户端库本身有内置的自动重连机制,但如果你用的是自研封装,一定要在协议层面对“当前 Leader 无效”的响应做处理。

还发现一个典型错误:有些网关会把第一条成功响应里的协调服务地址永久缓存,即便协调服务已经换了 Leader,网关还在往旧地址发请求。我们在接入层里加了强制刷新机制,让地址最短缓存 30 秒,每次通信失败就必须重新拉取坐标。

5.3 设备离线与元数据清理的幂等策略

设备管理里最烦人的问题之一,是僵尸注册。设备明明已经下线,却因为心跳没超时,协调服务一直认为它在线。更麻烦的是,一个设备因为网络原因反复掉线重连,每次重连都走一遍注册流程,如果注册逻辑没有做幂等处理,就会在元数据里留下大量重复设备记录。

我们最终的做法是:设备注册时用“设备唯一 ID + 网关 ID + 连接时间戳”作为幂等键。协调服务收到重复注册请求时,如果原记录还存在且状态未变,直接返回已有设备信息,不重复创建。心跳租约到期后,设备状态自动标记离线,但元数据记录保留一段时间,用于离线分析和恢复。

这个保留策略也踩过坑:一开始我们直接把离线的元数据删除,结果设备重新上线时又要重新注册、重新分配指令队列,很多管理员已经配好的指令通通丢失。后来改成逻辑删除,设备重连时如果还保留配置,直接恢复在线状态,这对业务友好得多。

6. 压测与运维:往十万台设备上打流量之前先做这些

6.1 压测指标与模拟工具

不要等设备真的到十万台才做压测。我们用自定义压测工具模拟了大量网关注册、心跳、指令下发请求,重点关注四个指标:协调服务吞吐量、Raft 日志复制延迟、Leader 选举恢复时间、客户端重连成功率。

具体做法是,每个网关虚拟节点通过一个压测客户端批量提交注册请求,然后持续心跳。协调服务在压测期间需要记录 P99 写入延迟、失败率、Leader 切换次数。我最关心的不是平均值,而是 P99,因为海量设备场景下,尾部延迟决定了设备重连是否雪崩。

实测中发现,写入吞吐量主要受磁盘刷盘策略影响。etcd 默认 fsync 落盘,如果对安全性要求稍低一点,可以把降为异步刷盘,换取明显更高的写入性能。但代价是节点崩溃时可能丢失少量已提交日志。这个需要在可靠性要求和业务风险之间做权衡。

6.2 监控告警:选举成功不是终点,还要盯日志落后和租约过期

部署完协调服务后的第二周,我们曾把一个 etcd 节点的磁盘塞满,当时 Raft 日志持续增长,因为没有开启自动压缩。节点一直追赶日志,始终追不上,但它还活着,所以集群没切换。结果就是 Leader 带着两个正常 Follower 承担全部压力,而那个落后节点成了一块“墓碑”。

从那以后,我的监控面板上固定放了三类指标:

  • Raft 节点当前 Term 和 Leader 心跳状态。
  • 每个节点已持久化的日志条数、落后主节点的日志条数。
  • 设备的 lease 租约数量和过期速度。

告警规则这样设:节点落后主超过 5000 条日志触发 warning;租约过期数量在 10 秒内持续上升触发 critical;Leader 一小时之内切换超过 3 次触发 pager。这套监控帮我们提前发现了多次磁盘 iops 下降和网络丢包问题,比客户端反馈早得多。

6.3 容量规划与故障演练

最后说下容量规划。每台设备的基础元数据加指令队列,大概 5KB 存储。十万台设备大约 500MB,但这只是数据量,Raft 的日志会在每次变更时膨胀,所以存储实际要按 5 到 10 倍规划。

故障演练很有必要。我每两个月会做一次随机的节点故障演练,比如直接 kill 掉一个 etcd 节点进程、模拟机柜断网、模拟磁盘只读。演练的目的不是证明系统高可用,而是让团队在真正出事时不慌。包括我在内,第一次演练时看到告警一堆都觉得大事不妙,练多了以后反而很平静,能按剧本一步步排查。

我的体会是:Raft 不是银弹,它解决的是共识问题,但解决不了架构设计里的边界划分问题。海量设备管理协调方案能不能跑得稳,一半在 Raft 本身,一半在它周围的路由、幂等、监控和运维机制。把这些配套做好,十万台设备线上运行才不至于天天胆战心惊。后续如果设备量再上一个数量级,我大概率还会继续沿着分片、分层、控制面数据面分离这条路走,把 Raft 当一个可靠底座,而不是万能服务器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询