极简架构在IoT平台中的项目复盘:设备接入层的高并发设计经验

📅 2026/7/21 23:45:22
极简架构在IoT平台中的项目复盘:设备接入层的高并发设计经验
极简架构在IoT平台中的项目复盘设备接入层的高并发设计经验一、项目场景与约束某工业物联网项目5000台设备每台每5秒上报一次数据温度、湿度、振动频率。计算得出约1000 QPS的数据写入。设备运行在3G/4G网络下网络不稳定需要支持数据补传。技术约束与之前类似团队3人运维能力有限不能引入Kafka或Kubernetes。需要在一台4C8G的云服务器上完成设备接入、数据存储和API服务。二、极简设计的四个核心决策决策一自定义二进制协议替代MQTT Broker。标准做法是搭建Mosquitto或EMQX做MQTT Broker。但对5000台设备而言引入一个独立需要运维的消息中间件性价比不高。自定义了一个极简的二进制协议| Header(2B) | Length(2B) | Type(1B) | DeviceID(8B) | Timestamp(8B) | Payload(NB) | CRC(2B) |单个上报消息约30-40字节。对比MQTT的MQTT CONNECT PUBLISH流程约200字节自定义协议节省了约80%的网络开销。对3G/4G网络下的设备而言低带宽意味着更快的上报速度和更低的流量费用。决策二连接复用而不是短连接。初始版本使用HTTP POST上报数据。5000台设备每5秒一次就是每小时360万次TCP握手。TCP的SYNACKACK三次握手在高频场景下占用了大量CPU。改为TCP长连接 连接池管理type DeviceConnPool struct { mu sync.RWMutex conns map[string]*DeviceConn // deviceID - conn maxConns int timeout time.Duration } type DeviceConn struct { conn net.Conn lastSeen time.Time writeLock sync.Mutex // 每个连接独立锁避免全局锁竞争 deviceID string }每台设备建立连接后保持。空闲超过5分钟才关闭。单服务器可维护5000个并发连接Go的goroutine模型天然适合这种场景。内存占用约80MB。决策三批量写入减少数据库压力。1000 QPS的数据写入如果每条单独INSERTPostgreSQL很难支撑。采用三级缓冲内存RingBuffer累计100条或100ms后触发一次批量INSERTRedis缓冲设备断线时的数据暂存重连后批量补传PostgreSQL时序分区按设备ID Hash分16个分区避免写入热点type BatchWriter struct { buffer chan DataPoint batchSize int ticker *time.Ticker db *sql.DB } func (w *BatchWriter) Run(ctx context.Context) { batch : make([]DataPoint, 0, w.batchSize) for { select { case dp : -w.buffer: batch append(batch, dp) if len(batch) w.batchSize { w.flush(batch) batch batch[:0] } case -w.ticker.C: if len(batch) 0 { w.flush(batch) batch batch[:0] } case -ctx.Done(): if len(batch) 0 { w.flush(batch) // 优雅关闭 } return } } } func (w *BatchWriter) flush(batch []DataPoint) { // PostgreSQL COPY协议批量插入比INSERT快10倍 tx, _ : w.db.Begin() stmt, _ : tx.Prepare(pq.CopyIn( device_data, device_id, timestamp, metric_type, value, )) for _, dp : range batch { stmt.Exec(dp.DeviceID, dp.Timestamp, dp.MetricType, dp.Value) } stmt.Close() tx.Commit() }优化后写入延迟单条INSERT方案P99约120ms → 批量方案P99约15ms。决策四Redis做热数据缓存。最新的数据最近1小时放在Redis中。查询最近数据的API从Redis读毫秒级响应。历史数据走PostgreSQL。Redis使用Sorted Setmember是deviceID:metricTypescore是时间戳方便范围查询。三、数据补传机制设备在3G/4G网络下频繁断线。断线期间的数据需要缓存和补传func (s *Server) handleReconnect(deviceID string, conn net.Conn) { // 设备重连后检查断线期间是否有未上报数据 lastReportTime : s.getLastReportTime(deviceID) gap : time.Since(lastReportTime) if gap 30*time.Second { // 发送补传指令 s.sendCommand(conn, Command{ Type: CmdBatchRetransmit, DeviceID: deviceID, Payload: marshalTimeRange(lastReportTime, time.Now()), }) } // 接收补传数据 s.receiveRetransmit(conn, deviceID) } func (s *Server) receiveRetransmit(conn net.Conn, deviceID string) { // 超时保护补传不超过60秒 deadline : time.Now().Add(60 * time.Second) conn.SetReadDeadline(deadline) for { packet, err : s.readPacket(conn) if err ! nil { break } if packet.Type PacketRetransmitEnd { break } s.batchWriter.buffer - packet.ToDataPoint() } }四、性能边界与可扩展性当前系统性能5000台设备稳定在线1000 QPS数据写入CPU使用率约35%内存约1.2GB写入P99延迟约15ms读取P99延迟约5msRedis热数据已知边界单服务器连接上限约2万台设备TCP文件描述符限制 内存超出需要水平扩展批量写入的100ms缓冲窗口意味着数据有100ms的延迟对秒级实时性有影响PostgreSQL时序分区方案在数据量超过10亿条后需要归档策略当设备数突破1万台时的扩展路径先加服务器做水平扩展——通过一致性哈希将设备分流引入TSDB如TimescaleDB替代原生PostgreSQL提升时序查询性能但不需要引入Kafka——当前的自定义TCP 内存批处理方案在10万设备级别仍然有效五、总结IoT平台极简架构的核心原则根据真实负载选型不提前为可能的扩展引入复杂度。关键决策5000设备的规模不需要MQTT Broker——自定义二进制协议TCP长连接足够批量写入是性价比最高的优化PostgreSQL的COPY协议比单条INSERT快10倍Redis做热数据分层兼顾实时查询和存储成本Go的goroutine天然适合高并发连接场景5000连接仅80MB内存最大的教训初期尝试了标准的MQTT InfluxDB组合但运维复杂度和资源消耗远超预期。回滚到自定义协议PostgreSQL的方案后开发时间反而缩短。这再次验证了那句话在可用范围内简单方案永远比标准方案更可靠。