工业数据采集利器:Flume Modbus Source 深度解析与实践

文章目录

工业数据采集利器:Flume Modbus Source 深度解析与实践

一、项目背景

在工业物联网(IoT)场景中,Modbus 协议是最常用的工业通信协议之一,广泛应用于 PLC、传感器、电表等设备的数据采集。然而,传统的数据采集方案存在以下痛点:

  1. 协议支持有限:多数方案仅支持 Modbus TCP,对 RTU 串口设备支持不足
  2. 配置僵化:设备点位配置硬编码,变更需重启服务
  3. 通信效率低:逐点读取寄存器,通信开销大、延迟高

针对以上问题,我开发了 Flume Modbus Source------一个基于 SQLite 配置驱动的 Apache Flume 自定义 Source,完美解决了上述痛点。

二、核心特性

特性 说明
双协议支持 同时支持 Modbus TCP 和 Modbus RTU
SQLite 配置驱动 设备和点位配置存储在 SQLite 中,支持运行时热重载
批量读取优化 自动合并同类型、地址连续的寄存器,大幅减少通信次数
多数据类型 支持 BOOLEAN/INT16/UINT16/INT32/FLOAT/DOUBLE 六种数据类型
四种寄存器类型 COIL/DISCRETE_INPUT/HOLDING_REGISTER/INPUT_REGISTER

三、系统架构

#mermaid-svg-ydJTGBFcUOQ3n3sF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ydJTGBFcUOQ3n3sF .error-icon{fill:#552222;}#mermaid-svg-ydJTGBFcUOQ3n3sF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ydJTGBFcUOQ3n3sF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .marker.cross{stroke:#333333;}#mermaid-svg-ydJTGBFcUOQ3n3sF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ydJTGBFcUOQ3n3sF p{margin:0;}#mermaid-svg-ydJTGBFcUOQ3n3sF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .cluster-label text{fill:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .cluster-label span{color:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .cluster-label span p{background-color:transparent;}#mermaid-svg-ydJTGBFcUOQ3n3sF .label text,#mermaid-svg-ydJTGBFcUOQ3n3sF span{fill:#333;color:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .node rect,#mermaid-svg-ydJTGBFcUOQ3n3sF .node circle,#mermaid-svg-ydJTGBFcUOQ3n3sF .node ellipse,#mermaid-svg-ydJTGBFcUOQ3n3sF .node polygon,#mermaid-svg-ydJTGBFcUOQ3n3sF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .rough-node .label text,#mermaid-svg-ydJTGBFcUOQ3n3sF .node .label text,#mermaid-svg-ydJTGBFcUOQ3n3sF .image-shape .label,#mermaid-svg-ydJTGBFcUOQ3n3sF .icon-shape .label{text-anchor:middle;}#mermaid-svg-ydJTGBFcUOQ3n3sF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .rough-node .label,#mermaid-svg-ydJTGBFcUOQ3n3sF .node .label,#mermaid-svg-ydJTGBFcUOQ3n3sF .image-shape .label,#mermaid-svg-ydJTGBFcUOQ3n3sF .icon-shape .label{text-align:center;}#mermaid-svg-ydJTGBFcUOQ3n3sF .node.clickable{cursor:pointer;}#mermaid-svg-ydJTGBFcUOQ3n3sF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .arrowheadPath{fill:#333333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ydJTGBFcUOQ3n3sF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ydJTGBFcUOQ3n3sF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ydJTGBFcUOQ3n3sF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ydJTGBFcUOQ3n3sF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .cluster text{fill:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF .cluster span{color:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ydJTGBFcUOQ3n3sF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ydJTGBFcUOQ3n3sF rect.text{fill:none;stroke-width:0;}#mermaid-svg-ydJTGBFcUOQ3n3sF .icon-shape,#mermaid-svg-ydJTGBFcUOQ3n3sF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ydJTGBFcUOQ3n3sF .icon-shape p,#mermaid-svg-ydJTGBFcUOQ3n3sF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ydJTGBFcUOQ3n3sF .icon-shape .label rect,#mermaid-svg-ydJTGBFcUOQ3n3sF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ydJTGBFcUOQ3n3sF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ydJTGBFcUOQ3n3sF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ydJTGBFcUOQ3n3sF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 加载设备与点位
TCP 连接
RTU 串口
Flume Event JSON
SQLite 配置数据库
ModbusSource
Modbus TCP 设备

