模拟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 {} // 阻塞主协程
}

几个在真实压测中验证过的经验:

  1. QoS 要和真实设备一致。我们早期模拟器全用 QoS0,压测很漂亮;后来发现真实设备心跳走 QoS1(至少一次),Broker 要维护每条消息的 Inflight 窗口,吞吐直接掉了 35%。模拟器必须还原真实协议行为。
  2. 要模拟"震荡在线"。真实设备会断网重连,我们加入了随机掉线逻辑(约 2% 设备每分钟重连),结果发现了 Broker 会话清理策略的一个隐藏瓶颈。
  3. 单机模拟有上限。一台 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 以内,全部指标达标。

五、最佳实践清单

  1. 先把业务语言翻译成四类指标(连接数、TPS、延迟、积压),压测报告才有验收依据;
  2. 模拟器必须还原真实的 QoS、重连与消息分布,否则压测结果是自欺欺人;
  3. 梯度加压 + 阶段间确认归零,才能区分"容量不够"和"恢复能力不够";
  4. 高频心跳与低频告警分 Topic 隔离,洪峰不伤害关键消息;
  5. 心跳类"最新状态"放缓存,MySQL 只做持久层,热点行是 IoT 场景最常见的隐形杀手;
  6. 限流和丢弃策略要按消息价值分级,可丢的心跳和不可丢的告警区别对待。

我们在设备租赁与资产管理类系统的定制开发上有多年的落地经验,IoT 设备接入、心跳调度、消息链路优化都是实战里打磨过的方案。如果你也在规划机器人租赁或类似平台,欢迎交流,压测方案可以帮你少走弯路。

posted @ 2026-09-07 14:53  15889726201  阅读(8)  评论(0)    收藏  举报