Docker Compose编排物联网边缘栈:MQTT+InfluxDB+Grafana实战

Docker Compose编排物联网边缘栈:MQTT+InfluxDB+Grafana实战

做物联网项目有个绕不开的现实:边缘端部署环境五花八门。工控机、
树莓派
、Nvidia Jetson,每台机器上手动装一遍MQTT Broker、数据库、可视化面板,光环境依赖就能折腾一天。更别提客户现场设备挂了要重装,你总不能飞过去敲apt install。

Docker Compose
在这场景下的价值很直接:一份配置文件描述整个边缘栈,任何能跑Docker的设备上docker compose up -d就能拉起全部服务。传感器数据从采集到可视化的链路,四个容器全部搞定。

架构设计:四个容器各司其职

整条数据链路是传感器→MQTT→InfluxDB→Grafana,Node-RED夹在中间做数据流编排。四个服务各管一段:

服务 角色 端口
Mosquitto MQTT消息中间件 1883
InfluxDB 2.x 时序数据库 8086
Grafana 数据可视化看板 3000
Node-RED 数据流编排(订阅+写入) 1880

Mosquitto负责接传感器上报的MQTT消息,Node-RED订阅这些Topic做格式转换后写入InfluxDB,Grafana从InfluxDB查数据画图。这套组合在工业网关上跑过半年以上,稳定性没问题。

docker-compose.yml 完整配置

version: "3.8"

services:
  mosquitto:
    image: eclipse-mosquitto:2.0
    container_name: iot-mosquitto
    restart: unless-stopped
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log
    healthcheck:
      test: ["CMD", "mosquitto_sub", "-t", "$$SYS/broker/uptime", "-C", "1", "-W", "5"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - iot-edge

  influxdb:
    image: influxdb:2.7
    container_name: iot-influxdb
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=YourStrongPass2024
      - DOCKER_INFLUXDB_INIT_ORG=iot-org
      - DOCKER_INFLUXDB_INIT_BUCKET=sensor_data
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=your-super-secret-token-here
      - TZ=Asia/Shanghai
    volumes:
      - influxdb-data:/var/lib/influxdb2
      - influxdb-config:/etc/influxdb2
    healthcheck:
      test: ["CMD", "influx", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - iot-edge

  grafana:
    image: grafana/grafana:10.2.0
    container_name: iot-grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=ChangeMeGrafana
      - GF_USERS_ALLOW_SIGN_UP=false
      - TZ=Asia/Shanghai
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      influxdb:
        condition: service_healthy
    networks:
      - iot-edge

  nodered:
    image: nodered/node-red:3.1
    container_name: iot-nodered
    restart: unless-stopped
    ports:
      - "1880:1880"
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - nodered-data:/data
    depends_on:
      - mosquitto
      - influxdb
    networks:
      - iot-edge

volumes:
  influxdb-data:
  influxdb-config:
  grafana-data:
  nodered-data:

networks:
  iot-edge:
    driver: bridge

这里有几个设计决策值得说明。

Mosquitto配置文件

Mosquitto 2.0默认只允许本地连接,需要配置文件显式开放。在./mosquitto/config/mosquitto.conf中写入:

listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

生产环境务必把allow_anonymous改成false,配合password文件做认证。这里先放开匿名,方便联调阶段快速验证链路。等后面上了TLS再收紧,下一篇会专门讲MQTT的安全加固。

Node-RED数据流:MQTT到InfluxDB的桥梁

Node-RED在这个架构里不是摆设,它做两件事:订阅Mosquitto的传感器Topic,把JSON解析后写入InfluxDB。

进Node-RED编辑器(http://边缘节点IP:1880),搭一个三节点的flow。

第一个是MQTT输入节点,broker指向mosquitto:1883(容器内用服务名访问),Topic填sensors/#。第二个是Function节点,解析payload并组装成InfluxDB写入格式:

var payload = JSON.parse(msg.payload);
var point = {
    measurement: "temperature_humidity",
    tags: { device_id: payload.device_id },
    fields: {
        temperature: parseFloat(payload.temperature),
        humidity: parseFloat(payload.humidity)
    },
    timestamp: new Date(payload.timestamp || Date.now())
};
msg.payload = [point];
return msg;

第三个是InfluxDB输出节点,配置InfluxDB 2.x连接,URL填http://influxdb:8086,填入初始化时设置的
token
、org和bucket。三个节点连起来就完成了数据链路核心部分。

不用Node-RED也行,可以直接让ESP32通过Telegraf的MQTT input插件往InfluxDB写。但Node-RED的好处是改数据流不用改设备固件,调试方便。

Grafana看板配置

Grafana启动后浏览器访问http://边缘节点IP:3000,默认账号admin,密码就是compose里设的那个。数据源添加InfluxDB,选
Flux
查询语言(InfluxDB 2.x默认),填入token和org。

建一个Dashboard,数据查询用Flux:

from(bucket: "sensor_data")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "temperature_humidity")
  |> filter(fn: (r) => r._field == "temperature")
  |> aggregateWindow(every: 1m, fn: mean)

温度和湿度各一个Panel,设置不同的Y轴,基本看板就出来了。如果传感器多了,按device_id tag做变量筛选,一个Dashboard看多个设备。

网络与持久化设计

网络用自定义bridge网络iot-edge,容器之间用服务名通信。Node-RED连Mosquitto不用写
IP
,直接mqtt://mosquitto:1883。Grafana连InfluxDB也是http://influxdb:8086。改IP不用动配置,这是Docker网络的基本技巧。

数据持久化分两类。InfluxDB和Grafana用命名卷,由Docker管理在/var/lib/docker/volumes/下,compose down不会丢数据。Mosquitto和Node-RED用bind mount,直接映射到本地目录,方便直接查看和备份配置文件。

关键数据用命名卷,需要直接操作文件的用bind mount,这个原则在实践中很实用。

健康检查与自动重启

每个服务都配了healthcheck,在无人值守的边缘场景下很重要。restart: unless-stopped保证容器异常退出会自动拉起来,但光有restart不够——如果服务进程没挂但已经不响应了,restart策略不会触发。

healthcheck的作用就是检测这种"活着但不可用"的状态。Docker检测到unhealthy后会标记,配合depends_on的条件判断,能让下游服务在上游恢复健康后再启动。Grafana的depends_on配了condition: service_healthy,InfluxDB没起来Grafana不会启动。

容器资源限制与OOM防护

边缘设备的内存和CPU资源有限,四个容器不加限制地抢占资源,遇到InfluxDB做compaction或Grafana刷新大量面板时,系统OOM Killer会随机杀进程。在树莓派上实测,InfluxDB compaction峰值能吃掉300MB内存,把Node-RED挤掉是常事。

Docker Compose的deploy.resources字段可以限制每个容器的内存上限。虽然deploy关键字最初面向Swarm,但Compose v2+也支持在单机模式下生效:

influxdb:
  deploy:
    resources:
      limits:
        memory: 512M
      reservations:
        memory: 256M

reservations保证最低预留,limits设硬上限。关键服务(InfluxDB、Mosquitto)给高优先级,辅助服务(Node-RED)给低配额。被OOM杀掉的容器靠restart: unless-stopped拉起来,但反复重启说明资源确实不够,这时候要么换大内存设备,要么精简服务栈。

另外给宿主机设swap能在内存峰值时缓冲一下。树莓派SD卡IO慢,swap设太大会拖垮整体性能,512MB swap够应急。

部署踩坑实录

端口冲突 是最常见的问题。1883端口如果宿主机已经装了mosquitto,容器就起不来。先netstat -tlnp | grep 1883确认端口占用,要么停掉宿主机的mosquitto,要么改compose端口映射成1884:1883。

内存不足 在树莓派上特别明显。四个容器跑起来大概占700MB-1GB内存,Pi 3B只有1GB基本跑不动。建议至少用Pi 4的2GB版本,或者把InfluxDB换成更轻量的SQLite方案。Grafana可以设GF_SERVER_ENABLE_GZIP=true压缩响应减少内存压力。

时区问题 坑过我好几次。InfluxDB容器默认UTC,传感器上报的时间戳和数据库存储的差8小时,Grafana画图的时间轴对不上。所有容器都加TZ=Asia/Shanghai,Node-RED写数据时也用本地时间,这个要在部署初期就统一好。

另外InfluxDB 2.x初始化有个细节:DOCKER_INFLUXDB_INIT_MODE=setup只在数据库首次启动时生效。如果已经初始化过了再改密码,不会生效。要重置得删掉influxdb-data卷重新来。

磁盘空间 是另一个隐患。InfluxDB默认不设数据保留期限,传感器数据一直累积磁盘迟早爆。边缘节点存储空间有限,必须配retention policy自动清理:

influx bucket update -n sensor_data --retention 30d

30天数据对温湿度监控够用。需要长期趋势分析可以在Grafana里做downsampling,把1分钟精度的数据聚合成1小时精度另存一个bucket,既省空间又不丢趋势信息。

日志文件不做轮换也会膨胀。Mosquitto日志配log_max_size限制大小,Node-RED在容器层面用Docker的logging driver控制:

nodered:
  logging:
    driver: "json-file"
    options:
      max-size: "10m"
      max-file: "3"

10MB一个文件保留3个,30MB封顶,查问题够用了。

Grafana告警配置

边缘场景下光有看板不够,还得有告警。Grafana的Alerting功能可以监控温度超限、设备离线、数据断流这些异常。

告警规则的数据查询和Panel查询写法一样,取最近5分钟最后一条温度数据:

from(bucket: "sensor_data")
  |> range(start: -5m)
  |> filter(fn: (r) => r._measurement == "temperature_humidity")
  |> filter(fn: (r) => r._field == "temperature")
  |> last()

配一个Threshold上限,比如35°C,超了就触发。通知渠道选Webhook,推到钉钉或者企业微信的群机器人,现场运维人员能第一时间收到。设备离线告警的思路类似:查询某设备最近一次数据上报时间,如果超过10分钟没有新数据就告警。

边缘节点的服务入口管理

边缘节点跑起来后,1880、1883、3000、8086四个端口散落各处,现场运维人员根本记不住。这时候需要一个统一的入口面板,把这些服务的Web UI聚合到一个导航页上。

我在这类场景里用过虎王科技开源的anime_nav_pro_plus导航站系统(Gitee: gitee.com/zesso),深色玻璃拟态的风格放运维大屏上很搭,把Grafana、Node-RED、InfluxDB Admin这几个面板的URL配进去,现场人员扫一眼就知道点哪个。

部署验证

整个栈启动后按顺序验证:docker compose ps看四个容器都是Up状态,用mosquitto_pub发一条测试消息,Node-RED的debug节点能看到数据,InfluxDB里能查到记录,Grafana看板有曲线。四个环节通了就算部署成功。

实际部署中建议加一个资源监控,用docker stats看各容器的CPU和内存占用。InfluxDB刚启动时做compaction会吃一波CPU,正常稳定后降到5%以下。如果Grafana的内存持续上涨不回落,可能是面板插件有内存泄漏,升版本或者精简面板。

边缘节点断电重启后,restart: unless-stopped会让容器自动拉起。但要确认数据卷没丢——docker volume ls检查命名卷还在,bind mount的本地目录文件还在。做过一次完整断电恢复测试再上线,比出了问题再排查靠谱得多。

边缘节点的容器编排还有很多细节值得展开,比如监控告警、OTA升级、断网续传这些后续会单独写。觉得这篇配置可以直接抄的话,点个赞收藏一下。

posted @ 2026-09-25 10:46  虎王科技  阅读(3)  评论(0)    收藏  举报