IoT Data Pipeline: Ingestion, Processing & Storage

Chào mừng bạn đến với IOT-7 trong IoT Patterns & Strategies Roadmap! Cho đến giờ, chúng ta đã bới device management và firmware update. Bây giờ chúng ta cần xử lý những gì thiết bị thực sự phát sinh: data. Rất rất nhiều data.
Một cảm biến nhiệt độ có thể gửi 1 đo lường mỗi 5 giây. 10.000 cảm biến có nghĩa là 2.000 readings/giây. 100.000 cảm biến = 20.000 readings/giây. Thêm vibration sensor, pressure sensor, GPS coordinate, ảnh từ edge camera — và bỗng nhiên bạn phải xử lý hàng gigabyte data được đẩy từ nơi lắp đặt lên cloud mỗi phút.
Nhưng challenge thực sự là: Bạn không thể chỉ dump raw sensor data vào database. Bạn cần:
- Ingest nó reliably mà không mất message
- Transform và enrich nó real-time
- Detect anomaly khi data arrive
- Store efficiently cho cả immediate query lẫn historical analysis
- Scale từ hàng trăm thiết bị đến hàng triệu
Bài viết này bao gồm cách production IoT system giải quyết vấn đề này.
Bạn sẽ học được gì
✅ Hiểu đặc điểm IoT data và tại sao standard database fail
✅ Thiết kế ingestion architecture (MQTT → message queue → processing)
✅ Chọn giữa stream vs batch processing với clear trade-off
✅ Nắm vững Lambda và Kappa architecture cho IoT
✅ Triển khai real-time stream processing với Kafka Streams
✅ Transform và enrich data trong pipeline
✅ Detect anomaly và quality issue khi data flows
✅ Thiết kế storage strategy: hot data, warm data, cold archive
✅ Xây dựng complete end-to-end pipeline với code example
Challenge của IoT Data
IoT data fundamentally khác với traditional application data. Hãy hiểu tại sao standard approach break.
Đặc điểm IoT Data
Tại sao Traditional Database Fail
Traditional Database (e.g., PostgreSQL):
- ✗ Optimized cho OLTP (frequent update, point lookup)
- ✗ Row-oriented storage (bad for time-series)
- ✗ Index bloat với high-cardinality dimension
- ✗ Slow aggregation qua time range
- ✗ Cần VACUUM (stall query)
IoT Time-Series Database (e.g., InfluxDB):
- ✓ Optimized cho OLAP (sequential scan, aggregation)
- ✓ Column-oriented storage (compress tỷ monotonic timestamp)
- ✓ Smart index cho time range (không individual row)
- ✓ Efficient downsampling (1 query → giây vs phút)
- ✓ Built-in retention policy (auto cleanup)IoT Data Pipeline Architecture
Complete pipeline có 4 layer:
Layer 1: Ingestion (MQTT → Message Queue)
Thiết bị publish đến MQTT. Message broker ensure message không bị mất:
MQTT (protocol) ← Devices (telemetry)
↓
MQTT Broker (Mosquitto, EMQX, HiveMQ) ← Stores message tạm thời
↓
Kafka / RabbitMQ ← Persistent message queue
↓
Stream Processor ← Multiple consumer có thể process cùng messageLayer 2: Processing (Stream Processing)
Real-time transformation, enrichment, và filtering:
Raw Event: {"deviceId":"temp-007","value":22.5,"ts":1673456789}
↓
Transform: Add location từ device registry
↓
Enrich: Add threshold từ device config
↓
Filtered: Only pass nếu value > threshold
↓
Output: {"deviceId":"temp-007","value":22.5,"location":"Berlin","threshold":25,"alert":false}Layer 3: Storage (Hot/Warm/Cold)
Các access pattern khác nhau cần storage khác:
Hot Data (Recent): TimescaleDB → Sub-second query, dashboard
Warm Data (Historical): ClickHouse → Batch analytics, reports
Cold Data (Archive): S3/GCS → Compliance, long-term analysisLayer 4: Consumption (API, Dashboard, Alert)
Data flows tới end user và system.
Stream Processing vs Batch Processing
Classic trade-off: Speed vs Simplicity
| Aspect | Stream Processing | Batch Processing |
|---|---|---|
| Latency | Giây | Giờ (end-of-day) |
| Use Case | Real-time alert, dashboard | Analytics, reports, ML |
| State | Khó (phải manage) | Dễ (stateless) |
| Tools | Kafka Streams, Flink, Spark Streaming | Spark, Hadoop, SQL |
| Cost | Continuous (24/7) | Variable (compute on-demand) |
| Complexity | Cao (windowing, state) | Thấp (simple SQL) |
| Example | Alert nếu temp > 30°C now | Daily average temp by location |
Khi nào dùng từng cái
Dùng Stream Processing nếu:
- Need alert hoặc action trong vài giây
- Dashboard need live data
- Anomaly detection phải real-time
- Example: "Alert when pressure spike detected"
Dùng Batch Processing nếu:
- Analysis có thể chờ giờ/ngày
- Generate report hoặc ML training data
- Simpler logic, less infrastructure
- Example: "Daily average temperature by location"
Dùng cả hai (Lambda Architecture) nếu:
- Need cả real-time alert AND accurate daily report
- Có thể tolerate more infrastructure complexity
Lambda vs Kappa Architecture
Cách thiết kế pipeline phục vụ cả speed và accuracy layer.
Lambda Architecture
Hai parallel stream: Speed Layer (real-time, approximate) + Batch Layer (accurate, slow)
Pros:
- ✓ Fast approximate + accurate result
- ✓ Có thể recover từ stream processor fail
- ✓ Proven pattern at scale (Netflix, Twitter)
Cons:
- ✗ Double infrastructure (stream + batch)
- ✗ Phải keep cả hai in sync
- ✗ Complex serving layer logic
Kappa Architecture
Single stream path. Dùng event log as replay source.
Pros:
- ✓ Simpler: one code path
- ✓ Dễ reason about
- ✓ Less infrastructure overhead
- ✓ Có thể recompute từ scratch anytime
Cons:
- ✗ Historical data needed (large Kafka log)
- ✗ Recomputation takes time
- ✗ Stream processor phải deterministic
Lambda vs Kappa Decision Tree
Bạn có cần accurate historical analytics?
├─ Có
│ └─ Có spare infrastructure?
│ ├─ Có → Lambda (speed + batch layer)
│ └─ Không → Stream-only with replay (Kappa)
└─ Không
└─ Kappa (simpler, pure streaming)Khuyến cáo cho IoT:
- Bắt đầu với Kappa nếu ingestion reliable (Kafka)
- Upgrade lên Lambda nếu cần bucketing/aggregation expensive để recompute
Stream Processing với Kafka Streams
Kafka Streams là library cho processing data trong Kafka topic. Không separate infrastructure — chạy trong application của bạn.
Basic Topology: Transform + Filter
// Node.js with Kafka Streams (using Kafka-js client)
const kafka = new Kafka({
clientId: 'iot-processor',
brokers: ['kafka:9092'],
});
const consumer = kafka.consumer({ groupId: 'iot-processing' });
const producer = kafka.producer();
await consumer.connect();
await producer.connect();
await consumer.subscribe({ topic: 'devices.telemetry' });
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
// Parse raw event
const event = JSON.parse(message.value.toString());
// Transform: lookup device metadata
const device = await getDeviceMetadata(event.deviceId);
// Enrich: add location and threshold
const enriched = {
...event,
location: device.location,
threshold: device.threshold,
timestamp: Date.now(),
};
// Filter: only alert if exceeds threshold
if (enriched.value > enriched.threshold) {
// Send to alert topic
await producer.send({
topic: 'devices.alerts',
messages: [
{
key: enriched.deviceId,
value: JSON.stringify(enriched),
},
],
});
}
// Send all event (enriched) to storage
await producer.send({
topic: 'devices.enriched',
messages: [
{
key: enriched.deviceId,
value: JSON.stringify(enriched),
},
],
});
},
});Stateful Processing: Windowing & Aggregation
Tính 5-phút average:
// Pseudo-code cho stateful Kafka Streams
// Trong Java/Scala, dùng actual Kafka Streams library
const windowState = new Map<string, WindowData>();
const WINDOW_SIZE = 5 * 60 * 1000; // 5 phút
await consumer.run({
eachMessage: async ({ message }) => {
const event = JSON.parse(message.value.toString());
const deviceId = event.deviceId;
// Tạo window key: device + time bucket
const windowStart = Math.floor(event.ts / WINDOW_SIZE) * WINDOW_SIZE;
const windowKey = `${deviceId}:${windowStart}`;
// Initialize window nếu new
if (!windowState.has(windowKey)) {
windowState.set(windowKey, {
sum: 0,
count: 0,
min: Infinity,
max: -Infinity,
});
}
// Update window
const window = windowState.get(windowKey);
window.sum += event.value;
window.count++;
window.min = Math.min(window.min, event.value);
window.max = Math.max(window.max, event.value);
// Emit aggregated result
const result = {
deviceId,
window: {
start: windowStart,
end: windowStart + WINDOW_SIZE,
},
stats: {
avg: window.sum / window.count,
min: window.min,
max: window.max,
count: window.count,
},
};
await producer.send({
topic: 'devices.aggregated',
messages: [{ key: deviceId, value: JSON.stringify(result) }],
});
},
});Data Transformation & Enrichment
Raw sensor data thường incomplete. Thêm context trước khi store.
Transformation Pipeline
interface RawEvent {
deviceId: string;
sensorId: string;
value: number;
ts: number;
}
interface EnrichedEvent extends RawEvent {
location: string;
deviceType: string;
zone: string;
unit: string;
threshold: number;
isAnomaly: boolean;
}
async function enrichEvent(raw: RawEvent): Promise<EnrichedEvent> {
// 1. Lookup device metadata
const device = await deviceRegistry.get(raw.deviceId);
if (!device) throw new DeviceNotFoundError(raw.deviceId);
// 2. Lookup sensor configuration
const sensor = device.sensors.find((s) => s.id === raw.sensorId);
if (!sensor) throw new SensorNotFoundError(raw.sensorId);
// 3. Validate value
if (raw.value < sensor.min || raw.value > sensor.max) {
// Out of range
console.warn(`Out of range: ${raw.deviceId}.${raw.sensorId} = ${raw.value}`);
}
// 4. Apply unit conversion (if needed)
const convertedValue = convertUnits(raw.value, sensor.unit, 'SI');
// 5. Detect anomaly
const historical = await getHistorical(raw.deviceId, raw.sensorId, 24 * 60 * 60 * 1000);
const isAnomaly = detectAnomaly(
convertedValue,
historical,
sensor.anomalyThreshold,
);
// 6. Enrich
return {
...raw,
value: convertedValue,
location: device.location,
deviceType: device.deviceType,
zone: device.zone,
unit: sensor.unit,
threshold: sensor.threshold,
isAnomaly,
};
}
function detectAnomaly(
value: number,
historical: number[],
threshold: number,
): boolean {
const mean = historical.reduce((a, b) => a + b) / historical.length;
const std = Math.sqrt(
historical.reduce((sq, n) => sq + (n - mean) ** 2, 0) / historical.length,
);
// Flag nếu > 3 standard deviation từ mean
return Math.abs(value - mean) > threshold * std;
}Data Quality & Anomaly Detection
Không phải all data là good data. Detect và quarantine problem early.
Quality Check
interface DataQualityReport {
total: number;
valid: number;
invalid: number;
duplicate: number;
outOfRange: number;
anomaly: number;
issues: string[];
}
async function validateEvent(event: EnrichedEvent): Promise<DataQualityReport> {
const report: DataQualityReport = {
total: 1,
valid: 1,
invalid: 0,
duplicate: 0,
outOfRange: 0,
anomaly: 0,
issues: [],
};
// Check 1: Missing field
if (!event.deviceId || !event.sensorId || event.value === undefined) {
report.valid = 0;
report.invalid = 1;
report.issues.push('Missing required field');
return report;
}
// Check 2: Duplicate detection (seen recently?)
const recentKey = `${event.deviceId}:${event.sensorId}:${event.value}`;
const lastSeen = await cache.get(recentKey);
if (lastSeen && Date.now() - lastSeen < 1000) {
report.duplicate = 1;
report.issues.push(`Duplicate trong 1s');
}
// Check 3: Out of range
if (event.value < -50 || event.value > 150) {
// Temperature sensor
report.outOfRange = 1;
report.issues.push(`Out of valid range: ${event.value}`);
}
// Check 4: Anomaly (already detected during enrichment)
if (event.isAnomaly) {
report.anomaly = 1;
report.issues.push('Detected anomaly');
}
// Check 5: Schema validation
// (done at ingestion, nhưng good re-check)
return report;
}
// Route event dựa trên quality
async function routeEvent(event: EnrichedEvent) {
const quality = await validateEvent(event);
if (quality.issues.length > 0) {
// Send to quarantine topic để investigate
await producer.send({
topic: 'devices.quarantine',
messages: [
{
key: event.deviceId,
value: JSON.stringify({ event, quality }),
},
],
});
return;
}
// Send to storage
await producer.send({
topic: 'devices.enriched',
messages: [{ key: event.deviceId, value: JSON.stringify(event) }],
});
}Storage: Hot/Warm/Cold Tiers
Quy tắc 90-10: 90% query access recent data, 10% access historical.
Tier Strategy
Data Lifecycle
// Lifecycle management
interface StorageConfig {
hot: {
database: 'timescaledb';
retention: 7 * 24 * 60 * 60 * 1000; // 7 ngày
compression: 'snappy';
};
warm: {
database: 'clickhouse';
retention: 365 * 24 * 60 * 60 * 1000; // 1 năm
compression: 'gzip';
};
cold: {
storage: 's3';
bucket: 'iot-archive';
retention: null; // Indefinite
format: 'parquet';
};
}
// Mỗi giờ, migrate old data
setInterval(async () => {
const now = Date.now();
// Send 7-100 ngày old data từ TimescaleDB sang ClickHouse
const toDrop = now - 14 * 24 * 60 * 60 * 1000; // Older than 14 days
const toWarm = now - 7 * 24 * 60 * 60 * 1000; // Older than 7 days
const warmData = await timescaledb.query(`
SELECT * FROM events
WHERE ts > ${toWarm} AND ts < ${toDrop}
`);
// Insert vào ClickHouse
await clickhouse.insert('events', warmData);
// Delete từ TimescaleDB
await timescaledb.query(`
DELETE FROM events WHERE ts < ${toWarm}
`);
// Mỗi tuần, archive ClickHouse sang S3
const toArchive = now - 365 * 24 * 60 * 60 * 1000;
const archiveData = await clickhouse.query(`
SELECT * FROM events WHERE ts < ${toArchive}
`);
// Convert sang Parquet và upload S3
const parquetFile = await convertToParquet(archiveData);
await s3.upload(`events/${datePartition(toArchive)}.parquet`, parquetFile);
// Delete từ ClickHouse
await clickhouse.query(`DELETE FROM events WHERE ts < ${toArchive}`);
}, 60 * 60 * 1000); // Mỗi giờXây dựng Complete IoT Pipeline
Đây là cách tất cả fit together: MQTT → Kafka → Processing → TimescaleDB → Grafana
Architecture Diagram
Docker Compose Setup
# docker-compose.yml cho IoT pipeline
version: '3.8'
services:
# MQTT Broker
emqx:
image: emqx/emqx:5.0
ports:
- '1883:1883' # MQTT
- '8083:8083' # WebSocket
- '18083:18083' # Dashboard
environment:
EMQX_LOADED_PLUGINS: 'emqx_management,emqx_recon'
# Kafka
kafka:
image: confluentinc/cp-kafka:7.5.0
ports:
- '9092:9092'
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
depends_on:
- zookeeper
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
ports:
- '2181:2181'
environment:
ZOOKEEPER_CLIENT_PORT: 2181
# TimescaleDB (PostgreSQL + TimescaleDB extension)
timescaledb:
image: timescale/timescaledb:latest-pg15
ports:
- '5432:5432'
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: iot
# Stream Processor (chạy Node.js app)
processor:
build: .
depends_on:
- kafka
- emqx
- timescaledb
environment:
KAFKA_BROKERS: kafka:29092
MQTT_URL: mqtt://emqx:1883
DATABASE_URL: postgresql://postgres:password@timescaledb:5432/iot
# Grafana cho dashboard
grafana:
image: grafana/grafana:latest
ports:
- '3000:3000'
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
depends_on:
- timescaledbStream Processor Application
// processor.js - Glue that binds it all together
const mqtt = require('mqtt');
const { Kafka } = require('kafkajs');
const { Client: PG } = require('pg');
const kafka = new Kafka({
clientId: 'iot-processor',
brokers: process.env.KAFKA_BROKERS.split(','),
});
const consumer = kafka.consumer({ groupId: 'iot-processing' });
const producer = kafka.producer();
const pgClient = new PG({
connectionString: process.env.DATABASE_URL,
});
const mqttClient = mqtt.connect(process.env.MQTT_URL);
async function start() {
await consumer.connect();
await producer.connect();
await pgClient.connect();
// Subscribe to raw telemetry
await consumer.subscribe({ topic: 'devices.telemetry' });
console.log('🚀 IoT Stream Processor started');
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
try {
const event = JSON.parse(message.value.toString());
// Transform: add timestamp
event.processedAt = Date.now();
// Enrich: add device metadata
const { rows } = await pgClient.query(
'SELECT * FROM devices WHERE device_id = $1',
[event.deviceId],
);
if (rows.length === 0) {
console.warn(`Device not found: ${event.deviceId}`);
return;
}
event.location = rows[0].location;
event.zone = rows[0].zone;
// Detect anomaly
event.isAnomaly = event.value > rows[0].threshold;
// Insert vào TimescaleDB
await pgClient.query(
`INSERT INTO metrics (device_id, value, location, is_anomaly, ts)
VALUES ($1, $2, $3, $4, to_timestamp($5 / 1000.0))`,
[
event.deviceId,
event.value,
event.location,
event.isAnomaly,
event.ts,
],
);
// Nếu anomaly, gửi alert
if (event.isAnomaly) {
await producer.send({
topic: 'alerts',
messages: [{ key: event.deviceId, value: JSON.stringify(event) }],
});
}
} catch (error) {
console.error('Error processing message:', error);
}
},
});
}
start().catch(console.error);Tóm tắt: IoT Data Pipeline Best Practice
1. **Ingest từ device** → MQTT Broker (Mosquitto, EMQX)
2. **Buffer trong queue** → Kafka (guarantee durability)
3. **Stream process** → Kafka Streams (transform, enrich, filter)
4. **Detect anomaly** → Real-time threshold + statistical model
5. **Store hot data** → TimescaleDB (7 ngày, sub-100ms query)
6. **Migrate warm data** → ClickHouse (analytical query, 1 năm)
7. **Archive cold data** → S3 Parquet (compliance, indefinite)
8. **Visualize** → Grafana dashboard
9. **Alert** → Automatic rule evaluation
10. **Iterate** → Monitor pipeline health, adjust thresholdChecklist cho Building Pipeline của bạn
- ✅ MQTT broker resilient (cluster hoặc HA setup)
- ✅ Kafka topic partitioned theo device ID (parallelism)
- ✅ Stream processor handle failure gracefully (exactly-once semantic)
- ✅ Data enrichment cached (không hit DB mỗi event)
- ✅ Anomaly detection có baseline (không chỉ static threshold)
- ✅ Hot storage có TTL policy (auto-delete old data)
- ✅ Warm storage queryable (SQL interface)
- ✅ Cold storage cost-optimized (Parquet, compression)
- ✅ Monitoring cover pipeline lag (broker → processor → storage)
Khuyến cáo Đọc Thêm
- Kafka: The Definitive Guide (Neha Narkhede et al.)
- Designing Data-Intensive Application (Martin Kleppmann)
- TimescaleDB Documentation: Best practice cho time-series
- ClickHouse Documentation: Analytics on IoT data
Series: IoT Patterns & Strategies Roadmap
Previous: Device Management & OTA Updates
Next: IoT Security: Device Identity, Encryption & Zero Trust
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.