模拟1000台设备同时上报心跳-机器人租赁平台IoT压测方案
模拟 1000 台设备同时上报心跳:机器人租赁平台 IoT 压测方案
平台上线前,客户问了我们一个很难回答的问题:"展会高峰期 1000 台设备同时在线,你们的系统能扛住吗?"口头保证没有意义,我们决定用一次完整的 IoT 平台压力测试来回答。这篇文章把压测目标定义、设备模拟器编写、梯度加压执行和积压问题定位的全过程记录下来,给同样在做设备接入类平台的团队一个可复用的模板。
一、压测目标:先把"能扛住"翻译成数字
"能扛住"是句空话,压测的第一步是把业务预期翻译成可量化的技术指标。我们和客户对齐了业务规模:签约设备 800 台,预留 30% 余量,按 1000 台并发设备设计。同时考虑到一台设备在租赁周期内会产生心跳、定位、电量、故障告警等多种上行消息,我们定义了四个核心指标:
| 指标 | 定义 | 目标值 |
|---|---|---|
| 并发连接数 | 同时保持的 MQTT 长连接数 | ≥ 1000,峰值冲击 1500 |
| 消息 TPS | 平台每秒成功处理的设备消息数 | ≥ 5000(每台设备 5s 一跳 + 业务消息) |
| 端到端延迟 | 设备发出消息到入库可查的耗时 | P99 ≤ 800ms,心跳类 ≤ 1s |
| 消息积压量 | MQ 队列中待消费消息堆积条数 | 稳态 ≈ 0,峰值冲击后 5 分钟内消化完 |
这里面有个容易忽略的细节:端到端延迟必须从"设备端发出"计时,而不是从"网关收到"计时。如果只测网关侧,Broker 的排队延迟就被漏掉了——而这恰恰是压测中最容易出问题的环节。
二、设备模拟器:用一台机器仿真一千台设备
总不能真买 1000 台机器人来压测。我们写了一个设备模拟器,用单机多连接的方式仿真设备集群。选型上没有用 Java,而是用了 Go——单机维持几千条长连接,Go 的 goroutine 模型内存占用和调度开销都明显优于线程池方案。
模拟器的核心逻辑:
// 伪代码:单模拟器实例仿真 N 台设备
func main() {
total := flag.Int("n", 1000, "设备数量")
broker := flag.String("broker", "tcp://iot.example.com:1883", "")
for i := 0; i < *total; i++ {
go func(seq int) {
clientID := fmt.Sprintf("sim-robot-%06d", seq)
opts := mqtt.NewClientOptions().
AddBroker(*broker).
SetClientID(clientID).
SetCleanSession(false). // 模拟真实设备的持久会话
SetAutoReconnect(true). // 模拟弱网重连
SetKeepAlive(30 * time.Second)
client := mqtt.NewClient(opts)
token := client.Connect()
token.Wait()
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
payload := buildHeartbeat(seq) // 随机电量/坐标/状态
client.Publish("robot/heartbeat/"+clientID, 1, false, payload)
if rand.Intn(50) == 0 {
client.Publish("robot/alarm/"+clientID, 1, false, buildAlarm())
}
}
}(i)
}
select {} // 阻塞主协程
}
几个在真实压测中验证过的经验:
- QoS 要和真实设备一致。我们早期模拟器全用 QoS0,压测很漂亮;后来发现真实设备心跳走 QoS1(至少一次),Broker 要维护每条消息的 Inflight 窗口,吞吐直接掉了 35%。模拟器必须还原真实协议行为。
- 要模拟"震荡在线"。真实设备会断网重连,我们加入了随机掉线逻辑(约 2% 设备每分钟重连),结果发现了 Broker 会话清理策略的一个隐藏瓶颈。
- 单机模拟有上限。一台 4C8G 的压测机稳定仿真 2000 连接没问题,但超过后模拟器自身成为瓶颈,压测数据失真。超过单机容量就横向加压测机,按设备号分段。
三、压测环境与梯度加压方案
压测环境与生产同构但规格减半(生产 3 节点集群,压测 2 节点),链路为:设备模拟器 → EMQX(MQTT Broker)→ RocketMQ → 消费服务 → MySQL/时序库。关键原则是除目标系统外,链路上每个组件都不能成为瓶颈,所以压测前先用工具逐段验证了 Broker → MQ 的转发能力。
加压不能一步到位,我们采用梯度方案:
| 阶段 | 并发连接 | 持续时长 | 观察重点 |
|---|---|---|---|
| P1 基线 | 200 | 30 min | 建立性能基线,校准监控 |
| P2 业务量 | 500 | 60 min | 稳态运行,观察资源曲线 |
| P3 目标量 | 1000 | 120 min | 四项指标是否达标 |
| P4 峰值冲击 | 1500 | 15 min | 找崩溃边界,观察降级表现 |
| P5 恢复验证 | 回落到 500 | 30 min | 积压消化速度、连接恢复能力 |
每个阶段之间不直接升压,先确认上一阶段的积压量已归零、GC 恢复正常,再继续加压。
四、结果分析:消息积压是怎么被定位的
P3 阶段前 20 分钟一切正常,TPS 稳定在 5200 左右。第 23 分钟,监控上看到消费组积压开始爬坡,5 分钟内从 0 涨到 4.2 万条,端到端延迟 P99 冲到 4 秒。
定位过程分三步:
第一步:看积压在哪个环节。 Broker 收到消息的速率正常(说明不是连接层问题),MQ 入队速率正常,问题出在消费侧。消费服务的 CPU 只有 45%,不是算力不足。
第二步:看消费线程在等什么。 抓栈发现大量线程阻塞在数据库写入上。消费服务是逐条写 MySQL,每条消息一个事务,MySQL 的写入 TPS 上限约 1800,远低于 5000 的消息速率——积压是必然的。
第三步:确认写入模式的问题。 单条 insert 改批量后仍不够,最终定位到心跳消息里有一个"最新状态回写"操作,每条消息都在竞争同一张状态表的行锁,热点行把吞吐锁死了。
对应的改造分三层:
// 1. 心跳消息批量消费:攒批 + 批量 upsert
@RocketMQMessageListener(topic = "ROBOT_HEARTBEAT", consumeMode = ConsumeMode.ORDERLY)
public void onMessage(List<MessageExt> msgs) {
// 500 条一批,重写为批量 upsert,消除逐条事务
stateService.batchUpsert(convert(msgs));
}
// 2. 热点状态表改为 Redis Hash 承载"最新状态",MySQL 只落变更记录
// HSET robot:state:{deviceId} battery 85 lng 121.5 status ACTIVE
// 3. 告警类消息(低频高优)走独立 Topic、独立消费组,不被心跳洪峰淹没
同时加了两道防护:
- Broker 侧限流:EMQX 按客户端限速,单设备消息速率超过阈值直接丢弃心跳(心跳丢了下一跳就补上,可容忍),告警消息永不丢弃;
- 消费侧弹性扩容:积压量超过阈值自动触发消费实例扩容,积压消化完自动缩容。
改造后复测:P3 阶段稳态积压为 0,P4 冲击 1500 连接时积压峰值 1.8 万条,停止加压后 3 分 40 秒消化完毕,端到端延迟 P99 回落到 700ms 以内,全部指标达标。
五、最佳实践清单
- 先把业务语言翻译成四类指标(连接数、TPS、延迟、积压),压测报告才有验收依据;
- 模拟器必须还原真实的 QoS、重连与消息分布,否则压测结果是自欺欺人;
- 梯度加压 + 阶段间确认归零,才能区分"容量不够"和"恢复能力不够";
- 高频心跳与低频告警分 Topic 隔离,洪峰不伤害关键消息;
- 心跳类"最新状态"放缓存,MySQL 只做持久层,热点行是 IoT 场景最常见的隐形杀手;
- 限流和丢弃策略要按消息价值分级,可丢的心跳和不可丢的告警区别对待。
我们在设备租赁与资产管理类系统的定制开发上有多年的落地经验,IoT 设备接入、心跳调度、消息链路优化都是实战里打磨过的方案。如果你也在规划机器人租赁或类似平台,欢迎交流,压测方案可以帮你少走弯路。

浙公网安备 33010602011771号