
凌晨两点线上告警响了。设备离线数量突然从几十台跳到六千多台我蹲在工位上一边翻日志一边怀疑是哪个网关又挂了。后来发现事情没这么简单——单点协调服务所在的节点内存溢出了整个设备注册、状态上报、配置下发全部卡死。那会儿我们平台的设备量刚过两万台就已经被这个架构折腾得够呛。后来痛定思痛查了一堆资料最终把目光停在了 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 机制触发离线通知。接口层面提供注册、注销、上报状态、查询配置、下发指令、指令回执确认。每个接口都需要带上幂等键比如设备注册时用网关生成的全局唯一请求 IDLeader 对相同请求 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 是 100mselection timeout 在 1000ms 左右。如果物联网网关和协调服务部署的机房之间有跨地域链路过短的心跳间隔会频繁触发选举反而增加脑裂风险。我们在跨地域场景下把 heartbeat 调到了 500mselection 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 秒内持续上升触发 criticalLeader 一小时之内切换超过 3 次触发 pager。这套监控帮我们提前发现了多次磁盘 iops 下降和网络丢包问题比客户端反馈早得多。6.3 容量规划与故障演练最后说下容量规划。每台设备的基础元数据加指令队列大概 5KB 存储。十万台设备大约 500MB但这只是数据量Raft 的日志会在每次变更时膨胀所以存储实际要按 5 到 10 倍规划。故障演练很有必要。我每两个月会做一次随机的节点故障演练比如直接 kill 掉一个 etcd 节点进程、模拟机柜断网、模拟磁盘只读。演练的目的不是证明系统高可用而是让团队在真正出事时不慌。包括我在内第一次演练时看到告警一堆都觉得大事不妙练多了以后反而很平静能按剧本一步步排查。我的体会是Raft 不是银弹它解决的是共识问题但解决不了架构设计里的边界划分问题。海量设备管理协调方案能不能跑得稳一半在 Raft 本身一半在它周围的路由、幂等、监控和运维机制。把这些配套做好十万台设备线上运行才不至于天天胆战心惊。后续如果设备量再上一个数量级我大概率还会继续沿着分片、分层、控制面数据面分离这条路走把 Raft 当一个可靠底座而不是万能服务器。