尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Raft在物联网海量设备管理中的协调层落地实践

Raft在物联网海量设备管理中的协调层落地实践 做物联网平台这几年我越来越深的体会是海量设备管理真正的瓶颈往往不在网络带宽也不在服务器算力而在“协调”这两个字。设备上线时怎么确认它的身份下发配置时怎么保证每台设备都拿到同一版本几千个网关同时上报时数据怎么不重不漏——这些都是协调问题。Raft这个看起来属于分布式数据库领域的共识算法恰恰是解决这类问题非常顺手的一把钥匙。这篇文章想聊的不是Raft的论文本身而是我把它放进物联网平台真实落地时的整体方案为什么海量设备管理会用到共识算法协调集群在端-边-云体系里放在哪一层设备注册、心跳、配置下发这些核心流程怎么基于Raft设计数据模型以及我踩过的那些坑。如果你正在搞物联网平台、设备接入网关或者在做设备数量达到十万级以上的后端系统这篇内容大概率对你有用。1. 为什么海量设备管理会把Raft“卷”进来1.1 设备管理的三个绕不开的痛点先看一个很典型的场景公司做智慧园区接入的传感器、门禁、摄像头加起来有几十万台。一开始架构很简单一台数据库加一台应用服务器设备上报就写库平台下发指令就更新状态。做到大概两三万台设备的时候问题开始冒头。第一个痛点是设备数据的唯一性。设备首次上线要注册但同一个设备可能因为网络抖动重试了好几次后端收到多条注册请求。如果两台应用服务器同时处理没有分布式锁或事务约束很容易出现同一条设备记录被插入两次、或者设备序列号对不上设备型号的情况。第二个痛点是配置的一致性。设备管理平台经常会做批量配置下发比如给某个产品线下的所有设备统一升级上报频率。这时候需要确保所有后端的规则配置是同一个版本否则某些请求打到这台机器是一个配置打到另一台机器又是一个配置设备端就会出现“上次配置和本次配置打架”的情况。第三个痛点是故障切换。应用服务器总会挂设备SDK总得知道现在该连谁。如果说连的主节点挂了之后还要人工改配置重启那就是灾难设备量越大灾难越大。这三个痛点本质上都是同一件事多个节点之间需要有一个可靠机制来“达成一致”——对某台设备的状态达成一致对某条配置的版本达成一致对“当前谁来当协调者”达成一致。Raft解决的就是这个“达成一致”的问题。1.2 从单点到多副本分布式共识的出场逻辑很多人一听共识算法就头大其实换个方式理解就特别简单。假设你开了一家连锁店原来只有一个店长所有账目他一个人记。后来店多了账目要抄成好几份放在不同门店每个店长手上都有一份。问题来了一个顾客退款了A店长把账改了B店长没来得及改过一会儿客户来B店查账发现两边的账本不一样。这就是典型的单点变多副本之后出现的“不一致”。Raft的做法是哪怕账本有很多份大家也约定只认一个“总店长”改账其他店长都听他指挥。总店长改完账以后把改动同步给多数门店只要超过半数的门店都确认收到了这笔账就算改成功了。如果总店长失联了剩下的门店就重新投票选一个新总店长。这就是选主、日志复制、多数派这三个词背后的真实含义。放到物联网设备管理里这套逻辑是完全通用的。设备注册就是一笔账设备状态变更就是一笔账配置下发也是一笔账。只要所有“账目”都经过同一个leader写入并同步到多数副本那么任何一台机器对外给出答案时都是同一份账本的结果不会出现“A机器说在线、B机器说离线”这种让人抓狂的现象。1.3 Raft和物联网场景的结合点在哪里物联网场景和大数据集群、微服务注册中心相比有个明显差异设备量极大但单台设备的有效信息量很小。这意味着你不能把每一台设备的每一次心跳都当成一条“重要的账”去走一遍Raft那会把协调集群直接打爆。真正需要走Raft的是设备生命周期里的“低频高价值”操作比如注册、注销、配置变更、版本升级指令、权限修改。而设备的高频状态比如温湿度读数、心跳时间戳应该走消息管道这是整个方案的第一个核心原则。另一个结合点是网关。设备不一定直连云端它大概率连着网关网关再上行到平台。所以Raft协调层服务的对象不只是设备终端还有网关节点本身。每个网关在上行数据之前需要先确认自己的配置版本、通道权限、归属项目是否变更。这个“网关级协调”做的事本质上和spring cloud架构里注册中心做的事类似只是设备量级更大、字段更精简、协议更偏向二进制而已。2. 先把Raft讲得像人话它到底在协调什么2.1 选主、记账、多数派三个核心机制Raft算法有三块核心机制理解这三块基本就能理解它为什么适合设备管理场景。选主Leader Election所有节点分成leader、follower、candidate三种状态。正常情况下只有一个leader其余全是follower。leader负责接收外部写入、分发日志。如果leader挂了或者网络分区了follower们会在一段时间收不到心跳之后发起选举得到多数派投票的节点成为新leader。机制上和团队里“组长失联成员重新投票选组长”完全一致。日志复制Log Replicationleader收到一条写请求之后不是自己记下来就完事而是把这条写操作包装成一个日志条目广播给所有follower等多数派节点确认写入了这条日志leader才把它应用到状态机里然后给客户端返回成功。应用状态机这一步你可以理解为真正把账改掉了。多数派Quorum任何决策都需要超过一半的节点确认。这个设计保证了即使有两个节点失联剩下的多数节点照样可以对外工作但也正因为是多数派如果网络被切成两半最多只有一个分区能凑齐多数派天然避免了双leader问题也就避免了“两个平台同时在管同一批设备”的混乱。2.2 term任期与日志复制如何保证“大家都认同一件事”Raft里有个概念叫term简单说就是“第几届领导”。每选出一个新leaderterm就加一。所有节点在通信时都带着自己的term号如果发现对方term比自己大就乖乖承认对方是新领导。这个机制看起来不起眼却是防止旧leader复活之后继续发号施令的关键。你想象一个场景原来的leader和集群网络断开了但它自己并不知道还在继续接收客户端请求。这时候follower那边已经超时选举出了新leaderterm更高。旧leader所在的网络分区是不满足多数派的所以它并没有真正形成有效决策。等网络恢复旧leader发现自己term太低会立刻退位成follower。Raft靠term和日志的编号比较确保了“旧账不被覆盖、新账必须包含旧账”最终所有节点上的日志序列都是从第一条到最后一条完全一致的。落到设备管理里就是无论哪台后端机器响应查询看到的设备配置演进历史是一样的。2.3 物联网场景下的一点点对比为什么不是Paxos也行分布式共识算法里Paxos理论地位很高但工程上落地很麻烦单是搞懂它就需要花很长时间。Raft的好处是把它拆成了选主、日志复制、安全性三个子问题每个都能单独理解、单独实现。业界有大量成熟的开源实现比如etcd、Consul都是基于Raft的。从应用角度你不用自己从零写日志复制直接用etcd这类组件就能拿到Raft的能力。物联网平台选Raft还有一个隐性理由生态。你别小看生态当你的协调层基于etcd时对应的watch机制、租约机制、事务API都是现成的配置监听、设备心跳保活、多键事务这些场景都有官方支持不用自己造轮子。这个工程效率差异在排期比较紧的业务里尤其明显。3. 设备管理协调方案的整体架构3.1 端-边-云三层视角下的协调层定位先把整体架构说清楚。最底下是海量终端设备包括传感器、门禁控制器、摄像头、水表电表这些乱七八糟的东西。中间是边缘层主要是物联网网关负责协议转换、数据汇聚、边缘缓存。最上面才是平台层包括设备接入服务、协调集群、业务应用、大数据分析链路。协调层的位置很特殊它在接入服务和业务应用之间承担的是“状态登记处”的职责。设备接入服务收到设备上线请求时先去找协调层注册确认业务应用要下发配置时先把配置事务写到协调层数据服务查询设备档案时从协调层读取一致性的元数据和状态缓存。大数据链路里的Hive、Spark分析任务也会读设备基础数据但只读副本或者数据仓库不会直接干扰协调层。这个分层带来的好处是设备接入服务可以随意水平扩展因为协调层保证了多人同时注册同一设备时只有一个人成功业务应用可以随意更新版本因为配置的权威版本在协调层不在某个应用进程内存里大数据平台可以从协调层以变更订阅的方式拿数据保证数仓里的设备维度表和线上是一套连续性更新。3.2 高频状态与低频元数据分离的设计原则这是整套方案里我个人觉得最重要的设计取舍没有之一。你要明白Raft能承受的写入量不是无限的。etcd单集群能够稳定处理的写QPS大概在几千到一万多这个范围就算你用批量事务优化也远扛不住“百万设备每30秒上报一次在线状态”这种全量心跳流量。一百万台设备每30秒一次心跳平均每秒就是三万多次写入这还没有算业务高峰期叠加。如果这些心跳全走Raft协调集群当场就废了。所以必须把数据分成两类。低频高价值数据走协调集群包括设备注册元数据、产品配置、设备生命周期状态、权限策略、指令记录。高频低价值数据走消息管道包括心跳时间戳、传感器读数、网关运行指标。设备当前“在不在线”这种信息可以缓存在接入层内存里用Redis或者本地缓存顶一段时间没必要每次都落到Raft日志里。只有设备的上线动作、下线动作这种“状态跃迁事件”才值得写一条状态记录到协调层。这个思路还可以进一步细化。设备状态变更事件是“有状态”的同一台设备从在线到离线算一次变更但设备持续在线期间再多次收到心跳都不算变更。所以心跳到了可以先做本地判断只有状态真的翻转了才上报协调层。这一层过滤通常能去掉99%的写放大。3.3 技术选型依托Etcd还是自研Raft组件我见过两个派别。一派直接用etcd集群在etcd的key-value之上封装设备管理语义另一派用hashicorp/raft这类库自研一个协调组件。我的建议很明确大多数人直接上etcd不要自研。etcd的好处是它把Raft的工程细节都处理完了包括日志存储、快照压缩、成员变更、数据备份、权限控制、Watch监听。这些东西自己实现一遍至少是几周的工作量而且容易在极端网络条件下翻车。反观设备管理场景需要的数据模型就是一个key-value加上版本号etcd天然支持。更重要的是etcd的Watch机制对设备管理来说简直是量身定做——网关可以监听自己关心的配置前缀配置一变马上收到通知。什么时候才考虑自研如果你们的设备平台对协调层的写入模型很特殊比如每条设备状态都必须要严格持久化且写入量极大且你不能接受中间加一层消息队列做缓冲。这种情况自研Raft组件是合理的但需要专门团队维护。绝大多数情况下直接基于etcd封装业务逻辑性价比最高。4. 核心流程的实操设计4.1 设备注册与元数据创建的“一笔一账”设备注册是设备生命周期里最重要的一次写操作也是最能体现Raft价值的地方。设备首次上线时接入服务会收到注册请求里面带着厂商ID、产品型号、设备序列号、证书信息。接入服务要做的事是先去协调集群查询这个设备是否已经存在如果不存在就通过一个条件事务创建它如果已经存在就校验当前注册请求携带的密钥是否匹配。条件事务在etcd里是基于版本号比较的。比如你以/devices/{productId}/{deviceId}为key先GET一次拿到当前的版本号和value然后发起一个事务如果当前版本号等于刚才查到的版本就写入新的设备元数据如果版本号变了说明有其他人也在注册这台设备事务失败。这个操作是原子性的Raft保证它的日志顺序一致所以并发注册时后台只会有一方成功另一方拿到版本冲突选择重试或者返回“设备已存在”。实际落地时我会把设备的完整元数据再拆成几个子key避免单key过大。比如meta子key存产品型号、证书指纹config子key存设备级配置覆盖acl子key存项目权限归属。这样后续更新配置时一个条件事务只锁自己涉及的子key不会把整个设备档案的写操作都串行化。4.2 心跳保活与状态上报的降级设计心跳设计是整个方案最容易写砸的地方。一开始我们的方案很朴素设备每15秒上报一次心跳接入服务直接把心跳时间戳写进协调集群。结果设备从几千台涨到几万台之后协调集群的写入延迟从1毫秒涨到了200毫秒然后雪崩式超时。原因就是我把Raft当成了普通数据库用。后来改成三段式。第一段设备心跳先打到接入服务接入服务在本地内存里更新一个“最近心跳时间”缓存这个缓存通常TTL设为设备心跳周期的三倍。第二段接入服务判断设备上次持久化的状态和当前状态是否一致。如果设备一直在线那这次心跳只是刷新缓存不产生任何协调层写入。第三段只有检测到状态翻转比如缓存里的最近心跳时间超过了阈值判定设备从在线变成离线才向协调集群写一条“设备离线”状态记录。最终线上效果很显著。一个十万台设备的集群每15秒一轮心跳全量是每秒六千多次请求但真正落到协调集群的写请求降到了每秒个位数因为大部分设备状态长时间不变。这才让协调集群稳如泰山。4.3 配置下发与指令执行的幂等闭环配置下发和指令下发的核心问题是设备端可能重复收到指令。网络抖动、SDK重试、消息队列重复投递任何一个环节都可能让设备把同一条命令执行两遍。Raft能保证协调层数据一致但保证不了设备端执行幂等所以必须在设计指令数据模型时把幂等性做进去。我的方案是每条下发指令包含一个全局唯一的command_id这个id由协调集群用UUID或者分布式ID生成。协调层在“指令记录”里存储command_id对应的目标设备、指令内容、下发状态和期望执行时间。接入服务向设备发送指令时把command_id一起下发。设备执行完指令后上报执行结果也要携带command_id。接入服务再用这个id对指令状态做条件更新只有从“已下发”变为“已确认”这一次才算数。这个模型的巧妙之处在于即使设备收到重复指令它也能通过command_id判断这条指令是不是已经处理过。实现时可以在设备端做一个简单的幂等表把最近执行过的command_id存下来收到重复指令直接返回上次结果。协调层只认一次状态流转防止了反复下发带来的混乱。真实生产里我们遇到过设备端重启导致重复确认的情况就是因为这个幂等设计才没有出乱子。4.4 Leader切换时设备端SDK该怎么办这是很多方案都会忽略的细节。当协调集群的leader挂掉并进行切换时接入服务里那些正在执行写请求的goroutine会在几秒内连续碰到超时或错误响应。如果SDK实现得不够健壮设备侧就会疯狂重试形成重试风暴进一步加剧协调集群的写入压力。正确处理方式是SDK内部要有“连接状态探测”和“指数退避重试”。当写请求失败时先不要立刻重试而是主动探测协调集群的leader地址拿到新leader之后再进行有限次数的重试。重试间隔从100毫秒开始指数退避最多到3秒左右防止无限连打。更重要的一点是接入服务在leader切换期间要拒绝新请求。我会在协调层上方加一个“集群状态探针”定期检查当前leader的健康状态如果检测到切换中的不稳定窗口直接对业务层返回503让上层服务短暂降级。这个策略看起来简单但能避免大量“失败-重试-再失败-再重试”的死循环。等leader切换完成探针恢复健康流量自然放通。5. 数据模型与关键配置的实际落地5.1 键值设计key怎么规划才能扛住海量设备用etcd做协调层key的规划其实就是业务架构的映射。我的习惯是用前缀分级每级之间用斜杠分隔依次是资源类型、项目ID、产品ID、设备ID。比如/devices/{projectId}/{productId}/{deviceId}/meta/devices/{projectId}/{productId}/{deviceId}/status/devices/{projectId}/{productId}/{deviceId}/config/products/{projectId}/{productId}/config/commands/{projectId}/{commandId}这样的前缀设计不止是为了清晰更重要的是让etcd的按前缀查询和Watch生效。你想监听某个项目下所有设备的上线状态就watch /devices/{projectId}这个前缀想监听某个产品线的配置变更就watch /products/{projectId}/{productId}这个前缀。配合权限策略还能做到一个项目组的应用只能访问自己项目的key别的项目的设备数据完全不可见。设备量大的时候etcd的数据总量会膨胀所以需要开启压缩compaction机制。etcd默认会保留最近几万条历史版本如果每台设备都是高频变更历史版本会撑爆存储。我一般把自动压缩周期调到合理区间根据业务对历史回溯的需求来配置比如只保留1小时内的历史版本。同时定期做碎片整理defrag避免磁盘空间只增不减。5.2 行、列权限在设备数据上的映射设备数据天然有行权限和列权限两种需求这个和传统数据仓库的行列权限设计非常像。行权限就是“这个项目组能看哪些设备”列权限就是“这个角色能看到设备档案里的哪些字段”。行权限在协调层的实现很简单就是基于key前缀的读写限制。etcd的RBAC支持通配符匹配你可以给某个角色分配只读权限/devices/{projectId}/*。这样这个角色下面的所有客户端只能访问属于该项目的设备数据跨项目的设备信息完全读取不到。这在多租户的物联网平台里非常关键否则只要拿到etcd访问凭证就能横扫所有设备数据那是重大安全事故。列权限的落地要比行权限麻烦一点因为etcd存储的是完整value没到字段级别。我的做法是协调层做一套设备字段映射策略设备档案里哪些字段对外可见、哪些字段要脱敏由接入服务统一控制。比如设备管理岗位能看到网关IP、根证书指纹而普通运营岗位只能看到设备型号、安装位置、最近在线时间。协调层负责把完整的设备档案持久化业务层通过统一的数据网关读取时再做字段裁剪。本质上是把“行权限”压到存储层把“列权限”升到服务层。5.3 集群部署参数与容量估算5节点示例协调集群部署多少节点取决于你对可用性和性能的要求。三节点允许故障一个节点五节点允许故障两个节点。物联网平台我一般建议直接上五节点因为设备管理服务在线率要求通常很高而且五节点在leader切换时更容易保持多数派稳定状态。容量估算方面可以按“变更写”来算而不是总设备数。假设你有100万台设备每天每台设备平均产生10次生命周期变更那一天的写入量是1000万次平均每秒约115次写入业务高峰按5倍算也就每秒600次左右。这远小于etcd单集群的写能力上限所以协调层不是瓶颈。真正的瓶颈在设备心跳但我们已经把心跳剥离出去了。剩下的消息管道比如Kafka或MQTT Broker才是支撑海量心跳数据的专门通道。部署层面还需要考虑跨机柜容灾。五节点分散在三个机柜每个机柜放奇数个节点尽量避免单个机柜故障导致多数派丢失。磁盘方面一定要让etcd的数据盘用SSD因为Raft每个写请求都要fsync落盘机械硬盘会拖累写延迟。这个细节看起来不起眼但影响很大从HDD换到SSD之后写延迟至少下降一个数量级。6. 常见故障与排查技巧实录6.1 协调集群写入毛刺磁盘fsync与快照第一个实际踩过的坑是写入延迟毛刺。线上etcd集群平时写延迟稳定在2毫秒左右但每天固定时间会出现一波严重的写入超时持续五分钟。查了半天才发现是备份任务把磁盘IO占满了Raft每次提交日志都要fsync磁盘响应变慢导致写入锁阻塞。解决方式很简单把etcd的存储目录放在独占SSD分区上并且和其他高IO任务隔离备份改用快照方式定期做etcd自带的snapshot然后恢复到备份实例查看不要直接在线上实例上做全量档。另外要注意快照本身也有开销快照太频繁会造成IO放大通常几十GB的数据量一天一次快照就够。6.2 follower读到了过期数据Raft的强一致性只对线性一致性读生效。etcd虽然默认对读请求会做线性一致处理但如果你的客户端配置打开了“串行读”或者某些sdk没走线性读接口就可能读到落后leader的旧数据。物联网平台里最怕的就是旧数据导致误判——比如设备刚注销注册服务却还在返回设备有效。排查方法很直接看客户端请求的响应头里有没有一个叫revision的字段线性一致读返回的revision会比串行读更大也更接近最新值更稳妥的方式是写一个测试脚本在leader切换前后连续读同一个key观察是否有读取到旧版本。生产环境我干脆统一配置成强制线性读代价是多打一次leader确认请求但对正确性的收益远大于性能损失。6.3 网络分区后恢复的“人工干预”实例有一次我们的协调集群有两个节点在一个机柜另外三个节点在另一个机柜两个机柜之间的交换机出了故障。故障那一侧的两个节点立刻无法联系leader它们开始发起选举但因为凑不够多数派始终无法选出新leader。另三个节点那边还能组成多数派继续对外服务。看起来一切正常等网络恢复两个失联节点自然会重新加入集群。但恢复的时候出了岔子。失联节点在分区期间积累了一些新的日志条目加入集群后和当前leader的日志发生冲突。etcd机制会强制让落后的follower删掉冲突日志并同步leader的日志这个流程本质上是自动的但如果你在恢复窗口期对集群做过持久化数据备份可能会恢复出旧数据。那次我们踩的坑是备份系统在分区恢复前执行了一次自动恢复演练结果把线上数据回滚到了旧版本。教训是etcd集群有网络分区时绝不能做恢复性操作等集群健康状态变正常再动备份恢复。6.4 设备端重试风暴怎么防设备端SDK的耐性和后端预期经常不一致。我们遇到过设备在LEADER切换窗口内疯狂重试每秒每台设备重试好多次结果协调层恢复工作之后设备重试请求像洪水一样涌进来立刻把系统打满。这就是起先提到的重试风暴。除了指数退避之外还有一个技巧是给设备端SDK加一个“全局开关”。你可以做一个远程配置项叫设备重试开关如果后端感知到集群异常就通过消息通道下发“暂停重试”的指令让设备端主动冷却一分钟。等集群恢复再下发“恢复重试”。这样可以非常高效地保护协调集群。这个策略在设备数量特别大的时候几乎是必须的因为靠SDK自觉退避还是不够强硬不如后端主动干预。7. 工程取舍与个人实践体会最后讲点落地这些年实际积累的感受。第一个感受是别把Raft神话。它解决的是副本之间的一致性问题不是一个万能存储。海量设备数据里真正需要严格一致性的场景其实很窄就是那些“状态跃迁”和“配置更新”的少数操作。大多数设备上报的流式数据用消息队列加离线数仓那一套处理完全够用。硬把所有数据往Raft里塞最终不是性能扛不住就是成本高得没意义。第二个感受是方案设计一定要先定义“什么必须死磕一致什么可以容忍最终一致”。我们的线上规则很简单凡是对设备生命周期产生核心影响的写请求走Raft协调层凡是可能产生重复消费也不影响大局的走消息管道加幂等消费。设备离线告警这种需求就算偶尔延迟几秒或者漏一次告警用户其实也能接受但配置下发改错设备型号这种错误一旦发生就是生产事故。把正确性预算花在刀刃上系统才既可靠又经济。第三个感受是扩展性不能忘。现在十万台设备用这套方案很稳但如果未来做到百万台协调层本身不会成为瓶颈不过etcd的存储容量会成为一个新的限制因素。我现在的预案是当数据总量突破合理范围时采用“多协调集群横向分隔”的模式比如按区域拆集群各区域独立运行上层统一路由。Raft集群扩容是有硬件上限的与其硬撑一个超大集群不如从一开始就规划好数据隔离边界。这套方案不是最炫的技术方案但它是经过线上流量检验、能够在海量设备和复杂网络环境下真正干活的一套组合拳。如果你正在设计物联网平台建议你从设备注册、状态翻转、配置管理这三个最小的Raft场景开始落地跑顺了再逐步扩展。等这些核心链路稳定了你会发现“海量设备管理的协调问题”其实比想象中更有章法很多之前让人头痛的并发冲突在Raft面前都会变成干干净净的日志序列。
返回列表