去年在为一个农业温室项目搭建监控系统时,我深刻体会到物联网设备数据管理的重要性。当时使用了现成的商业平台,不仅费用高昂,还遇到数据导出受限、自定义报警规则复杂等问题。这促使我开发了Data Shore这个轻量级解决方案,它完美解决了中小型IoT场景下的四大核心需求:
技术选型上采用Node.js+Express的组合,主要考虑到:
当温度传感器(设备ID: dev001)通过MQTT上报数据时,系统处理流程如下:
devices/dev001/data主题消息javascript复制// mqttService.js核心处理逻辑
client.on('message', (topic, payload) => {
const [_, deviceId, dataType] = topic.split('/');
const data = JSON.parse(payload.toString());
if(dataType === 'data') {
deviceService.saveData(deviceId, data); // 存储设备数据
faultService.checkThresholds(deviceId, data); // 阈值检查
}
});
考虑到IoT数据的高写入频率,我们做了以下优化:
temperature_data、humidity_data)sql复制-- 设备表示例
CREATE TABLE `devices` (
`device_id` VARCHAR(50) PRIMARY KEY,
`name` VARCHAR(100) NOT NULL,
`type` ENUM('temperature','humidity','pressure') NOT NULL,
`location` POINT SRID 4326, -- 存储地理坐标
`status` ENUM('online','offline','warning') DEFAULT 'offline',
`meta` JSON DEFAULT NULL, -- 扩展属性
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
关键提示:使用MySQL 8.0+的JSON类型字段存储动态属性,避免频繁修改表结构
采用双重认证机制确保设备合法性:
javascript复制// 设备注册接口
router.post('/devices', async (req, res) => {
const { device_id, secret } = req.body;
// 生成设备专属token(有效期30天)
const token = jwt.sign({ device_id }, SECRET_KEY, {
expiresIn: '30d'
});
await pool.execute(
'INSERT INTO devices SET ?',
{ device_id, auth_token: hashToken(token) }
);
res.json({ success: true, token });
});
针对高频数据写入场景(如每秒百级数据点),我们采用:
javascript复制// database.js配置示例
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 50, // 根据服务器配置调整
queueLimit: 5000,
timezone: '+08:00'
});
现象:设备数据延迟入库
排查步骤:
$SYS/broker/messages/stored主题bash复制DEBUG=mqtt:* npm run dev
解决方案:
典型报错:ER_CON_COUNT_ERROR
预防措施:
javascript复制async function query(sql, params) {
const conn = await pool.getConnection();
try {
const [rows] = await conn.query(sql, params);
return rows;
} finally {
conn.release(); // 关键!
}
}
javascript复制pool.on('acquire', (conn) => {
console.log(`Connection ${conn.threadId} acquired`);
});
实时推送设备状态变更到前端:
javascript复制// 在Express中集成WS
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
// 监听设备状态变更事件
eventEmitter.on('deviceUpdate', (data) => {
ws.send(JSON.stringify(data));
});
});
扩展weather服务支持实时数据获取:
javascript复制const fetchWeather = async (location) => {
const response = await axios.get(`https://api.weatherapi.com/v1/current.json`, {
params: {
key: process.env.WEATHER_API_KEY,
q: location
}
});
await weatherService.saveData({
location,
temperature: response.data.current.temp_c,
humidity: response.data.current.humidity,
// 其他字段...
});
};
通过压力测试发现两个关键瓶颈及解决方案:
MQTT消息处理延迟
历史数据查询缓慢
sql复制-- 分区表示例
CREATE TABLE `device_data_2023` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`device_id` VARCHAR(50) NOT NULL,
`value` DECIMAL(10,2) NOT NULL,
`recorded_at` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`, `recorded_at`),
INDEX `idx_device_time` (`device_id`, `recorded_at`)
) PARTITION BY RANGE (UNIX_TIMESTAMP(recorded_at)) (
PARTITION p01 VALUES LESS THAN (UNIX_TIMESTAMP('2023-02-01')),
PARTITION p02 VALUES LESS THAN (UNIX_TIMESTAMP('2023-03-01')),
...
);
使用Docker Compose编排服务:
yaml复制version: '3'
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- mysql
- mqtt
environment:
DB_HOST: mysql
MQTT_BROKER: mqtt://mqtt:1883
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
mqtt:
image: eclipse-mosquitto
ports:
- "1883:1883"
- "9001:9001"
volumes:
mysql_data:
推荐监控指标及工具:
系统层面:
业务层面:
bash复制# PM2监控配置
pm2 monit
pm2 install pm2-prometheus-exporter
现象:数据库时间比实际晚8小时
原因:MySQL默认使用UTC时区
解决:
javascript复制mysql.createPool({
timezone: '+08:00'
});
关键代码:
javascript复制client.on('connect', () => {
console.log('MQTT connected');
initSubscriptions(); // 重连后重新订阅
});
client.on('error', (err) => {
console.error('MQTT error', err);
scheduleReconnect(); // 指数退避重连
});
function scheduleReconnect() {
const delay = Math.min(30, retryCount) * 1000;
setTimeout(() => {
client.reconnect();
retryCount++;
}, delay);
}
mermaid复制graph LR
A[设备端] -->|MQTT/CoAP| B(Edge Gateway)
B -->|HTTP/WebSocket| C[Data Shore]
C --> D{数据分析}
D -->|告警| E[通知服务]
D -->|报表| F[可视化平台]
(注:实际实现时应根据具体需求选择扩展方向,农业项目可优先考虑离线边缘计算功能)
bash复制# 1. 注册设备
curl -X POST http://localhost:3000/api/devices \
-H "Content-Type: application/json" \
-d '{
"device_id": "temp_sensor_01",
"name": "温室温度传感器",
"type": "temperature",
"location": "Greenhouse-A"
}'
# 2. 获取设备token(用于MQTT认证)
curl -X POST http://localhost:3000/api/devices/temp_sensor_01/token
# 3. 通过MQTT上报数据(使用mosquitto_pub)
mosquitto_pub -t devices/temp_sensor_01/data \
-m '{"value":25.3,"unit":"celsius"}' \
-u temp_sensor_01 -P [token]
# 4. 查询设备数据
curl "http://localhost:3000/api/devices/temp_sensor_01/data?limit=5"
bash复制# 1. 模拟设备上报故障
mosquitto_pub -t devices/temp_sensor_01/fault \
-m '{
"code": "HIGH_TEMP",
"message": "温度超过安全阈值",
"value": 42.5
}'
# 2. 查询未解决故障
curl "http://localhost:3000/api/faults?resolved=false"
# 3. 标记故障为已解决
curl -X PUT http://localhost:3000/api/faults/123/resolve
在2核4G云服务器上的测试结果:
| 场景 | 请求量 | 平均响应时间 | 错误率 |
|---|---|---|---|
| 设备注册 | 1000次/min | 23ms | 0% |
| 数据上报 | 5000次/min | 56ms | 0.2% |
| 历史查询 | 100次/min | 320ms | 0% |
| 复合操作 | 2000次/min | 189ms | 1.5% |
优化建议:
推荐的项目结构扩展:
code复制src/
├── lib/ # 公共库
│ ├── logger.js # 统一日志处理
│ └── validator.js # 参数校验
├── jobs/ # 定时任务
│ ├── dataCleanup.js # 数据归档
│ └── deviceCheck.js # 设备状态检查
└── test/ # 测试代码
├── integration/ # 集成测试
└── unit/ # 单元测试
测试示例(使用Jest):
javascript复制describe('Device Service', () => {
beforeAll(async () => {
await resetTestDatabase();
});
test('should create device', async () => {
const device = await deviceService.create({
device_id: 'test_001',
name: 'Test Device'
});
expect(device).toHaveProperty('token');
expect(device.status).toBe('offline');
});
});
javascript复制const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', limiter);
javascript复制router.get('/devices', (req, res) => {
const devices = await deviceService.list();
// 过滤掉auth_token等敏感字段
const safeData = devices.map(d => _.omit(d, ['auth_token']));
res.json(safeData);
});
javascript复制const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert')
};
https.createServer(options, app).listen(443);
javascript复制const mqttOptions = {
clientId: 'backend',
ca: fs.readFileSync('./ca.crt'),
rejectUnauthorized: true
};
使用winston进行分级日志记录:
javascript复制const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// 在中间件中记录访问日志
app.use((req, res, next) => {
logger.info({
method: req.method,
path: req.path,
ip: req.ip
});
next();
});
扩展/health接口返回更多系统指标:
javascript复制router.get('/health', async (req, res) => {
const [dbStatus, mqttStatus] = await Promise.all([
checkDatabase(),
checkMQTT()
]);
res.json({
status: dbStatus && mqttStatus ? 'healthy' : 'degraded',
uptime: process.uptime(),
memory: process.memoryUsage(),
db: dbStatus,
mqtt: mqttStatus
});
});
使用Vue.js调用API的示例:
javascript复制// 设备列表组件
export default {
data() {
return {
devices: []
}
},
async created() {
const res = await axios.get('/api/devices');
this.devices = res.data.data;
},
methods: {
async updateStatus(deviceId, status) {
await axios.put(`/api/devices/${deviceId}`, { status });
}
}
}
Android设备上报数据示例(Kotlin):
kotlin复制fun publishData(deviceId: String, value: Double) {
val mqttOptions = MqttConnectOptions().apply {
userName = deviceId
password = getToken().toCharArray()
}
val client = MqttAsyncClient("tcp://mqtt.example.com:1883", deviceId)
client.connect(mqttOptions)
val payload = JSONObject()
.put("value", value)
.put("timestamp", System.currentTimeMillis())
.toString()
.toByteArray()
client.publish("devices/$deviceId/data",
MqttMessage(payload).apply { qos = 1 })
}
配置MySQL数据源后,可以创建:
sql复制-- 温度周报表查询
SELECT
DATE(recorded_at) AS day,
AVG(value) AS avg_temp,
MAX(value) AS max_temp
FROM temperature_data
WHERE device_id = 'temp_sensor_01'
AND recorded_at > NOW() - INTERVAL 7 DAY
GROUP BY day
ORDER BY day;
实现按月统计的端点:
javascript复制router.get('/api/devices/:id/stats', async (req, res) => {
const { id } = req.params;
const { period = 'day' } = req.query;
const sql = `
SELECT
DATE_FORMAT(recorded_at, ?) AS time_unit,
AVG(value) AS avg_value
FROM device_data
WHERE device_id = ?
GROUP BY time_unit
`;
const formatMap = {
day: '%Y-%m-%d',
month: '%Y-%m',
hour: '%Y-%m-%d %H:00'
};
const [rows] = await pool.execute(sql, [formatMap[period], id]);
res.json({ success: true, data: rows });
});
使用mysqldump创建自动化备份脚本:
bash复制#!/bin/bash
BACKUP_DIR=/var/backups/data_shore
DATE=$(date +%Y%m%d)
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME > $BACKUP_DIR/dump_$DATE.sql
find $BACKUP_DIR -type f -mtime +30 -delete # 保留30天
关键配置管理建议:
bash复制# 加密敏感配置
ansible-vault encrypt production.env
高可用架构示例:
code复制 [HAProxy]
|
+--------------+--------------+
| | |
[Node.js实例1] [Node.js实例2] [Node.js实例3]
| | |
[Redis] [MySQL集群] [MQTT集群]
关键配置点:
使用VS Code调试配置:
json复制{
"type": "node",
"request": "launch",
"name": "Debug MQTT",
"skipFiles": ["<node_internals>/**"],
"envFile": "${workspaceFolder}/.env",
"runtimeArgs": ["--inspect"],
"program": "${workspaceFolder}/src/app.js"
}
推荐工具链:
package.json示例:
json复制{
"scripts": {
"lint": "eslint src/",
"test": "jest",
"precommit": "npm run lint && npm test"
},
"husky": {
"hooks": {
"pre-commit": "npm run precommit"
}
}
}
建议的开发优先级:
基础功能完善(1-2周)
性能优化(2-3周)
高级功能(3-4周)
生态扩展(持续迭代)
实际开发中,建议先通过MVP验证核心业务流程,再逐步扩展功能模块。我们在实施智慧农业项目时,就是先确保温度监控的稳定性,再逐步加入灌溉控制等复杂功能。