文章目录
- [工业数据采集利器:Flume Modbus Source 深度解析与实践](#工业数据采集利器:Flume Modbus Source 深度解析与实践)
工业数据采集利器:Flume Modbus Source 深度解析与实践
一、项目背景
在工业物联网(IoT)场景中,Modbus 协议是最常用的工业通信协议之一,广泛应用于 PLC、传感器、电表等设备的数据采集。然而,传统的数据采集方案存在以下痛点:
- 协议支持有限:多数方案仅支持 Modbus TCP,对 RTU 串口设备支持不足
- 配置僵化:设备点位配置硬编码,变更需重启服务
- 通信效率低:逐点读取寄存器,通信开销大、延迟高
针对以上问题,我开发了 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 实现智能分组:
- 按类型分组:COIL/DISCRETE_INPUT/HOLDING_REGISTER/INPUT_REGISTER 各自独立
- 按地址排序:同类型内按起始地址升序排列
- 贪心合并 :地址间隔 ≤
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!