175 lines
6.9 KiB
Markdown
175 lines
6.9 KiB
Markdown
# Решение: 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
|
||
|
||
```sql
|
||
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
|