Files
sless/doc/decisions/iot-telemetry-storage-2026-04-04.md
T

6.9 KiB
Raw Blame History

Решение: IoT Telemetry Storage Architecture

Дата: 2026-04-04

Агент: GitHub Copilot (Claude Sonnet 4.6)

Статус: ПРИНЯТО


Контекст

IoT платформа принимает данные с датчиков через MQTT. Данные проходят: EMQX → iot-mqtt-bridge → RabbitMQ → event-dispatcher → function pod.

Проблема: данные не сохраняются. Функция получает событие и забывает его. Для клиентов (мониторинг объектов, счётчики, производство) нужно:

  • Автоматическое хранение всей телеметрии
  • Доступ к историческим данным
  • Низкий порог входа — не требовать от клиента настройки БД

Решения

1. Хранилище — Postgres, отдельная DATABASE per tenant

Выбрано: один Postgres инстанс, отдельная DATABASE на каждого клиента.

Отклонено: одна таблица с tenant_id колонкой.

  • Причина: изоляция только программная. Ошибка в WHERE → утечка чужих данных.

Структура:

Postgres (StatefulSet в namespace iot)
├── sless_platform   — системные данные платформы (tenants, etc)
├── tenant_{hash}    — данные клиента A (полная изоляция)
└── tenant_{hash}    — данные клиента B (полная изоляция)

Безопасность:

  • Каждый tenant имеет свой Postgres USER с уникальным паролем (UUID)
  • Пароль генерируется при создании tenant, хранится в k8s Secret
  • Клиент B физически не может подключиться к DATABASE клиента A

2. Доступ клиента — только через REST API

Выбрано: клиент читает телеметрию через REST API платформы.

Отклонено: прямой доступ к Postgres через connection string.

  • Причина: Postgres внутри кластера, не должен торчать наружу. Security.

API:

GET /v1/namespaces/{ns}/iot/telemetry
    ?device={device_id}
    &from={RFC3339}
    &to={RFC3339}
    &limit={int}

GET /v1/namespaces/{ns}/iot/devices/{id}/last

Авторизация — Bearer токен (тот же механизм что и для functions).


3. Схема таблицы telemetry

CREATE TABLE iot_telemetry (
    id        BIGSERIAL PRIMARY KEY,
    device_id TEXT        NOT NULL,
    ts        TIMESTAMPTZ NOT NULL DEFAULT now(),
    payload   JSONB       NOT NULL
);

CREATE INDEX idx_iot_telemetry_device_ts
    ON iot_telemetry (device_id, ts DESC);

Почему JSONB: у каждого клиента разные наборы данных:

  • датчик температуры: {"temp": 22.5, "humidity": 60}
  • GPS трекер: {"lat": 55.75, "lon": 37.61, "speed": 60}
  • счётчик воды: {"liters": 1234.5, "flow": 0.3}

Фиксированная схема невозможна. JSONB + индекс по (device_id, ts) даёт достаточную производительность для малого и среднего бизнеса.


4. schema.sql при деплое функции

Клиент может положить schema.sql рядом с функцией:

my-function/
├── handler.py
├── schema.sql    ← CREATE TABLE IF NOT EXISTS my_alerts (...)
└── requirements.txt

При деплое оператор выполняет schema.sql в БД tenant'а. Это позволяет клиентам без знания Python настраивать дополнительные таблицы.


5. DB_DSN в функцию

При запуске function pod оператор прокидывает DB_DSN из Secret в env var:

DB_DSN=postgresql://tenant_abc:password@iot-postgres.iot.svc:5432/tenant_abc

Функция использует стандартный драйвер, не знает о деталях платформы.


6. Разделение sless и iot операторов

Решение: sless-operator и iot-operator — ОТДЕЛЬНЫЕ компоненты с раздельными namespace.

Мотивация:

  • В будущем могут быть в разных кластерах
  • Независимый деплой и масштабирование
  • Разные команды могут владеть компонентами
  • Нет cross-dependency в коде (loose coupling)

Namespace layout:

namespace: sless          — платформа serverless
                            (sless-operator, event-dispatcher, RabbitMQ, Postgres invocations)
namespace: sless-{hash}   — tenant функции (function pods каждого клиента)

namespace: iot            — платформа IoT
                            (iot-operator, EMQX, Postgres telemetry)
namespace: iot-{hash}     — tenant IoT ресурсы (IoTDevice CRDs)

Связь между sless и iot:

  • Общий идентификатор tenant: {hash} одинаковый в sless-{hash} и iot-{hash}
  • MQTT событие → RabbitMQ в namespace sless → function pod в sless-{hash}
  • iot-operator НЕ импортирует Go пакеты sless-operator
  • Общение только через k8s API и RabbitMQ endpoints

Postgres:

  • sless: отдельный Postgres для invocations логов
  • iot: отдельный Postgres для telemetry per-tenant
  • Разные StatefulSet, разные PVC, разные credentials

7. Postgres инстанс для IoT

Выбрано: postgres:16-alpine StatefulSet в namespace iot.

Причина: простота для разработки. При передаче в production девопсы заменят на Managed Postgres от Nubes — connection string поменяется, код не меняется.

Ресурсы:

  • PVC: 10Gi (начальный размер, увеличивается по мере роста)
  • Memory limit: 512Mi
  • CPU: 0.5 cores

План реализации

  1. StatefulSet Postgres в namespace iot
  2. iot-operator: provisioning при создании IoTDevice namespace
    • CREATE USER tenant_{ns} PASSWORD '{uuid}'
    • CREATE DATABASE tenant_{ns} OWNER tenant_{ns}
    • CREATE TABLE iot_telemetry + индекс
  3. iot-mqtt-bridge: INSERT telemetry при получении MQTT сообщения
  4. iot-operator: REST API /v1/namespaces/{ns}/iot/telemetry
  5. sless-operator: при деплое function → прокинуть DB_DSN + выполнить schema.sql