IoT Workshop #1: ออกแบบ Architecture ก่อนลงมือจริง
IoT Workshop #1: ออกแบบ Architecture ก่อนลงมือจริง
Branch:
step-01-fiber-bootstrap(จุดเริ่ม) — อ้างอิงภาพรวมจากstep-19-e2ePhase: Planning (1/3) Repo: kangana1024/showkhun-workshop
มีโปรเจกต์หลายอันที่ล้มเหลวไม่ใช่เพราะโค้ดแย่ แต่เพราะ ไม่เคยคิดก่อนสร้าง เลย (╥_╥)
เราเลยอยากให้ Workshop ซีรีส์นี้เริ่มต้นด้วยการ “คิดก่อน code” — ออกแบบ Architecture ให้แน่น ก่อนที่น้องๆ จะเปิด IDE แม้แต่บรรทัดเดียว มาลุยกันเลย!
สิ่งที่น้องๆ จะได้จากบทความนี้
- เข้าใจภาพรวม System Architecture แบบ Event-Driven สำหรับ IoT
- เห็น Data Flow ทั้ง 3 แบบ: รับข้อมูล (ingest), ส่งคำสั่ง (command), ดึง History (query)
- รู้จัก Communication Patterns — REST, MQTT, WebSocket ใช้ตอนไหน
- เข้าใจ ว่าทำไม ถึงเลือก Tech Stack แต่ละตัว (ไม่ใช่แค่ “มันดี”)
- รู้ว่าทำไมเราเขียน alerting เป็น Go เอง แทนที่จะใช้ Kapacitor
- เห็น Deployment ทั้ง Dev และ Production
ก่อนอื่น — ทำไมต้อง Architecture First?
ลองนึกภาพว่าน้องๆ กำลังจะสร้างบ้าน แล้วเริ่มสั่งอิฐก้อนแรกโดยยังไม่มีแบบแปลน…
นั่นแหละปัญหา เพราะพอผนังขึ้นไปแล้วค่อยรู้ว่าประตูอยู่ผิดที่ มันรื้อยากมาก IoT System ก็เหมือนกัน — ถ้าไม่คิด Data Flow ไว้ก่อน พอ Sensor ส่งข้อมูลมา 1,000 เครื่องพร้อมกัน ระบบ crash แน่นอน
Architecture คือ แบบแปลน ที่บอกว่าแต่ละส่วนอยู่ที่ไหน คุยกันยังไง และมีทางออกฉุกเฉินไหม
ภาพรวมของระบบ — Event-Driven Architecture
ระบบ IoT Platform ที่เราจะสร้างในซีรีส์นี้ทำงานแบบ Event-Driven — ทุกอย่างเริ่มจาก “event” ที่ชื่อว่า sensor reading แล้วระบบก็ตอบสนองต่อ event นั้น (เก็บ, broadcast, alert) มาดูภาพรวมกัน:
graph TD
A[🌡️ Devices / Sensors] -->|MQTT 1883| B[📡 Mosquitto Broker]
A -->|REST 3000| C[🖥️ Backend Go + Fiber]
B -->|subscribe devices/+/telemetry| C
B -->|subscribe devices/+/telemetry| T[🔁 Telegraf]
C -->|CRUD| M[(🍃 MongoDB 8)]
C -->|write sensor_data| I[(📈 InfluxDB 2.7)]
T -->|write telegraf_sensor_data| I
C -->|Flux query 8086| I
C -->|alert engine| H[🔔 Webhook]
C -->|WebSocket /api/v1/ws| F1[📱 LynxJS Mobile]
C -->|WebSocket /api/v1/ws| F2[🖥️ Vite Admin]
C -->|publish devices/+/command| B
ดูแล้วอาจรู้สึกว่าเยอะ แต่ไม่ต้องตกใจ — หัวใจมีแค่นี้: ทุก reading ที่เข้ามา ไม่ว่าจะมาทาง REST หรือ MQTT จะวิ่งผ่าน ingestion path เดียวกันใน backend แล้วแตกออกไป 3 ทาง (เขียน InfluxDB / fan-out WebSocket / run alert engine) ส่วน Telegraf เป็น pipeline เสริมที่วิ่งคู่ขนาน
สังเกตให้ดี: ใน architecture จริงของเรา ไม่มี Kapacitor และไม่ได้รัน Chronograf เป็น service — เราเขียน alerting engine เป็น Go เองในตัว backend (เดี๋ยวอธิบายว่าทำไม) infra ที่รันจริงมีแค่ 4 ตัว: MongoDB, Mosquitto, InfluxDB, Telegraf
Data Flow: จาก Sensor ถึง Screen
อยากให้น้องๆ เข้าใจว่าข้อมูลมันเดินทางยังไง ก่อนจะมาดู code ทีหลัง
เปรียบเทียบกับชีวิตจริง: ลองนึกถึงระบบน้ำในบ้าน — น้ำออกจากประปา (Sensor) ผ่านท่อใหญ่ (MQTT/REST) ไปถึงมิเตอร์ (Backend) แล้วค่อยกระจายไปห้องครัว ห้องน้ำ (Frontend) แต่ละจุดมีวาล์วควบคุม และมีถังเก็บน้ำสำรอง (Database) ไว้เผื่อต้องย้อนดูว่าเมื่อกี้ใช้น้ำไปเท่าไหร่
Flow 1: Sensor Data Ingestion (เส้นทางหลัก)
นี่คือเส้นทางที่ข้อมูล sensor วิ่งจาก device มาถึงหน้าจอ user จุดเด็ดคือ REST และ MQTT มาบรรจบที่ ingestion path เดียวกัน:
Sensor Device
│
├──(A) MQTT Publish devices/{device_id}/telemetry
│ payload: {"fields":{"temperature":28.5,"humidity":65},"tags":{"location":"room-a"}}
│ │
│ ├──▶ Mosquitto Broker
│ │ ├──▶ Telegraf (subscribe devices/+/telemetry)
│ │ │ └──▶ InfluxDB measurement: telegraf_sensor_data
│ │ └──▶ Go Backend (subscribe devices/+/telemetry)
│ │
└──(B) REST POST /api/v1/sensors/data ────▶ Go Backend
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
Validate vs registry Write InfluxDB Run alert engine
(MongoDB) + enrich measurement: (threshold/offline/
tags (device_type, sensor_data anomaly) → webhook
group_id) │
▼
Broadcast via WebSocket
│
├──▶ LynxJS Mobile (real-time dashboard)
└──▶ Vite Admin (monitoring)
สังเกตว่า reading จาก ทั้งสองทาง ถูก validate กับ device registry, เติม tag ที่ได้จาก registry (device_type, group_id) แล้วถึงเขียนลง InfluxDB — เพราะใช้ logic เดียวกัน reading จาก REST กับ MQTT จึงได้ผลเหมือนกันเป๊ะ
Flow 2: Device Command (เส้นทางย้อนกลับ)
น้องๆ อยากสั่งให้อุปกรณ์ทำอะไร เช่น เปิด/ปิดไฟ ก็ใช้ Flow นี้
Admin/User Action (Mobile App / Admin Panel)
│
▼ REST API Call
│ POST /api/v1/devices/{id}/commands
│ Body: {"action":"reboot","payload":{"delay":5}}
│
└──▶ Go Backend
│
├──▶ ตรวจ device มีอยู่จริง (MongoDB registry)
└──▶ MQTT Publish devices/{device_id}/command
│ Payload: {"action":"reboot","payload":{"delay":5}}
│
└──▶ Device รับคำสั่ง → ทำงาน (เช่น สั่ง relay)
Flow 3: Historical Data Query
ดูกราฟย้อนหลัง “เมื่อคืนอุณหภูมิสูงสุดเท่าไหร่” — ใช้ Flow นี้ ซึ่งอ่าน InfluxDB ผ่าน Flux (ไม่ใช่ InfluxQL):
User requests chart data
│
▼ REST API Call
│ GET /api/v1/devices/{id}/readings?range=24h&window=1h&aggregate=mean
│
└──▶ Go Backend
│
├──▶ validate device vs registry
├──▶ สร้าง Flux query แบบ bounded + parameterised (กัน injection)
└──▶ Query InfluxDB 2.7
│
└──▶ คืน rows {time, field, value, tags} + meta (ค่าที่ถูก clamp)
│
└──▶ Render chart บน Mobile/Admin
Component Diagram: ข้างในแต่ละส่วนมีอะไร?
Backend Components (Go + Fiber)
Backend เราแบ่งเป็นชั้น เหมือนกับแผนก HR ในบริษัท — มี Receptionist รับงาน (Handler), มี Manager วางแผน (Service), และมี Worker ลงมือทำ (Repository / database):
┌──────────────────────────────────────────────────┐
│ Go Fiber Application │
│ │
│ ┌──────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Router │ │ Middleware │ │ Config │ │
│ │ /api/v1 │ │ - JWT/RBAC │ │ - Viper │ │
│ │ /healthz │ │ - CORS │ │ - .env │ │
│ │ .../ws │ │ - ReqLogger │ │ │ │
│ └────┬─────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ┌────▼─────────────────────────────────────┐ │
│ │ Handler Layer │ │
│ │ device sensor reading command │ │
│ │ auth alert ws │ │
│ └────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────▼─────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ DeviceService SensorService AuthService│ │
│ │ AlertEngine (+ alert_eval, alert_rule) │ │
│ └────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────▼─────────────────────────────────────┐ │
│ │ Repository (Mongo) | Database (Influx)│ │
│ │ devices, users, | write points, │ │
│ │ groups, alerts | Flux query (tsquery)│ │
│ └──────────────────────────────────────────┘ │
│ │
│ Side modules: mqtt (Paho) · ws (hub) · │
│ ratelimit · notify (webhook) · auth (argon2id) │
└──────────────────────────────────────────────────┘
Service Communication Matrix
แต่ละส่วนคุยกันยังไง ใช้ Port อะไร — นี่คือ “แผนที่ท่อสื่อสาร” ของระบบจริง:
| Source | Target | Protocol | Port | Purpose |
|---|---|---|---|---|
| Devices | Mosquitto | MQTT | 1883 | Sensor telemetry |
| Devices | Backend | HTTP | 3000 | REST telemetry (ทางเลือก) |
| Mosquitto | Telegraf | MQTT | 1883 | Config-driven pipeline |
| Mosquitto | Backend | MQTT | 1883 | Telemetry subscribe + command publish |
| Telegraf | InfluxDB | HTTP | 8086 | Write telegraf_sensor_data |
| Backend | MongoDB | TCP | 27017 | Registry/users/alerts CRUD |
| Backend | InfluxDB | HTTP | 8086 | Write sensor_data + Flux query |
| Backend | Webhook | HTTP/HTTPS | — | Alert notification (Slack-compatible) |
| Mobile App | Backend | HTTP / WS | 3000 | API + real-time |
| Admin Panel | Backend | HTTP / WS | 3000 | API + real-time |
สังเกตว่า ไม่มีแถวของ Kapacitor หรือ Chronograf — alerting อยู่ในตัว backend แล้ว และ monitoring dashboard เราทำเองใน admin panel (ดู #20 Monitoring)
Communication Patterns: เลือกอะไรใช้ตอนไหน?
นี่คือส่วนที่หลายคนสับสนมากที่สุด — REST vs MQTT vs WebSocket ต่างกันยังไง?
เปรียบเทียบกับการสื่อสารในชีวิตจริง:
- REST = โทรศัพท์ ถามตอบทันที รอ response ก่อนแล้วค่อยทำต่อ
- MQTT = วิทยุสื่อสาร ส่งออกอากาศ ใครสนใจ subscribe ไว้ก็รับเอง
- WebSocket = LINE Group ที่เปิดค้างไว้ แล้วใครก็ส่งข้อความเข้ามาได้ตลอด
1. Request-Response (REST API)
ใช้สำหรับ CRUD, queries, commands ที่ต้องการ response ทันที ทุก response ใช้ envelope เดียวกัน: สำเร็จ {"data": ...}, error {"error":{"code","message"}}
Client ──── GET /api/v1/devices ────────▶ Go API
Client ◀── 200 {"data":[{...},{...}]} ──── Go API
Endpoints หลัก (ของจริง):
POST/GET/PUT/PATCH/DELETE /api/v1/devices— device registryPOST /api/v1/sensors/dataและ/batch— ingestGET /api/v1/devices/:id/readings,/api/v1/sensors/query— Flux queryPOST /api/v1/devices/:id/commands— สั่ง commandPOST /api/v1/auth/login//refresh/GET /api/v1/auth/me— authPOST/GET/PUT/DELETE /api/v1/alert-rules— alert rule CRUD
2. Publish-Subscribe (MQTT)
ใช้สำหรับ device communication — lightweight, low bandwidth เหมาะกับ sensor ที่ส่งข้อมูลต่อเนื่อง
Device ──── PUBLISH devices/sensor-01/telemetry ───▶ Mosquitto
│
Telegraf ◀── SUBSCRIBE devices/+/telemetry ───────────────┤
Go API ◀── SUBSCRIBE devices/+/telemetry ───────────────┘
Go API ──── PUBLISH devices/sensor-01/command ────▶ Mosquitto ──▶ Device
Topic Structure (ของจริง):
devices/{device_id}/telemetry # Sensor data (device → cloud)
devices/{device_id}/command # Commands (cloud → device)
backend ดึง
device_idจาก topic เสมอ (segment ที่ 2) ไม่เชื่อค่าใน payload — กันการปลอม device id
3. WebSocket (Real-time Push)
ใช้ push real-time data ไป frontend โดย client ไม่ต้อง poll ซ้ำๆ client เปิด WS ไปที่ /api/v1/ws แล้ว subscribe เป็น “room” ต่อ device หรือ group
Client ──── WS connect /api/v1/ws ──────────▶ Go API
Client ──── {"action":"subscribe","room":"device:sensor-01"} ──▶
Client ◀──── {"type":"sensor_data","room":"device:sensor-01","payload":{...}} ──── (ต่อเนื่อง)
ข้อความที่ใช้จริง:
{ "action": "subscribe", "room": "device:sensor-01" }
{ "action": "unsubscribe", "room": "group:<group_id>" }
{ "type": "sensor_data", "room": "device:sensor-01",
"payload": { "device_id": "sensor-01", "fields": {"temperature": 27.7}, "timestamp": "2026-..." } }
room ถูกจำกัด prefix แค่
device:กับgroup:เท่านั้น (กัน client subscribe room ภายในเอง) และ hub ใช้ goroutine เดียวเป็นเจ้าของ state ทั้งหมด client ที่ช้าจน buffer เต็มจะถูกตัดทิ้งเพื่อไม่ block คนอื่น
Tech Stack Justification: ทำไมถึงเลือกอันนี้?
เรารู้ว่าน้องๆ หลายคนอยากถาม “ใช้ Node.js ได้เหมือนกันมั้ย?” หรือ “MongoDB แทน PostgreSQL ได้มั้ย?” — ดังนั้นขอตอบด้วยเหตุผล ไม่ใช่ความรู้สึก
ทำไมเลือก Go + Fiber?
| Criteria | Go + Fiber | Alternatives |
|---|---|---|
| Concurrency | Goroutines เบามาก (stack เริ่มแค่ ~2KB) เหมาะกับ connection พันๆ จาก sensor | thread-based กิน memory มากกว่า |
| MQTT | Eclipse Paho Go client นิ่งและ mature | มีหลายภาษา แต่ Go ecosystem ลงตัว |
| Binary | single binary, ไม่ต้องลาก runtime | Node.js/Python ต้องมี runtime |
| Fiber | API คล้าย Express เขียนเร็ว | Gin/Echo ก็ดี แต่ Fiber คุ้นมือสาย Express |
IoT Backend ต้องรับ connection พร้อมกันนับพัน Goroutines จัดการเรื่องนี้ได้ดีมาก — เราเลย run REST handler, MQTT subscriber, WebSocket hub และ alert engine อยู่ใน process เดียวกันได้สบายๆ
ทำไมเลือก MongoDB (8.0)?
- Flexible Schema: device metadata หลากหลาย ไม่ต้อง migrate ทุกครั้งที่เพิ่ม sensor แบบใหม่
- Document Model: device config / location เป็น nested object พอดีกับ document
- Index ครบ: unique index บน
device_id, compound index สำหรับ filter และ TTL index สำหรับalert_history(ลบ record เก่าทิ้งเอง) - Typed filter: เราเขียน filter เป็น typed (กัน NoSQL injection)
ทำไมเลือก InfluxDB 2.7 + Flux (ไม่ใช่ 1.8 / 3)?
- time-series database ออกแบบมาเพื่อข้อมูลที่มี timestamp โดยเฉพาะ — query 30 วันย้อนหลังเร็วกว่าเก็บใน Mongo มาก
- 2.7 มี Org/Bucket/Token/UI ในตัว provision ผ่าน env ได้คำสั่งเดียว และยังใช้ Flux ได้เต็มที่ — เราใช้ Flux อ่านข้อมูลออก REST แบบ bounded + parameterised
- เราใช้ 2 bucket: raw (
iot_workshop) และ downsampled (iot_workshop_downsampled) แยก retention กัน
ทำไม “เขียน alerting เป็น Go เอง” แทน Kapacitor?
นี่คือจุดที่แพลนแรกกับของจริง ต่างกันมากที่สุด — แพลนเดิมจะใช้ Kapacitor (ส่วน A ของ TICK) แต่พอลงมือทำจริง การเขียน engine เป็น Go เองให้ข้อดีกว่า:
- ประเมิน reading ได้ทันที ตอน ingest ไม่ต้องรอ Kapacitor subscribe กลับจาก InfluxDB
- reuse logic เดียวกันทั้ง REST และ MQTT
- unit-test ได้ละเอียด (evaluator เป็น pure function)
- จัดการ rule ผ่าน REST + MongoDB ได้เหมือน resource อื่นในระบบ
จึงเป็นเหตุผลที่ infra ของเราไม่มี Kapacitor และไม่ได้รัน Chronograf เป็น service (monitoring เราทำเองใน admin panel)
ทำไมเลือก LynxJS (Mobile)?
- เขียน UI ด้วยสไตล์คล้าย React (component-based) แต่ออกไป native
- เบา เหมาะกับการแสดง real-time data
- จาก codebase เดียวออกได้หลาย platform
ทำไมเลือก Vite + React (Admin)?
- Fast DX: Vite HMR เร็วมาก feedback loop สั้น
- TypeScript: type safety สำหรับ admin UI ที่ซับซ้อน จับ bug ก่อน runtime
- Ecosystem: React มี library ครบ (React Router, TanStack Query, React Hook Form + Zod, Zustand)
Deployment Architecture
Development (Docker Compose)
infra service รันใน container แยก แต่คุยกันผ่าน Docker network ส่วน backend เรา make run รันตรงๆ บนเครื่อง (dev loop เร็วกว่า) — เหมือนอพาร์ทเมนต์หลายห้องในตึกเดียว ต่างคนต่างห้อง แต่ใช้ทางเดินร่วมกัน
# infra/docker-compose.yml (4 services จริง)
services:
mongodb: # MongoDB 8.0.16 (port 27017)
mosquitto: # MQTT Broker (port 1883 + 9001 ws)
influxdb: # InfluxDB 2.7.12 (port 8086)
telegraf: # Telegraf 1.39 (MQTT → InfluxDB, ไม่ expose port)
# backend รันแยกด้วย `make run` ที่ http://localhost:3000
Production Considerations
พอ traffic เยอะขึ้น เราสามารถ scale backend ออกเป็นหลาย instance โดยมี Load Balancer คอยแบ่งงาน (backend เป็น stateless, WebSocket hub ต่อ instance — production จริงใช้ shared pub/sub เช่น Redis เสริม)
┌─── Load Balancer (nginx/traefik) ───┐
│ │
┌─────▼──────┐ ┌─────────▼───────┐
│ Backend #1 │ │ Backend #2 │
└──────┬─────┘ └────────┬────────┘
│ │
┌──────▼───────────────────────────────────▼──────┐
│ Shared Infrastructure │
│ MongoDB Replica Set | InfluxDB | Mosquitto │
└──────────────────────────────────────────────────┘
⚠️ ค่า default ทั้งหมด (รหัสผ่าน, token, anonymous MQTT) เป็น dev-only — production ต้องเปลี่ยน credential, ปิด anonymous MQTT, เปิด TLS, ตั้ง
APP_CORS_ALLOW_ORIGINSให้เจาะจง และตั้งAPP_AUTH_JWT_SECRETที่สุ่มยาวพอ
สรุป — เราได้อะไรจากบทความนี้?
ก่อนลงมือ code เราได้วางรากฐานไว้แล้ว:
- System Architecture แบบ Event-Driven — รู้ว่าแต่ละส่วนอยู่ที่ไหน และ telemetry จาก REST/MQTT บรรจบที่ ingestion path เดียวกัน
- Data Flow 3 แบบ: Ingestion (sensor → screen), Command (user → device), Query (Flux history)
- Communication Patterns: REST, MQTT, WebSocket ใช้ตอนไหน
- Tech Stack พร้อมเหตุผล รวมถึง “ทำไมเขียน alert engine เองแทน Kapacitor”
- Deployment infra 4 service + backend แยก สำหรับทั้ง Dev และ Production
เรียนรู้ WHY ก่อน HOW เสมอ แล้วตอน HOW จะง่ายขึ้นมาก (ง่าวง่าว)
Next Step
บทความหน้าเราจะลงลึกเรื่อง Database & Data Model Design — ออกแบบ Schema ของทั้ง MongoDB collections และ InfluxDB 2.7 buckets/measurements ให้ตรงกับ code จริง
Navigation: