# ThingsBoard + ChirpStack 集成生产部署实战

适用:500-1000 台 IoT 设备的中小项目(牧场、园区、农业、冷链等)

架构:单机 Docker Compose,9 个服务,4C8G 起步

关键词:ThingsBoard CE 4.3 / ChirpStack 4.10 / PostgreSQL 16 + TimescaleDB / Redis / Caddy / Prometheus / Grafana


0. 一句话架构

复制代码
牧场主手机/H5 + PC浏览器 + LoRa网关(4G回传)
        ↓ HTTPS / MQTT over TLS
   Caddy 反向代理 (80/443/8883, Let's Encrypt 自动证书)
        ↓
   ┌────────────┬────────────┬────────────┐
   │ ranch-h5   │ tb-node    │ ranch-h5   │
   │ (Express)  │ (TB 4.3.1) │ -frontend  │
   │   :3000    │   :9090    │ (Nginx)    │
   └────┬───────┴──────┬─────┴────────────┘
        │              │ JDBC
        ↓              ↓
   ┌────┴────┐  ┌──────┴──────────┐
   │ redis   │  │ postgres (PG16 │
   │ :6379   │  │ + TimescaleDB) │
   └─────────┘  └─────────────────┘

ChirpStack LoRa 栈(gateway-bridge 1700UDP → chirpstack:8080 → tb-node:1883)
Prometheus + Grafana 监控

1. 服务器与资源规划

1.1 推荐规格

规格 配置 设备规模
最低测试 2核4G + 50G SSD(不可生产 ≤50 台
⭐ 推荐生产 4核8G + 100G SSD 500~1000 台
高配 8核16G + 500G SSD 2000~5000 台
集群 3台 4C8G (K8s) 5000+

⚠️ 别用突发性能实例(阿里云 t5/t6、腾讯云 S 系列共享型),CPU 积分机制会让持续负载限速到 10-20%。

1.2 4C8G 资源分配

组件 内存 说明
tb-node -Xmx3G JVM 堆占物理内存 30-40%,不超 50%
PostgreSQL shared_buffers=2G 占物理内存 25%
Redis maxmemory 256M 只做缓存,LRU 淘汰
ChirpStack 栈 ~200MB 整体很轻
H5 后端 (PM2 2 instance) ~300MB
Caddy + Nginx ~50MB
Prometheus + Grafana ~300MB
系统预留 ~1.5G 别吃满

1.3 磁盘与容量

挂载:/ 50G + /data 100G+(独立数据盘)

容量公式:每台设备每天 ≈ 57KB(5分钟一次 × 200B),500 设备 × 90 天 ≈ 2.5GB 原始遥测,TimescaleDB 压缩后约 300MB。100G 数据盘足够;要保留 1 年原始数据上 200G。

1.4 网络策略

公网只开 3 个端口

端口 用途
80/tcp HTTP(Caddy 自动跳 HTTPS / LE 验证)
443/tcp HTTPS(H5 + TB UI + REST + WS)
8883/tcp MQTT over TLS(LoRa 网关公网回传)

坚决不开:5432 (PG) / 6379 (Redis) / 9090 (TB 直接) / 8080 (ChirpStack) / 9090 (Prometheus) / 3000 (H5 后端) / 3001 (Grafana)


2. Docker Compose 完整部署

2.1 目录结构

复制代码
/opt/ranch/
├── docker-compose.prod.yml
├── .env                     # 敏感变量,chmod 600
├── .env.example
├── caddy/Caddyfile
├── tb/thingsboard.conf
├── postgres/postgresql.conf
├── chirpstack/
│   ├── chirpstack.toml
│   └── gateway-bridge.toml
├── ranch-h5/
│   ├── Dockerfile
│   ├── ecosystem.config.js
│   └── src/
├── ranch-h5-frontend/dist/
├── prometheus/{prometheus.yml,alert_rules.yml}
├── scripts/{backup.sh,restore.sh}
└── data/{postgres,redis,tb,caddy-data,prometheus,grafana,backups}

2.2 .env 关键变量

bash 复制代码
DOMAIN=ranch.example.com
LE_EMAIL=admin@example.com

POSTGRES_USER=postgres
POSTGRES_PASSWORD=$(openssl rand -base64 24)
POSTGRES_DB=thingsboard

CHIRPSTACK_PG_DB=chirpstack
CHIRPSTACK_PG_PASSWORD=$(openssl rand -base64 24)

REDIS_PASSWORD=$(openssl rand -base64 24)

TB_VERSION=4.3.1
TB_JAVA_OPTS=-Xms2G -Xmx3G -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:+ExitOnOutOfMemoryError

H5_JWT_SECRET=$(openssl rand -hex 32)
TB_API_KEY=your_tb_api_key_generated_from_tb_ui

BACKUP_DIR=/data/backups
DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=xxx

2.3 docker-compose.prod.yml(9 服务 + 1 网络)

yaml 复制代码
name: ranch-prod
networks:
  ranch-net:
    external: true   # docker network create ranch-net

services:
  # 1. PostgreSQL 16 + TimescaleDB
  postgres:
    image: timescale/timescaledb:2.17.2-pg16
    restart: always
    networks: [ranch-net]
    ports: ["127.0.0.1:5432:5432"]   # 仅本地
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - /opt/ranch/data/postgres:/var/lib/postgresql/data
      - /opt/ranch/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
    command: postgres -c config_file=/etc/postgresql/postgresql.conf
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
    deploy:
      resources: { limits: { cpus: "2.0", memory: 3500M } }
    logging: { driver: json-file, options: { max-size: "50m", max-file: "3" } }

  # 2. Redis 7
  redis:
    image: redis:7-alpine
    restart: always
    networks: [ranch-net]
    command: >
      redis-server --appendonly yes --appendfsync everysec
      --maxmemory 256mb --maxmemory-policy allkeys-lru
      --requirepass ${REDIS_PASSWORD} --tcp-keepalive 60
    volumes: [/opt/ranch/data/redis:/data]
    deploy: { resources: { limits: { cpus: "0.5", memory: 400M } } }

  # 3. ThingsBoard CE
  tb-node:
    image: thingsboard/tb-node:${TB_VERSION}
    restart: always
    networks: [ranch-net]
    depends_on:
      postgres: { condition: service_healthy }
      redis:    { condition: service_healthy }
    ports:
      - "127.0.0.1:9090:9090"     # 仅本地,由 Caddy 反代
      - "0.0.0.0:1883:1883"      # MQTT(内网 ChirpStack)
      - "0.0.0.0:8883:8883"      # MQTT over TLS
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
      SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
      SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
      CACHE_TYPE: redis
      REDIS_HOST: redis
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      JAVA_OPTS: ${TB_JAVA_OPTS}
      DATABASE_TS_TYPE: sql
      DATABASE_TS_LATEST_TYPE: sql
    volumes:
      - /opt/ranch/data/tb:/data
      - /opt/ranch/tb/thingsboard.conf:/config/thingsboard.conf:ro
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:9090/actuator/health || exit 1"]
      interval: 30s
      start_period: 120s
    deploy: { resources: { limits: { cpus: "3.0", memory: 4000M } } }

  # 4. ChirpStack
  chirpstack-gateway-bridge:
    image: chirpstack/chirpstack-gateway-bridge:4.1.3
    restart: always
    networks: [ranch-net]
    ports: ["0.0.0.0:1700:1700/udp"]
    volumes:
      - /opt/ranch/chirpstack/gateway-bridge.toml:/etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml:ro

  chirpstack:
    image: chirpstack/chirpstack:${CHIRPSTACK_VERSION:-4.10.0}
    restart: always
    networks: [ranch-net]
    depends_on: [postgres]
    ports: ["127.0.0.1:8080:8080"]
    environment:
      POSTGRESQL_DSN: "host=postgres port=5432 dbname=${CHIRPSTACK_PG_DB} user=${CHIRPSTACK_PG_USER} password=${CHIRPSTACK_PG_PASSWORD} sslmode=disable"
      REDIS_SERVER: "redis://:${REDIS_PASSWORD}@redis:6379"
      INTEGRATION_MQTT_ENABLED: "true"
      INTEGRATION_MQTT_SERVER: "tcp://tb-node:1883"
    volumes: [/opt/ranch/chirpstack/chirpstack.toml:/etc/chirpstack/chirpstack.toml:ro]

  # 5. H5 后端
  ranch-h5-backend:
    build: { context: /opt/ranch/ranch-h5, dockerfile: Dockerfile }
    restart: always
    networks: [ranch-net]
    depends_on:
      tb-node: { condition: service_healthy }
      postgres: { condition: service_healthy }
    ports: ["127.0.0.1:3000:3000"]
    environment:
      NODE_ENV: production
      PORT: ${H5_API_PORT}
      TB_URL: http://tb-node:9090
      TB_API_KEY: ${TB_API_KEY}
      JWT_SECRET: ${H5_JWT_SECRET}
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
    volumes: [/opt/ranch/ranch-h5/logs:/app/logs]
    healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"], interval: 15s }
    deploy: { resources: { limits: { cpus: "0.7", memory: 500M } } }

  # 6. H5 前端
  ranch-h5-frontend:
    image: nginx:1.27-alpine
    restart: always
    networks: [ranch-net]
    volumes:
      - /opt/ranch/ranch-h5-frontend/dist:/usr/share/nginx/html:ro
      - /opt/ranch/ranch-h5-frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro

  # 7. Caddy 反代
  caddy:
    image: caddy:2-alpine
    restart: always
    networks: [ranch-net]
    ports: ["80:80", "443:443", "8883:8883"]
    volumes:
      - /opt/ranch/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - /opt/ranch/data/caddy-data:/data
      - /opt/ranch/data/caddy-config:/config
    environment: { DOMAIN: ${DOMAIN}, EMAIL: ${LE_EMAIL} }

  # 8. Prometheus
  prometheus:
    image: prom/prometheus:v2.54.1
    restart: always
    networks: [ranch-net]
    ports: ["127.0.0.1:9090:9090"]
    volumes:
      - /opt/ranch/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - /opt/ranch/prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml:ro
      - /opt/ranch/data/prometheus:/prometheus
    command: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.path=/prometheus", "--storage.tsdb.retention.time=30d", "--web.enable-lifecycle"]
    deploy: { resources: { limits: { cpus: "0.5", memory: 400M } } }

  # 9. Grafana
  grafana:
    image: grafana/grafana:11.2.0
    restart: always
    networks: [ranch-net]
    depends_on: [prometheus]
    ports: ["127.0.0.1:3001:3000"]
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: your_strong_grafana_password
      GF_USERS_ALLOW_SIGN_UP: "false"
    volumes:
      - /opt/ranch/data/grafana:/var/lib/grafana
      - /opt/ranch/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro

  # 可选:exporter
  node-exporter:
    image: prom/node-exporter:v1.8.2
    restart: always
    networks: [ranch-net]
    pid: host
    volumes: ["/proc:/host/proc:ro", "/sys:/host/sys:ro", "/:/rootfs:ro"]
    command: ["--path.procfs=/host/proc", "--path.sysfs=/host/sys", "--path.rootfs=/rootfs",
              "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"]

  postgres-exporter:
    image: prometheuscommunity/postgres-exporter:v0.15.0
    restart: always
    networks: [ranch-net]
    environment:
      DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"

2.4 启动流程

bash 复制代码
# 1. 首次创建网络
docker network create ranch-net

# 2. 创建数据目录
sudo mkdir -p /opt/ranch/data/{postgres,redis,tb,caddy-data,caddy-config,prometheus,grafana,backups}
sudo chown -R $USER:$USER /opt/ranch

# 3. 首次跑 TB schema 安装(--loadDemo 会带演示数据,正式上线时去掉)
docker compose -f /opt/ranch/docker-compose.prod.yml run --rm -it tb-node install-tb.sh --loadDemo

# 4. 启动
docker compose -f /opt/ranch/docker-compose.prod.yml up -d

# 5. 看 TB 启动(首次 3-5 分钟做 schema 迁移)
docker compose -f /opt/ranch/docker-compose.prod.yml logs -f tb-node
# 看到 "Started ThingsBoardApplication" 才算起完

🚫 生产禁止 docker compose down -v :会删除所有数据卷(PG/Redis/TB 全没)。日常用 stop / restart / up -d


3. ThingsBoard 性能调优

3.1 JVM 参数(写到 .env 的 TB_JAVA_OPTS)

服务器 JVM 参数
⭐ 4C8G -Xms2G -Xmx3G -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/heapdump.hprof -Djava.security.egd=file:/dev/./urandom
8C16G -Xms6G -Xmx6G -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35

-Xms = -Xmx 避免运行时扩缩;ExitOnOutOfMemoryError 让 OOM 时 Docker 自动重启;HeapDumpOnOutOfMemoryError 留证据。

3.2 thingsboard.conf 关键配置

ini 复制代码
# 缓存(设备/凭证 10 分钟过期,最新遥测 1 小时)
cache.specs.devices.maxSize: 10000
cache.specs.devices.timeSinceLastAccess: 600
cache.specs.latestTelemetry.maxSize: 100000
cache.specs.latestTelemetry.timeSinceLastAccess: 3600

# HikariCP 连接池
spring.datasource.hikari.maximum-pool-size: 20
spring.datasource.hikari.minimum-idle: 5

# 规则引擎 / 告警
actors.rule-engine.thread-pool-size: 4
alarms.batch.size: 100
alarms.batch.max-delay: 1000

# 异步写遥测(关键性能点)
sql.attributes.batch_size: 1000
sql.ts.batch_size: 1000
sql.ts.batch_threads: 2

# Calculated Fields(v4.3)
calculated_fields.thread-pool-size: 2
calculated_fields.eval-pool-size: 4

# 日志级别生产用 WARN,排查临时改 DEBUG
logger.level.root: WARN
logger.level.org.thingsboard: WARN

⚠️ 规则链 Debug 模式生产必关!Debug 模式每条消息都写 event,流量大能写爆盘。临时开 10-15 分钟排查完立刻关。

3.3 TimescaleDB 启用与策略

bash 复制代码
# 启用扩展(一次)
docker exec -it postgres psql -U postgres -d thingsboard -c \
  "CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"

# 数据保留:90 天后自动删原始遥测
docker exec -it postgres psql -U postgres -d thingsboard <<'SQL'
SELECT add_retention_policy('ts_kv', INTERVAL '90 days');

-- 压缩:7 天后自动压缩(5-10 倍节省)
ALTER TABLE ts_kv SET (
  timescaledb.compress,
  timescaledb.compress_segmentby = 'entity_id, key',
  timescaledb.compress_orderby = 'ts DESC'
);
SELECT add_compression_policy('ts_kv', INTERVAL '7 days');

-- 连续聚合:每小时平均电量(仪表板趋势图直接读这个,不用扫原始数据)
CREATE MATERIALIZED VIEW ts_kv_hourly_battery
WITH (timescaledb.continuous) AS
SELECT entity_id, key,
       time_bucket(INTERVAL '1 hour', ts) AS hour_bucket,
       avg(long_v) AS avg_battery,
       min(long_v) AS min_battery,
       max(long_v) AS max_battery
FROM ts_kv WHERE key = 'batteryLevel'
GROUP BY entity_id, key, time_bucket(INTERVAL '1 hour', ts);

SELECT add_continuous_aggregate_policy('ts_kv_hourly_battery',
  start_offset => INTERVAL '3 hours',
  end_offset => INTERVAL '1 hour',
  schedule_interval => INTERVAL '1 hour');

SELECT add_retention_policy('ts_kv_hourly_battery', INTERVAL '1 year');
SQL

ts_kvbool_v / str_v / long_v / dbl_v / json_v 分字段存值,聚合数值用 long_vdbl_v。GPS 建议在设备端或 ChirpStack 解码器里拆成独立 double key。

3.4 PostgreSQL 参数(postgresql.conf)

ini 复制代码
max_connections = 100
shared_buffers = 2GB              # 物理内存 25%
effective_cache_size = 5GB        # 物理内存 50-75%
work_mem = 16MB                   # (RAM*0.25)/max_connections/2
maintenance_work_mem = 512MB
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min
random_page_cost = 1.1            # SSD
effective_io_concurrency = 200    # SSD 并发
jit = off                         # IoT 简单查询,JIT 反而开销大
log_min_duration_statement = 1000 # 超 1s 记录
autovacuum = on
autovacuum_max_workers = 3

8C16G:max_connections=200, shared_buffers=4GB, effective_cache_size=12GB, work_mem=32MB, maintenance_work_mem=1GB, max_wal_size=8GB

3.5 Redis 关键配置

ini 复制代码
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes               # AOF 持久化
appendfsync everysec

Redis 存的是可重建缓存(原始数据在 PG),256MB 足够 1000+ 设备。挂了大不了重建缓存,不用给太大内存。


4. HTTPS 与反向代理

Caddy vs Nginx 选哪个 ?Caddy 自动 HTTPS 配置 3 行搞定证书+反代+WS;Nginx 静态/缓存/gzip 更成熟。推荐 Caddy 占 80/443 做 TLS 终结,Nginx 做前端静态文件托管,两者不冲突。

4.1 Caddyfile(子域名方案,新手推荐)

caddy 复制代码
# 主站
ranch.example.com {
    tls {env.EMAIL}
    encode zstd gzip
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    # WebSocket(先匹配)
    @ws_path path /ws/*
    handle @ws_path {
        reverse_proxy http://ranch-h5-backend:3000 {
            header_up Host {host}
            header_up X-Real-IP {remote_host}
            header_up X-Forwarded-For {remote_host}
            header_up X-Forwarded-Proto {scheme}
            header_up Connection "upgrade"
            header_up Upgrade "websocket"
            transport http { read_timeout 300s write_timeout 300s }
        }
    }

    # H5 API
    @api_path path /api/*
    handle @api_path {
        reverse_proxy http://ranch-h5-backend:3000
    }

    # 其它走前端
    handle /* {
        reverse_proxy http://ranch-h5-frontend:80
    }
}

# TB 管理后台(独立子域名,避免 context_path 坑)
tb.ranch.example.com {
    tls {env.EMAIL}
    encode zstd gzip
    reverse_proxy http://tb-node:9090 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
        header_up Connection "upgrade"
        header_up Upgrade "websocket"
    }
}

DNS 两条 A 记录:ranch.example.com + tb.ranch.example.com → 同一公网 IP。

4.2 防火墙

bash 复制代码
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8883/tcp
sudo ufw allow 1700/udp
sudo ufw enable

# 同时检查云厂商安全组!
# 阿里云/腾讯云/AWS 的安全组是独立的,开了 ufw 安全组没放行还是进不来

5. H5 后端生产化

5.1 PM2 配置

js 复制代码
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'ranch-h5-api',
    script: './src/app.js',
    instances: 2,
    exec_mode: 'cluster',
    autorestart: true,
    watch: false,
    max_memory_restart: '250M',
    restart_delay: 3000,
    max_restarts: 10,
    min_uptime: '10s',
    env: { NODE_ENV: 'production', PORT: 3000 },
    error_file: './logs/err.log',
    out_file:   './logs/out.log',
    merge_logs: true,
  }]
};

5.2 Dockerfile(多阶段 + 非 root + tini)

dockerfile 复制代码
# 阶段 1:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY . .

# 阶段 2:运行
FROM node:20-alpine AS runtime
WORKDIR /app
RUN addgroup -S ranch && adduser -S ranch -G ranch
RUN mkdir -p /app/logs && chown -R ranch:ranch /app
RUN npm install -g pm2 && apk add --no-cache tini
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/ .
RUN chown -R ranch:ranch /app
USER ranch
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["pm2-runtime", "ecosystem.config.js"]

pm2-runtime 专门为容器设计:前台运行 + 正确转发信号(SIGTERM 优雅退出)。普通 pm2 start 会 fork 后台,容器一启动就退出。

5.3 Nginx 前端配置

nginx 复制代码
server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    gzip on;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml image/svg+xml;
    gzip_comp_level 6;

    # 静态资源长缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # index.html 不缓存
    location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; }

    # SPA 路由回退
    location / { try_files $uri $uri/ /index.html; }

    # 禁止访问隐藏文件
    location ~ /\. { deny all; }
}

5.4 健康检查 + 限流

js 复制代码
// /api/health
router.get('/api/health', async (req, res) => {
  const health = { status: 'ok', timestamp: new Date().toISOString(), services: {} };
  try { await tb.checkConnectivity(); health.services.thingsboard = 'connected'; }
  catch { health.services.thingsboard = 'disconnected'; health.status = 'degraded'; }
  try { await db.query('SELECT 1'); health.services.database = 'connected'; }
  catch { health.services.database = 'disconnected'; health.status = 'degraded'; }
  res.status(health.status === 'ok' ? 200 : 503).json(health);
});

// 限流
const globalLimiter = rateLimit({ windowMs: 60*1000, max: 100 });
const loginLimiter  = rateLimit({ windowMs: 60*1000, max: 5, skipSuccessfulRequests: true });

// CORS 严格限定
const corsOptions = {
  origin: process.env.NODE_ENV === 'production'
    ? ['https://ranch.example.com']
    : ['http://localhost:5173'],
  credentials: true
};

app.use(helmet());
app.use(cors(corsOptions));
app.use(globalLimiter);
app.use('/api/auth/login', loginLimiter);

6. 备份与恢复

💀 没有验证过恢复的备份 = 没有备份。每月做一次恢复演练。

6.1 backup.sh

bash 复制代码
#!/bin/bash
# 用法:/opt/ranch/scripts/backup.sh
# Cron: 0 3 * * * /opt/ranch/scripts/backup.sh >> /var/log/ranch-backup.log 2>&1
set -euo pipefail
BACKUP_ROOT="/opt/ranch/data/backups"
DATE=$(date +%Y%m%d_%H%M%S)
KEEP_LOCAL_DAYS=7
COMPOSE_FILE="/opt/ranch/docker-compose.prod.yml"
ENV_FILE="/opt/ranch/.env"
DINGTALK_WEBHOOK="${DINGTALK_WEBHOOK:-}"
S3_REMOTE="ranch-s3:"
source "${ENV_FILE}" 2>/dev/null || true
BACKUP_DIR="${BACKUP_ROOT}/${DATE}"
mkdir -p "${BACKUP_DIR}"
exec > >(tee -a "${BACKUP_DIR}/backup.log") 2>&1

notify() {
  [ -n "${DINGTALK_WEBHOOK}" ] && curl -s -X POST "${DINGTALK_WEBHOOK}" \
    -H 'Content-Type: application/json' \
    -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[Ranch Backup] $1: $2\"}}" >/dev/null 2>&1 || true
}
trap 'notify FAIL "Backup FAILED at '"${DATE}"'"' ERR

# 1. 备份 ThingsBoard DB
docker exec postgres pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -Fc --blobs --no-owner \
  > "${BACKUP_DIR}/thingsboard.dump"

# 2. 备份 ChirpStack DB
docker exec postgres pg_dump -U "${POSTGRES_USER}" -d "${CHIRPSTACK_PG_DB}" -Fc --blobs --no-owner \
  > "${BACKUP_DIR}/chirpstack.dump"

# 3. TB 实体导出(仪表板 / 规则链 / 设备)
mkdir -p "${BACKUP_DIR}/tb-entities"
TB_JWT=$(curl -s -X POST "http://127.0.0.1:9090/api/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"tenant@thingsboard.org","password":"your_tenant_password"}' \
  | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -n "${TB_JWT}" ]; then
  curl -s "http://127.0.0.1:9090/api/tenant/dashboards?pageSize=1000"  -H "X-Authorization: Bearer ${TB_JWT}" > "${BACKUP_DIR}/tb-entities/dashboards.json"
  curl -s "http://127.0.0.1:9090/api/rule-chains?pageSize=100"           -H "X-Authorization: Bearer ${TB_JWT}" > "${BACKUP_DIR}/tb-entities/rulechains.json"
  curl -s "http://127.0.0.1:9090/api/tenant/devices?pageSize=1000"      -H "X-Authorization: Bearer ${TB_JWT}" > "${BACKUP_DIR}/tb-entities/devices.json"
fi

# 4. 备份配置
cp "${COMPOSE_FILE}" "${BACKUP_DIR}/"
cp "${ENV_FILE}"     "${BACKUP_DIR}/"
cp -r /opt/ranch/{caddy,tb,postgres,prometheus} "${BACKUP_DIR}/" 2>/dev/null || true

# 5. 打包
cd "${BACKUP_ROOT}"
tar czf "${DATE}.tar.gz" "${DATE}"
rm -rf "${BACKUP_DIR}"
SHA=$(sha256sum "${DATE}.tar.gz" | awk '{print $1}')
echo "${SHA}  ${DATE}.tar.gz" > "${DATE}.tar.gz.sha256"

# 6. 清理本地旧备份
find "${BACKUP_ROOT}" -name "*.tar.gz*" -mtime +${KEEP_LOCAL_DAYS} -delete

# 7. 异地备份(rclone 配置 S3/COS/OSS)
if command -v rclone &>/dev/null && rclone listremotes | grep -q "^${S3_REMOTE}"; then
  rclone copy "${BACKUP_ROOT}/${DATE}.tar.gz" "${S3_REMOTE}/ranch-backups/"
  rclone copy "${BACKUP_ROOT}/${DATE}.tar.gz.sha256" "${S3_REMOTE}/ranch-backups/"
  rclone delete "${S3_REMOTE}/ranch-backups/" --min-age "30d" || true
fi

notify SUCCESS "Backup ${DATE}.tar.gz done, sha256=${SHA:0:16}..."

rclone 配置示例(腾讯云 COS):

bash 复制代码
rclone config
# name> ranch-s3
# Storage> 5 (S3)
# provider> 28 (Tencent COS)
# 填 access_key / secret_key / endpoint
rclone lsd ranch-s3:   # 测试

6.2 restore.sh

bash 复制代码
#!/bin/bash
# 用法:./restore.sh /path/to/20250101_030000.tar.gz [--yes]
# 流程:确认 → 备份当前 → 停服务 → pg_restore → 恢复配置 → 启动 → 健康检查
set -euo pipefail
[ $# -lt 1 ] && { echo "Usage: $0 <backup.tar.gz> [--yes]"; exit 1; }

BACKUP_ARCHIVE="$1"
ASSUME_YES="${2:-}"
COMPOSE_FILE="/opt/ranch/docker-compose.prod.yml"
ENV_FILE="/opt/ranch/.env"
RESTORE_DIR="/tmp/ranch-restore-$$"
source "${ENV_FILE}" 2>/dev/null || true
[ ! -f "${BACKUP_ARCHIVE}" ] && { echo "[ERROR] not found: ${BACKUP_ARCHIVE}"; exit 1; }

# 校验 SHA
[ -f "${BACKUP_ARCHIVE}.sha256" ] && (cd "$(dirname ${BACKUP_ARCHIVE})" && sha256sum -c "$(basename ${BACKUP_ARCHIVE}.sha256)")

# 二次确认
if [ "${ASSUME_YES}" != "--yes" ]; then
  echo "⚠️  This will OVERWRITE existing data with ${BACKUP_ARCHIVE}"
  read -p "Type 'YES' to continue: " C; [ "$C" = "YES" ] || { echo Aborted.; exit 1; }
fi

# 0. 先备份当前状态(万一恢复错能回滚)
SAFETY="/opt/ranch/data/backups/pre-restore-$(date +%Y%m%d_%H%M%S)"
mkdir -p "${SAFETY}"
docker exec postgres pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -Fc --no-owner > "${SAFETY}/thingsboard.dump" || true
docker exec postgres pg_dump -U "${POSTGRES_USER}" -d "${CHIRPSTACK_PG_DB}" -Fc --no-owner > "${SAFETY}/chirpstack.dump" || true

# 1. 解压
mkdir -p "${RESTORE_DIR}"
tar xzf "${BACKUP_ARCHIVE}" -C "${RESTORE_DIR}"
DUMP_DIR=$(ls -d "${RESTORE_DIR}"/*/ | head -1)

# 2. 停应用服务(保 PG/Redis)
cd /opt/ranch
docker compose -f "${COMPOSE_FILE}" stop tb-node chirpstack chirpstack-gateway-bridge \
  ranch-h5-backend ranch-h5-frontend caddy prometheus grafana 2>/dev/null || true
sleep 5

# 3. 终止连接
docker exec postgres psql -U "${POSTGRES_USER}" -d postgres -c "
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = '${POSTGRES_DB}' AND pid <> pg_backend_pid();" || true

# 4. 恢复 ThingsBoard DB
docker exec postgres psql -U "${POSTGRES_USER}" -d postgres -c "DROP DATABASE IF EXISTS ${POSTGRES_DB};"
docker exec postgres psql -U "${POSTGRES_USER}" -d postgres -c "CREATE DATABASE ${POSTGRES_DB} OWNER ${POSTGRES_USER};"
docker exec postgres psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;" 2>/dev/null || true
cat "${DUMP_DIR}/thingsboard.dump" | docker exec -i postgres pg_restore -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" --no-owner

# 5. 恢复 ChirpStack DB(同流程)

# 6. 启动 + 健康检查
docker compose -f "${COMPOSE_FILE}" up -d
for i in {1..30}; do
  curl -fsS "http://127.0.0.1:9090/actuator/health" 2>/dev/null | grep -q '"status":"UP"' && { echo "✅ tb-node UP"; break; }
  sleep 10
done

rm -rf "${RESTORE_DIR}"
echo "✅ Restore done. Safety: ${SAFETY}"

6.3 恢复演练流程

  1. 按量付费开一台同配置测试机
  2. 装 Docker + Compose,初始化空环境
  3. rclone 下载最新备份
  4. restore.sh backup.tar.gz --yes
  5. 登录 TB 对比设备数、最近遥测、规则链、仪表板
  6. 记录演练日志

6.4 crontab

cron 复制代码
# 每日 3 点备份
0 3 * * * /opt/ranch/scripts/backup.sh >> /var/log/ranch-backup.log 2>&1
# 每周日 4 点 VACUUM FULL(可选)
# 0 4 * * 0 docker exec postgres vacuumdb -U postgres --all --analyze

7. 监控与告警

7.1 Prometheus 抓取

prometheus.yml

yaml 复制代码
global: { scrape_interval: 15s, evaluation_interval: 15s }
rule_files: ["alert_rules.yml"]

scrape_configs:
  - job_name: 'thingsboard'
    metrics_path: '/actuator/prometheus'
    static_configs: [{ targets: ['tb-node:9090'] }]
  - job_name: 'node'
    static_configs: [{ targets: ['node-exporter:9100'] }]
  - job_name: 'postgres'
    static_configs: [{ targets: ['postgres-exporter:9187'] }]
  - job_name: 'redis'
    static_configs: [{ targets: ['redis-exporter:9121'] }]  # 需另部署
  - job_name: 'prometheus'
    static_configs: [{ targets: ['localhost:9090'] }]
  - job_name: 'caddy'
    static_configs: [{ targets: ['caddy:2019'] }]

7.2 关键告警规则

alert_rules.yml

yaml 复制代码
groups:
  - name: ranch-infra
    rules:
      - alert: InstanceDown
        expr: up == 0
        for: 1m
        labels: { severity: critical }
        annotations: { summary: "服务 {{ $labels.job }} 挂了!" }

      - alert: DiskAlmostFull
        expr: (node_filesystem_size_bytes{fstype!~"tmpfs|fuse.lxcfs|squashfs"} - node_filesystem_avail_bytes) / node_filesystem_size_bytes > 0.85
        for: 5m
        labels: { severity: warning }

      - alert: DiskCritical
        expr: (node_filesystem_size_bytes{fstype!~"tmpfs|fuse.lxcfs|squashfs"} - node_filesystem_avail_bytes) / node_filesystem_size_bytes > 0.92
        for: 2m
        labels: { severity: critical }
        annotations: { summary: "磁盘超 92%,即将写满!" }

      - alert: HighCPU
        expr: 100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 10m

      - alert: HighMemory
        expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.90
        for: 5m

      - alert: TBJvmHeapHigh
        expr: jvm_memory_used_bytes{application="thingsboard",area="heap"} / jvm_memory_max_bytes{application="thingsboard",area="heap"} > 0.85
        for: 5m

      - alert: TBRuleEngineErrors
        expr: rate(rule_engine_error_total[5m]) > 0.5
        for: 3m

      - alert: TBMessagesDropped
        expr: rate(queue_dropped_total[5m]) > 10
        for: 2m

      - alert: PGConnectionHigh
        expr: pg_stat_activity_count > 80
        for: 5m

      - alert: High5xxRate
        expr: rate(caddy_http_response_status_count_total{code=~"5.."}[5m]) / rate(caddy_http_response_total[5m]) > 0.05
        for: 3m

7.3 告警通知

最简方案:Grafana 配 DingDing Notification Channel(UI 点几下即可)。

  • 钉钉群 → 智能群助手 → 添加机器人 → 选"加签"或"自定义关键词"
  • Grafana → Alerting → Contact points → Type: DingDing → URL: 钉钉 webhook

7.4 Grafana 面板布局

区域 关键面板
JVM (tb-node) 堆内存、GC 次数/耗时、线程数、启动时间
PostgreSQL 当前连接数、TPS、缓存命中率、表大小、慢查询数、WAL 速率
Redis 内存、命中率、连接数、evicted keys、AOF 重写
TB 业务 设备在线数、MQTT 连接数、规则引擎消息速率、告警 Active 数、CF 触发次数

7.5 日志速查

bash 复制代码
docker logs -f tb-node --tail=100              # TB 日志
docker logs tb-node 2>&1 | grep -iE 'error|exception' | tail -50  # 错误
docker logs --since 1h tb-node                 # 最近 1 小时

8. 安全加固清单

类别
API Key v4.3 生成时只显示一次,立刻存 .env;最小权限;不写前端;不提交 Git;90 天轮换
默认密码 sysadmin@thingsboard.org / tenant / postgres / redis / chirpstack / grafana 全部改
JWT 密钥 TB security.jwt.token.key 改成自己的 64 字节随机;H5 JWT_SECRET 32 字节
Docker 容器非 root(USER ranch);不映射 /var/run/docker.sock;只读 rootfs;每月 docker compose pull 更新
SSH PermitRootLogin no + PasswordAuthentication no + 改端口 + fail2ban
数据库 PG/Redis/ChirpStack 数据库不暴露公网;强密码;ChirpStack 用独立用户
TB 用户权限 牧场主 = Customer User(只看自家);H5 后端 = 专用 API Key;不给客户 Tenant Admin
审计日志 TB Audit Log 默认开启,定期导出
MQTT TLS 公网必须开 TLS(Access Token 明文传输,路由器/运营商能抓到)
定期升级 订阅 TB Security Advisories;补丁版本小、安全且自动迁移;升级前必备份
ini 复制代码
# SSH /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
Port 2222
bash 复制代码
sudo apt install -y fail2ban
sudo systemctl enable fail2ban --now
ini 复制代码
# TB JWT 配置
security.jwt.token.key: "your_64_byte_hex_key_generated_by_openssl_rand_hex_32"
security.jwt.token.expirationTime: 9000000    # 2.5 小时
security.jwt.token.refreshExpTime: 604800000  # 7 天

9. 上线 Checklist(40 项)

服务器基础(8 项)

  • SSH 禁用 root + 禁用密码 + 改端口 + fail2ban
  • 防火墙/安全组只开 22/80/443/8883/1700udp
  • 时区 Asia/Shanghai
  • swap 配置(4G)
  • ulimits(nofile 65535)
  • 主机名 ranch-prod-01
  • 内核参数(vm.swappiness=10, tcp_tw_reuse=1)

Docker 环境(4 项)

  • Docker ≥ 26.x + Compose v2
  • 数据卷挂到独立数据盘
  • 自定义 bridge 网络 ranch-net
  • 日志 json-file + max-size 轮转

数据库(6 项)

  • PG16 + TimescaleDB 扩展已启用(\dx 验证)
  • postgresql.conf 按模板调优
  • ts_kv 转 hypertable
  • 保留 90 天 + 压缩 7 天 + 连续聚合策略
  • ChirpStack 独立库和用户
  • 恢复演练至少成功过一次

ThingsBoard(8 项)

  • TB ≥ 4.3.1,启动无 ERROR/Schema 失败
  • 默认密码全改
  • JWT 密钥已替换
  • TB_QUEUE_TYPE=in-memory(单机够用)
  • Caffeine/HikariCP 已调优
  • 日志级别 WARN,所有规则链 Debug 模式已关闭
  • H5 后端专用 API Key + 最小权限
  • Tenant 配置已调(默认告警 TTL、队列大小等)

ChirpStack(4 项)

  • ChirpStack v4.10 启动正常,admin 密码已改
  • Gateway Bridge 配置完成,网关 ACTIVE
  • Service Profile / Device Profile 已建
  • MQTT Integration 已配,payload 正确转发到 TB

H5 前后端(6 项)

  • .env 填好(JWT_SECRET、TB_API_KEY、数据库密码),无硬编码
  • PM2 cluster 模式 + max_memory_restart
  • Docker 化 + 非 root
  • 前端 Nginx SPA 路由 + 静态缓存
  • Caddy SSL 证书已签
  • API 限流 + CORS 限定 + Helmet 已开

监控备份(4 项)

  • Prometheus + Grafana 启动,所有 target UP
  • Grafana 4 面板已导入
  • 备份 cron 已加,钉钉通知已测通
  • 告警规则已配,已模拟触发验证

业务验证(6 项)

  • 真实项圈上线,GPS/电量 5 分钟上报正常
  • Geofencing CF 工作(牛进出栏状态变化正确)
  • 越界告警触发 + 蜂鸣/电击 Downlink 正常
  • 4G 手机打开 H5,地图/列表/告警/详情正常
  • 越界告警通知正常
  • 频控生效

10. 压力测试

目标:500 台 × 5 分钟/次 = 约 1.7 msg/s。TB 官方 4C8G 单机能力是 1万-5万 msg/s,余量充足。

bash 复制代码
# 安装 mqtt-bench
wget https://github.com/krylovsk/mqtt-benchmark/releases/download/v1.0.1/mqtt-bench-linux-amd64
chmod +x mqtt-bench && sudo mv mqtt-bench /usr/local/bin/

# 压测(压测时把间隔缩短到 5s = 60倍速)
mqtt-bench \
  --broker=tcp://你的IP:1883 \
  --clients=500 --count=60 --interval=5 \
  --topic=v1/devices/me/telemetry \
  --username=TEST_DEVICE_TOKEN \
  --qos=1 --format=json \
  --message='{"latitude":43.82,"longitude":87.61,"batteryLevel":85,"rssi":-70}'

压测期间观察指标

指标 健康 告警 危险
CPU <50% 70% >85%
内存 <60% 80% >90%
TB JVM 堆 <70% 85% >95%
PG 连接数 <30 60 >80
规则引擎 p99 延迟 <500ms 1s >5s
磁盘 <60% 85% >92%

水平扩展(单机扛不住时)

  • TB 集群:多 tb-node + Kafka 队列 + K8s 编排
  • PG 读写分离:主库写 + 1-2 只读副本
  • ChirpStack 独立机器:>3000 设备时拆分

5000 台以下不建议折腾集群,直接升配到 8C16G 或 16C32G 更便宜简单。


11. 故障排查手册

排查铁律:先看日志docker logs tb-node --tail=200 90% 的问题能找到答案。

Q:设备不上报遥测

  1. ChirpStack UI → Gateways → 网关是否 last seen 几秒前
  2. docker logs chirpstack-gateway-bridge --tail=50 看 UDP 包
  3. ChirpStack UI → Device → LoRaWAN frames(DevEUI/AppKey 配错、解码器报错、Region 频段)
  4. 手动订阅:docker exec -it chirpstack mosquitto_sub -h tb-node -p 1883 -u TOKEN -t v1/devices/me/telemetry -v
  5. 临时开规则链 Debug 模式查卡点

Q:告警不触发

  1. 看 CF 状态:Device → Calculated Fields,safeZoneStatus 是否更新
  2. 检查 Alarm Rules(key 名/阈值/severity),看是否被 Clear 条件误清
  3. Alarms 页面查历史(是否创建了但被自动 Clear)

Q:H5 打不开

  1. docker logs caddy --tail=50(证书是否签成功)
  2. docker logs ranch-h5-backend --tail=50(Express 报错)
  3. curl https://你的域名/api/health
  4. curl https://你的域名/(Nginx 是否返 index.html)

Q:数据库膨胀 / TB 变卡

bash 复制代码
# 最大的表
docker exec -it postgres psql -U postgres thingsboard -c "
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;"

# ts_kv chunk 状态
docker exec -it postgres psql -U postgres thingsboard -c "
SELECT hypertable_name, COUNT(*) AS chunks,
       pg_size_pretty(SUM(chunk_total_size_bytes)) AS total_size
FROM timescaledb_information.chunks GROUP BY hypertable_name;"

Q:OOM / TB 频繁重启

  1. dmesg | grep -i oom 看内核 OOM Killer
  2. JVM 堆是否太大挤压 PG/Redis
  3. Redis maxmemory 是否被突破
  4. Docker mem_limit 是否太低
  5. 是否有大消息 payload(固件升级/图片传输)

Q:证书过期

  1. Caddy 自动续期失败:日志看 ACME 错误
  2. echo | openssl s_client -connect domain:443 -servername domain 2>/dev/null | openssl x509 -noout -dates
  3. Nginx 方案:certbot renew --dry-run

Q:TB 升级后起不来

  1. 别慌别瞎操作 ,看日志:docker logs tb-node --tail=200
  2. Schema migration 失败 → 用升级前备份 restore
  3. 紧急回滚:改 .envTB_VERSION 旧 tag,docker compose up -d tb-node

12. 运维命令速查

bash 复制代码
# Docker Compose
COMPOSE=/opt/ranch/docker-compose.prod.yml
docker compose -f $COMPOSE up -d                      # 启动
docker compose -f $COMPOSE stop                      # 停止
docker compose -f $COMPOSE restart tb-node           # 重启某服务
docker compose -f $COMPOSE up -d --build ranch-h5-backend  # 重建
docker compose -f $COMPOSE pull && docker compose -f $COMPOSE up -d  # 升级
docker compose -f $COMPOSE ps                        # 状态
docker compose -f $COMPOSE logs -f tb-node           # 实时日志
docker exec -it tb-node bash
docker exec -it postgres psql -U postgres thingsboard

# TB
docker logs -f tb-node --tail=100
docker logs tb-node 2>&1 | grep -iE 'error|exception' | tail -50
curl -s http://127.0.0.1:9090/actuator/health | jq .

# PG
SELECT count(*) FROM pg_stat_activity;
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE state = 'idle in transaction' AND xact_start < NOW() - INTERVAL '1 hour';

# Redis
docker exec -it redis redis-cli -a $REDIS_PASSWORD
INFO memory
INFO stats
DBSIZE

# 证书
echo | openssl s_client -connect ranch.example.com:443 -servername ranch.example.com \
  2>/dev/null | openssl x509 -noout -dates -subject
docker exec caddy caddy list-certificates
docker exec caddy caddy reload --config /etc/caddy/Caddyfile

# 备份
/opt/ranch/scripts/backup.sh
ls -lh /opt/ranch/data/backups/
/opt/ranch/scripts/restore.sh /opt/ranch/data/backups/20250101_030000.tar.gz

# TB 升级流程
/opt/ranch/scripts/backup.sh
vim /opt/ranch/.env       # 改 TB_VERSION
docker compose -f $COMPOSE pull tb-node
docker compose -f $COMPOSE up -d tb-node
docker compose -f $COMPOSE logs -f tb-node  # 看 "Started ThingsBoardApplication"

# 系统
htop; df -h; free -h; iostat -x 1; ss -tlnp
docker stats --no-stream

13. 高危坑点 TOP 10

  1. TB 升级前必须备份------容器启动自动 schema 迁移,失败 TB 起不来,没备份就得手动修库
  2. JVM 堆不超过物理内存 50% -------Xmx=6G 在 8G 机器上 = PG/Redis/系统没内存 = 整体 swap 卡爆
  3. PG shared_buffers 25% 而非越大越好------PG 依赖 OS Page Cache,要给系统留一半缓存
  4. 遥测必须设保留策略------500 设备 90 天不清理 = 几十 GB,VACUUM 跑不动
  5. 备份必须做恢复演练------备份可能 0 字节、pg_dump 静默失败、关键表漏了
  6. 生产禁止 docker compose down -v------会删所有数据卷(PG/Redis/TB 全没)
  7. 规则链 Debug 模式生产必关------每条消息写 debug event,1天几个 G 撑爆盘
  8. 公网 MQTT 必须开 TLS------Access Token 明文,Wireshark 能抓
  9. API Key 最小权限 ------H5 后端 key 只给 READ_TELEMETRY / READ_DEVICES,别给全权限
  10. TB 镜像不映射 docker.sock ------/var/run/docker.sock 映射进容器 = 给 root

14. FAQ

Q:生产一定要 K8s 吗?

不需要。500-1000 台单机 Compose 完全够用,运维成本低。K8s 是 5000+/高可用场景才需要。

Q:TimescaleDB 必装吗?

强烈建议。压缩率 5-10 倍、自动保留、连续聚合,不装 3 个月后 ts_kv 几十 GB。

Q:Caddy vs Nginx?

新手选 Caddy(自动 HTTPS 极简);前端静态用 Nginx(缓存/gzip 更成熟)。两个都用不冲突

Q:JVM 堆多大?

物理内存 30-40%,不超过 50%。4C8G 给 2-3G;8C16G 给 4-6G。Grafana 看 Full GC 后老年代稳定在 50-70% 即可。

Q:遥测保留多久?

原始 90 天,小时聚合 1 年,天聚合永久。合规要 3 年就存聚合数据,原始 GPS 不必存那么久。

Q:本地备份够吗?

必须异地! 硬盘坏/误删/机房故障/勒索病毒都能一起没。对象存储 100G 几块钱/月。

Q:单机能扛多少设备?

4C8G + SSD,每设备 5 分钟一次遥测,轻松 500-1000 台。牧场 500 台 = 1.7 msg/s,利用率不到 1%。

Q:MQTT 公网要 TLS 吗?

必须。 设备 Access Token 每次连接都裸奔,TLS 是免费且必要的。

Q:ChirpStack 和 TB 要分开吗?

小项目放一起(200MB 内存而已),3000+ 设备再拆分。

Q:H5 后端 Docker 还是 PM2 直跑?

PM2 直跑没毛病,但 Docker 三个好处:环境一致、故障自愈、部署标准化。不熟 Docker 就 PM2。

Q:H5 怎么零停机部署?

前端:换 dist 目录 + symlink 切换;后端:pm2 reload all(要正确处理 SIGTERM)。TB 升级做不到零停机(要 schema 迁移),凌晨 2 点操作,停机 1-3 分钟。

Q:出问题第一件事?

看日志docker logs tb-node --tail=200 能告诉你 90% 的答案。不要猜。


附录:参考资源


核心心法:备份 + 监控 + 恢复演练 = 生产的三大支柱。少一项都是裸奔。

相关推荐
慧都小妮子11 天前
ThingsBoard PE 智慧灌溉:实现精准灌溉与水资源优化利用
物联网·mqtt·智慧农业·iot·规则引擎·thingsboard·精准灌溉
慧都小妮子1 个月前
ThingsBoard PE 智慧能源:打造企业数字化能源管理平台
运维·物联网·能源·数据采集·数据可视化·thingsboard·制造业数字化
慧都小妮子1 个月前
ThingsBoard PE 接入物联网关 MQTT 数据配置流程:从规则链到遥测入库
物联网·mqtt·数据采集·iot·规则引擎·工业通信·thingsboard
Rysxt_8 个月前
ThingsBoard RPC订阅教程
mqtt·rpc·thingsboard
_三石_1 年前
Thingsboard 3.4 源码运行 Mac Mini
thingsboard
三阶码叟1 年前
基于 Netty 框架的 Java TCP 服务器端实现,用于启动一个 TCP 服务器来处理客户端的连接和数据传输
tcp·thingsboard
宁波阿成2 年前
用docker Desktop 下载使用thingsboard/tb-gateway
docker·容器·gateway·iot·thingsboard
徒步僧2 年前
ThingsBoard规则链节点:Clear Alarm节点详解
thingsboard
Java程序之猿2 年前
Linux内网环境部署thingsboard(离线部署)
linux·服务器·thingsboard