PLC / 传感器
Modbus RTU 设备

电表 / 仪表
Flume Channel
Flume Sink

Kafka / HDFS / Logger

核心组件职责

组件 职责
SQLiteConfigLoader 从 SQLite 读取设备连接参数和采集点位配置
ModbusClientFactory 根据协议类型创建对应的 Modbus 客户端
ModbusTcpClient / ModbusRtuClient 执行 Modbus 协议通信
ModbusDataParser 将原始寄存器数据解析为目标数据类型
RegisterGrouper 核心优化:将寄存器合并为批量读取组
ModbusSource Flume Source 主类,驱动整个采集流程

四、核心亮点:批量读取优化机制

1. 问题背景

Modbus 协议每次请求只能读取一段连续地址范围。如果一个设备配置了 N 个点位,朴素实现需要 N 次通信,效率极低。

2. 优化策略

通过 RegisterGrouper 实现智能分组:

  1. 按类型分组:COIL/DISCRETE_INPUT/HOLDING_REGISTER/INPUT_REGISTER 各自独立
  2. 按地址排序:同类型内按起始地址升序排列
  3. 贪心合并 :地址间隔 ≤ batch.max.gap(默认 10)时合并为同一组

3. 效果对比

场景 优化前(逐点读取) 优化后(批量读取)
10 个连续 HOLDING_REGISTER 10 次事务 1 次事务
地址 0~9 和 100~109 各 10 个 20 次事务 2 次事务
混合 COIL + HOLDING + INPUT N 次事务 按类型各 1 次

4.分组示例

复制代码
设备点位配置:
  HOLDING_REGISTER  addr=0,  count=1  (温度)
  HOLDING_REGISTER  addr=1,  count=1  (湿度)
  HOLDING_REGISTER  addr=2,  count=2  (气压, FLOAT)
  HOLDING_REGISTER  addr=100,count=4  (累计电量, DOUBLE)
  COIL              addr=0,  count=1  (告警状态)

分组结果 (batch.max.gap=10):
  Group 1: HOLDING_REGISTER [0..3]  → 1 次 readHoldingRegisters(0, 4)
  Group 2: HOLDING_REGISTER [100..103] → 1 次 readHoldingRegisters(100, 4)
  Group 3: COIL [0..0] → 1 次 readCoils(0, 1)

总计: 3 次 Modbus 事务 (原 5 次)

五、SQLite 配置说明

1. 数据库初始化

bash 复制代码
sqlite3 modbus_config.db < src/main/resources/init.sql

2. 设备配置表(modbus_device)

sql 复制代码
-- Modbus TCP 设备示例
INSERT INTO modbus_device (name, protocol, host, port, unit_id, poll_interval_ms, enabled)
VALUES ('温湿度传感器', 'TCP', '192.168.1.100', 502, 1, 5000, 1);

-- Modbus RTU 设备示例
INSERT INTO modbus_device (name, protocol, host, port, serial_port, unit_id, poll_interval_ms, enabled)
VALUES ('电表', 'RTU', NULL, 9600, '/dev/ttyUSB0', 1, 10000, 1);

3. 点位配置表(modbus_register)

sql 复制代码
-- 温度(INT16,1个保持寄存器)
INSERT INTO modbus_register (device_id, name, register_type, address, count, data_type)
VALUES (1, '温度', 'HOLDING_REGISTER', 0, 1, 'INT16');

-- 累计电量(DOUBLE,占4个保持寄存器)
INSERT INTO modbus_register (device_id, name, register_type, address, count, data_type)
VALUES (2, '累计电量', 'HOLDING_REGISTER', 20, 4, 'DOUBLE');

4.支持的数据类型

数据类型 占用寄存器数 说明
BOOLEAN 1 布尔值
INT16 1 16位有符号整数
UINT16 1 16位无符号整数
INT32 2 32位有符号整数(大端序)
FLOAT 2 32位 IEEE 754 浮点数
DOUBLE 4 64位 IEEE 754 双精度浮点数

六、Flume 集成部署

1. 编译打包

bash 复制代码
mvn clean package -DskipTests

编译产物为 target/FlumeModbusSource-1.0-SNAPSHOT.jar(包含所有依赖的 fat jar)。

2.Flume Agent 配置

properties 复制代码
# Agent 组件定义
modbusAgent.sources=modbusSource
modbusAgent.sinks=kafkaSink
modbusAgent.channels=memoryChannel

# Modbus Source 配置
modbusAgent.sources.modbusSource.type=hmq.flume.source.ModbusSource
modbusAgent.sources.modbusSource.sqlite.db.path=/opt/flume/modbus_config.db
modbusAgent.sources.modbusSource.reload.interval=60
modbusAgent.sources.modbusSource.batch.max.gap=10

# Memory Channel 配置
modbusAgent.channels.memoryChannel.type=memory
modbusAgent.channels.memoryChannel.capacity=10000
modbusAgent.channels.memoryChannel.transactionCapacity=1000

# Kafka Sink 配置
modbusAgent.sinks.kafkaSink.type=org.apache.flume.sink.kafka.KafkaSink
modbusAgent.sinks.kafkaSink.channel=memoryChannel
modbusAgent.sinks.kafkaSink.kafka.bootstrap.servers=localhost:9092
modbusAgent.sinks.kafkaSink.kafka.topic=modbus-data

3.启动命令

bash 复制代码
flume-ng agent \
  -n modbusAgent \
  -f flume-modbus.conf \
  -Dflume.root.logger=INFO,console

七、Flume Event 格式

1.Header 结构

Key 示例值 说明
device_name 温湿度传感器 设备名称
register_name 温度 点位名称
data_type INT16 数据类型
timestamp 1718352000000 采集时间戳

2.Body 结构(JSON)

json 复制代码
{
  "device": "温湿度传感器",
  "register": "温度",
  "value": 256,
  "timestamp": 1718352000000
}

八、Docker 模拟环境

项目提供完整的 Docker 模拟环境,一键启动即可体验:

bash 复制代码
cd docker
docker-compose up -d

# 查看实时采集日志
docker logs -f flume-agent

Docker 环境包含

  • Modbus Slave 模拟器:基于 Python pymodbus
  • Flume Agent:集成 Modbus Source
  • SQLite 配置数据库:预配置测试数据

九、运行效果展示

启动后日志输出示例:

复制代码
2026-06-22 11:37:08,782 [INFO] Modbus TCP 连接成功: modbus-slave:502
2026-06-22 11:37:08,783 [INFO] 设备[1]已加载 3 个点位配置
2026-06-22 11:37:08,783 [INFO] 共 3 个寄存器被分为 2 个批量读取组
2026-06-22 11:37:08,840 [INFO] Event: { headers:{device_name=TestDevice, register_name=temperature, data_type=FLOAT} body: {"device":"TestDevice","register":"temperature","value":25.5,"timestamp":1782128228839} }

十、技术栈

组件 版本 用途
Apache Flume 1.9.0 数据采集框架
j2mod 2.7.0 Modbus TCP/RTU 通信
SQLite JDBC 3.42.0.0 SQLite 数据库访问
Gson 2.10.1 JSON 序列化

结语

Flume Modbus Source 为工业数据采集提供了一套完整、高效的解决方案。通过 SQLite 配置驱动和批量读取优化,实现了灵活配置与高效采集的完美结合。项目已在 GitHub/Gitee 开源,欢迎 Star 和 Fork!

项目地址https://gitee.com/huangmingquan/flume-modbus-source