先说清楚我的判断——这是一个典型的规模升级问题,不是技术选型问题。
在技术社区看到老齐这期分享的标题,第一反应是:能问出“100万终端如何优化”的团队,大概率已经有一个能跑通几千或几万台设备的平台了。规模到十万级之前,很多问题可以被硬件配置掩盖,但到百万级,架构里的每一个小妥协都会被放大成故障。所以这篇笔记我不打算摆一堆名词,而是沿着一条真实业务链路,从连接接入、消息处理、数据存储、容量规划、故障排查这几个层面,逐一拆解,最后给出一个可以直接照着评估的优化清单。
这个内容适合正在做物联网平台、车联网、工业数据采集,或者准备从中小规模往大规模升级的团队参考。产品经理也能看,因为里面有不少“为什么这么选”的解释,能帮你理解研发同学在卡点的时候到底在纠结什么。
1. 先给“100万终端”建立一个体感
1.1 100万在线不等于100万请求
很多从互联网转过来的朋友,一听到100万并发,脑子里浮现的是双十一秒杀那种流量峰值。但物联网平台完全是另一回事。100万终端在线,意味着的是100万条长连接长期驻留,同时每秒还要处理几万甚至十几万条小消息。
我习惯先用一个简单公式建立体感:
假设每台设备每30秒上报一次数据,那么平台的理论TPS就是:
1000000 ÷ 30 ≈ 33333 TPS
这只是平均值。如果业务存在集中上报场景——比如早上8点大批设备同时启动、或者某块区域发生告警风暴——峰值要达到平均值的3到5倍,也就是10万TPS左右。消息体按1KB算,平台每秒要吞吐100MB左右的原始数据,一天下来的原始写入量大概是8.6TB。
注意,这还只是“正常业务”的数据。物联网设备上报有一个特点:数据量小、条数多、频率固定。这类负载对消息中间件和时序数据库的考验,和互联网应用的大消息、突发流量完全不一样。
1.2 物联网场景的三个本质差异
我梳理过物联网平台和互联网应用在架构设计上的差异,核心有三点:
第一是连接状态的管理成本。互联网应用大部分是无状态请求,网关层很容易水平扩展。但物联网设备是一条长连接,服务端要为每一条连接维持会话状态、保活状态、离线消息状态。100万连接,就是100万个有状态对象,这对网关的内存模型和负载均衡策略都有直接影响。
第二是消息语义的复杂性。互联网请求失败可以重试,物联网消息则有QoS级别、消息去重、离线补发、顺序性这些语义约束。设备端弱网重连后,服务端要能判断哪些消息已经送达,哪些需要重发,这在分布式环境下并不轻松。
第三是冷热数据的并存。互联网业务的数据冷热分层很明显,物联网则存在大量“永远在写入、极少被读”的温数据,以及设备告警、远程控制这类需要低延迟的实时数据。存储引擎选错了,要么成本爆表,要么查询慢到没法用。
你可以把物联网平台想象成一个永远坐满人的体育场:不是一瞬间涌进100万人,而是这一百万人一直在场内走动、说话、偶尔举手示意。架构要让每个人都感觉“服务在线”,同时还要应付某些区域突然的喧哗。这和“放一场烟花秀”(高并发峰值)是完全不同的设计命题。
2. 连接接入层:先扛住入口
2.1 协议选型:MQTT的统治地位是怎么形成的
100万终端的接入,协议基本绕不开MQTT。这个协议在设计上就是为“大量设备、弱网络、小消息”准备的。它有非常轻量的报文头,固定头最小只有2字节,对一个频繁上报数据的场景来说,省下来的流量非常可观。
更重要的是MQTT的会话语义。客户端可以指定cleanSession,服务端会维护会话状态,设备断线重连后可以继续消费断点消息,不需要业务层自己实现一套离线消息逻辑。对海量设备来说,这能省下极大的开发成本。
现在主流版本是MQTT 3.1.1和MQTT 5.0。5.0增加了属性、用户属性、主题别名这些能力,对大型平台做精细化运维有帮助。但注意,设备端SDK对5.0的支持参差不齐,如果存量设备数量很大,升级协议版本是个大工程,不是改个参数就能解决的。
2.2 接入集群的容量规划与连接保持
先算一笔账。一个典型的MQTT接入节点(比如一台16C64G的云主机),单机承载连接数的经验值在10万到30万之间,取决于消息吞吐量。如果设备上行频率很高,连接数要往低放;如果只是低频遥测,可以往高放。
按单机20万连接估算,100万终端至少需要5个接入节点。这还没算N+1冗余,实际生产至少准备7到8个节点。
接入层前面一定要有4层负载均衡,而不是7层。因为MQTT是一条TCP长连接,7层负载均衡要终结HTTP协议,对长连接不友好,而且会引入额外的解析开销。用4层负载(比如LVS、云上的NLB),直接按IP和端口转发TCP流量,效率和稳定性都高很多。
这里有一个容易踩坑的点:负载均衡的会话保持。长连接模式下,设备第一次连接落在哪个节点,后续重连最好还落在同一个节点,否则会话状态要跨节点同步,复杂度会飙升。所以接入节点的路由策略,要么按设备ID做哈希,要么依赖负载均衡的源IP哈希,同时配合会话状态的分布式存储(比如Redis)来兜底。
2.3 心跳、保活、半开连接的细节
物联网设备在移动网络下,经常出现“假死”状态:物理链路已经断了,但服务端不知道,连接一直占着。这就是半开连接问题。
TCP本身的keepalive默认是2小时,对物联网场景太久了。MQTT协议层的心跳(Keep Alive)机制是更好的方案:客户端在心跳间隔内发送PINGREQ,服务端收到后回应PINGRESP。如果服务端在一个半间隔内没收到任何报文,就可以判定连接失效并主动断开。
实际生产我建议心跳时间设置60到120秒,服务端检测超时设为心跳间隔的1.5倍。不是越短越好,心跳太频繁会消耗设备电量和服务端资源;太长则故障发现太慢。还有一个容易被忽略的点:心跳超时后的清理动作。连接断开后,要触发设备离线事件、清理会话状态、更新设备状态缓存,这些步骤如果没做成异步的,连接风暴时服务端会卡死在清理逻辑里。
注意:不要指望TCP keepalive来保活。在移动网络里,NAT会话超时时间往往比TCP默认值短得多,只有应用层心跳才能真正反映设备在线状态。
3. 消息链路:数据进来之后怎么流动
3.1 消息中间件的选型与分区规划
接入层收到消息后,下一步是进入消息中间件。Kafka在这个位置几乎是事实标准,原因不只是吞吐量高,更关键的是它的分区模型天然支持并行消费和消息回溯。
分区数的规划要结合吞吐量和消费逻辑来定。经验公式是:分区数 = 目标TPS / 单分区吞吐能力,同时还要考虑消费者实例数。Kafka单分区顺序写入,吞吐量大约在每秒数千到上万条,实际受消息大小、复制因子、磁盘性能影响很大。
对前面算的3.3万TPS,我建议Kafka的Topic分区数设置在24到48个。分区不是越多越好,分区太多会导致文件句柄开销大、分区间数据均衡变难、消费端Rebalance时间变长。我见过有人为了追求吞吐给一个Topic配200个分区,结果集群元数据压力大,消费端一重启就是分钟级的Rebalance。
生产端还有一个很实在的优化:批量发送与压缩。物联网消息虽然单条小,但积少成多,开启压缩后吞吐和带宽占用差距非常明显。实测下来,相同负载下开启LZ4压缩可以减少60%到70%的带宽消耗,CPU开销增长却很小。
3.2 规则引擎与流式处理的分工
消息落到Kafka之后,下游一般有两类消费逻辑:一类是实时处理,比如告警判断、设备联动、数据清洗;另一类是异步归档,比如写入时序库、冷存储。
这两类逻辑的计算模型完全不一样。实时处理要求低延迟,往往有状态(比如连续N次超过阈值才告警),适合用流式框架(比如Flink)或者轻量规则引擎来做。异步归档则是大吞吐的批式写入,更适合用独立的消费者进程批量消费后写入存储。
划边界的时候有一个原则:不要在规则引擎里做太重的计算。规则引擎擅长的是“条件-动作”,比如“温度大于80且持续3分钟则告警”。如果你在规则引擎里做复杂的机器学习推理、跨设备聚合统计,很快会把实时链路拖垮。重计算的逻辑要么下沉到流式计算框架,要么做成单独的离线任务。
3.3 存储层:时序、关系、缓存的三角布局
100万设备的存储,绝对不会用一套数据库解决所有问题。
设备最近的原始数据,要支持高频查询和聚合分析,适合用时序数据库。挑选时重点看压缩率、聚合查询能力和集群扩展性。这类库的写入模型和Kafka很像,都是追加写、按时间分区,所以吞吐量一般不是瓶颈。
设备状态类和业务元数据,比如设备当前在线状态、最新固件版本、所属用户,适合放关系型数据库或缓存。Redis在这里极为常用,但要注意:不要把设备状态数据长期放Redis,Redis是内存数据库,100万设备的元数据虽然不大,但如果消息里带的业务状态也全部塞进去,内存会持续上涨,最后被迫做LRU淘汰,导致状态丢失逻辑复杂。
还有一类是归档数据,比如超过30天的原始遥测,从时序库定期转储到廉价对象存储。这个动作一定要做成自动化。很多平台上线初期没做冷热分离,半年后存储成本翻了几倍,再回头做归档非常痛苦。
| 数据类别 | 典型内容 | 存储选型 | 保存周期 |
|---|---|---|---|
| 实时遥测 | 设备上报的温湿度、位置、状态 | 时序数据库 | 7到30天 |
| 设备状态 | 在线状态、影子数据、版本信息 | Redis + 关系库 | 长期 |
| 业务流水 | 告警事件、控制命令、日志 | 关系型数据库 | 30到90天 |
| 归档数据 | 历史遥测、历史事件 | 对象存储 | 长期 |
4. 容量规划与压测:别等上线后才发现扛不住
4.1 容量估算的几个关键公式
我每次做方案评审,都会先让团队把这几个数算出来,算完基本能判断架构哪里会先撑不住。
消息吞吐:
TPS = 设备总量 / 平均上报周期(秒) 峰值TPS = TPS × 峰值倍率(一般取3~5)网络带宽:
带宽(MB/s) = TPS × 单条消息大小(bytes) / 1024 / 1024100万台设备,每30秒上报1条1KB消息,理论带宽约32.5MB/s,加上心跳和控制指令,按50MB/s规划比较稳。
存储容量:
日存储量(GB) = TPS × 单条消息大小(bytes) × 86400 / 1024^3前面的场景,日增约8.6TB。如果保存30天,至少需要260TB的存储空间。这个数字一出,很多团队就会意识到:原始数据不可能全放数据库,必须做过滤、降采样和冷热分离。
接入层内存:
每连接内存 = 内核Socket缓冲 + 应用层会话对象 + 收发缓冲区经验值一个连接占15KB到50KB内存。100万连接,保守估算需要20GB到50GB内存。注意这是纯连接占用的内存,不算消息处理、业务逻辑的开销。单节点16G64G的机器,内存很大一部分是给连接缓冲用的。
4.2 全链路压测怎么做才不翻车
容量算完之后,一定要做压测验证。很多团队压测就是拿个脚本发几千个请求,这放在物联网场景完全没有参考价值。
首先要压的是连接能力。用测试工具模拟大量设备建立MQTT连接,逐步提高连接数,同时观察接入节点的CPU、内存、文件句柄数,以及连接建立的成功率和时延。这一步能暴露出来很多连接管理的bug,比如句柄泄漏、线程池耗尽、负载均衡会话保持失效。
其次是消息吞吐。在连接池稳定的前提下,逐步提高设备的发送频率,观察Kafka、时序库的写入延迟和堆积情况。这里要特别注意:压测机的网络也可能成为瓶颈,单台压测机模拟10万设备基本到顶了,所以压测要采用分布式施压,否则你会误判服务端能力上限。
最后是故障演练。主动杀掉一个接入节点,或者断开网络模拟设备集体重连,观察平台是否会出现雪崩。这一步能验证你的连接风暴预案是否真实有效,而不是停在文档层面。
4.3 灰度与逐步放量:一次只加10万台
100万终端不是一夜之间接进来的,实际上线过程往往是设备存量逐步增长。我的建议是分阶段放量:从10万到30万,到50万,再到100万。每个阶段稳定运行一周以上,观测监控指标,确认没有性能劣化,再进入下一阶段。
为什么强调逐步放量?因为很多问题只在特定规模下才会暴露。比如状态同步的延迟、Kafka分区的数据倾斜、时序库的合并策略,10万台时你可能完全感知不到,到50万台时突然变成线上故障。灰度放量给了架构调整的时间窗口。
注意:放量前一定要先压测到目标规模的1.2倍,而不是刚好目标值。要留出20%的冗余,否则一个突发告警风暴就可能把系统打到极限。压测永远比真实业务多压10%到20%。
5. 真实运营中的坑:这些故障最常发生
5.1 连接风暴:大面积断电后的集体重连
这是物联网平台最具破坏性的故障场景。想象一下一个片区的集中器全部断电,约30分钟恢复供电后,所有设备同时尝试重新连接平台。如果设备端没有重连退避策略,就是一个100万级的连接洪峰。
设备端的标准做法是指数退避+抖动:首次重连延迟1秒,后续逐渐增加(2秒、4秒、8秒…直到一个上限如60秒),再加上一个随机偏移,避免所有设备同步重试。服务端也要有对应的保护机制:接入层入口做请求速率限制,负载均衡设置连接超时和队列长度,Kafka在极端情况下允许短暂堆积,而不是把压力直接传递到下游。
我自己的一次教训是:前期只设计了服务端的限流,忽略了设备端的退避。结果某个版本的设备固件重连逻辑有Bug,断网恢复后3分钟内形成重连风暴,直接把接入集群的CPU打满。从那以后,我把设备端重连策略写进了设备接入规范,作为强制要求。
5.2 消息堆积:消费能力跟不上怎么办
消息堆积是IoT平台最常见的“慢性病”。现象往往不是突然故障,而是某个Kafka消费组的堆积量缓慢上升,一周后延迟达到数小时。
排查思路首先要看消费组是否均衡。如果消费者实例数少于分区数,或者每个实例消费多个分区但负载不均,就可能出现某些分区积压。其次看消费逻辑的耗时。很多消费逻辑里塞了重计算、外部API调用,单个消息处理几十毫秒,吞吐自然上不去。优化方向要么是并行化,要么是把耗时操作从消费链路剥离,改成异步批处理。
还有一个隐蔽问题:降级机制缺失。当下游存储抖动,消费者重试失败后如果直接卡住等超时,整个消费链路会瘫痪。正确做法是快速失败,先把消息本地缓存或写入本地“死信队列”,主链路继续消费,后台再慢慢补。
5.3 数据倾斜与分区不均
Kafka和时序库都会遇到数据倾斜的问题。原因往往是设备ID的哈希分布不均,或者某个网关账号下挂了大量设备,导致某个分区写入量远超其他分区,形成热点。
优化的核心是重设分区策略。比如按设备ID哈希后再加上网关维度,避免单一维度造成的串扰。时序库的写入也要注意打散:不要按设备ID连续写入,否则很容易把写入压力集中到某些数据节点。
比较容易忽略的是告警风暴造成的数据倾斜。当某个区域发生大规模告警时,相关设备的上报频率会指数级提升,短时间内这些设备的流量会占据所在分区的全部资源。如果没有任何限流策略,这条链路会拖垮整个集群的其他业务。
6. 优化不止是“扛住”,更要做“好用”
6.1 边缘计算:把压力消化在源头
当平台规模到了一定程度,一个自然的优化方向是:不是所有数据都必须上云。
边缘计算的价值在于把一部分计算前置到靠近设备的地方。比如在网关或边缘节点上做数据清洗、异常检测、本地缓存,只把有价值的特征数据和异常事件上报云端。以一个温控场景为例:云端关心的是“温度是否失控”,而不是设备每5秒上报的温度值。边缘节点可以本地聚合1分钟数据,只上报“分钟均值+告警事件”,云端的存储和计算压力直接下降一个数量级。
这也符合一个原则:能不上报的数据就不上报,能本地处理的事件就本地处理。100万终端平台如果所有数据全量上云,架构压力会非常大,边缘计算可以说是规模升级的必选项。
6.2 从“有监控”到“可预测”
大规模平台运行一段时间后,你会发现监控告警体系也要跟着进化。初期只要做到“有监控、有告警”就够了,但百万级平台必须往前走一步:容量预测和故障预测。
比如根据设备接入增长曲线,预测下个月需要多少个接入节点、存储空间要扩多少;根据消息量的周期变化,提前预知深夜低谷和上午高峰。这些用简单的趋势分析就能做得比较准,远比等到CPU告警再扩容及时。
故障预测则更复杂一些,但有一个很实用的切入点:连接数异常偏差检测。正常情况下,设备的连接数曲线是相对平滑的。当预测值与实际值出现显著偏差时,往往意味着某个区域的网络故障或设备批量掉线,此时主动巡检和修复远好于等待用户投诉。
我个人在实际运营中的体会是:优化到后期,拼的不是某一个中间件选得多好,而是整个系统的可观测性和自动化程度。你对系统运行状态的感知越实时、越精确,就能越早把故障消灭在萌芽阶段,也敢放心地加设备、加业务。
最后分享一个小技巧:在做容量规划时,建议把公式和参数维护成一个共享文档或脚本,每次业务提新需求(比如“下季度接入50万台新设备”),直接把数字带入,5分钟就能评估架构是否需要调整。这个动作已经成为我评审任何物联网项目的第一道工序。