Files
IoT/HISTORY/2026-08-16-session-log.md
T

1206 lines
88 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Журнал сессии — 2026-08-16 (IoT: интеграция с shared-sqs, выбор пути миграции)
**Рабочая папка:** /home/naeel/nubes/IoT
> Правило этой сессии (задано пользователем): ВСЁ документировать в HISTORY,
> каждый шаг — сразу.
---
## 1. Контекст (состояние на входе)
- Пользователь завершил handoff по shared-sqs:
- `HANDOFF-NEW-CHAT.md` — в SQS-репозитории, закоммичен и запушен (`5fa34c2`).
- `doc/sqs-integration.md` — создан в IoT-репозитории, **не закоммичен** (untracked).
- `2026-08-13-migration-to-managed.md` — также untracked в IoT-репо.
- Git IoT: HEAD `368ff06` (feature/replace-kafka-with-sqs), origin отстаёт на 1 коммит
(HEAD не запушен: «fix(ingress): remove cert-manager + ssl-redirect»).
## 2. `doc/sqs-integration.md` — что важно для IoT (прочитан, суть)
- shared-sqs: прод v0.1.35, все тесты зелёные (api 22/22, sdk 15/15, FIFO/DLQ e2e),
нагрузка 168 591 операция — 0 сервисных ошибок.
- Внешний endpoint: `https://sqs.containerk8s.dev.nubes.ru` (регион us-east-1).
- Внутренний endpoint (поды того же кластера Nubes, realm iot-naeel):
`http://containerk8s.f1ffb134-7d16-45bd-8bef-69f6ec8ab33c.svc.cluster.local:4100`.
- Проблемы ПЛАТФОРМЫ (не сервиса):
1. Таймауты каждые 31–33с на внешнем пути (~5.5% запросов без ответа 30с).
Доказано tcpdump; внутри кластера потерь нет (port-forward 24977 раундов, 0 сбоев).
2. MSS=1448 при MTU 1400 → тела >~1400 байт виснут ~51с (PMTUD сломан).
Тикет Nubes подготовлен (`SQS-service/doc/thinking/nubes-ticket.md`).
- Рекомендации интеграции: AWS SDK + override endpoint; главная — внутренний адрес
(обходит шлюз полностью); ретраи ≥3, read_timeout ≥30с (для внешнего пути);
сообщения до ~1300 байт; VisibilityTimeout 30с+; FIFO требует MessageGroupId
(dedup 5 мин); DLQ через RedrivePolicy; лимит 50 очередей на тенанта.
- Креды детерминированы из email: tenantID `t-`+sha256(email)[:8],
AccessKey `SSAK-`+sha256(email)[:12], SecretKey sha256(email+":shared-sqs:secret-key:v1").
- Блокер прода — только тикет Nubes (для интернет-клиентов); для внутренней
интеграции блокеров нет.
## 3. Как сейчас развёртывается IoT (факты из репозитория)
- Код полностью на Go 1.25, модуль `gitea.services.ngcloud.ru/Nail/IoT`.
- Один образ Docker Hub `naeel/iot-operator:v0.2.6` (distroless), три бинарника:
`/iot-operator`, `/mqtt-bridge`, `/sqs-consumer`.
- Собственный k8s кластер `iot-naeel`, namespace `sless`, API `185.247.187.149:6443`.
- Деплой с ВМ: `ssh naeel@5.172.178.213`, каталог `/home/naeel/terra/IoT`,
`kubectl apply -f deployments/k8s/` (kubeconfig на ВМ, токен ~24ч).
- Компоненты:
- `iot-operator` — controller-manager (CRD IoTDevice) + REST API :9090;
- `iot-mqtt-bridge` — EMQX → SQS SendMessage;
- `iot-sqs-consumer` — SQS → Postgres (long poll 20с, delete после записи);
- EMQX 5.5.1 — MQTT-брокер (HTTP auth → operator).
- Секреты (ns sless): `iot-bridge-credentials`, `iot-sqs-credentials`
(`SQS_ENDPOINT=https://qu.kube5s.ru` — старый внешний адрес, устарел),
`iot-postgres-secret` (managed PG, `...svc.cluster.local:5432/sqsdb`).
- Ingress: устройства по `wss://iot.kube5s.ru/mqtt`.
- Миграция на managed Node.js (план от 13.08) — НЕ выполнена; Node.js-кода в репо нет.
## 4. Обсуждение: HTTP-контейнер vs Managed Node.js (вывод этой сессии)
- Вывод AI: HTTP-контейнер (как у shared-sqs) лучше: Go сохраняется целиком,
Dockerfile готов, схема доказана shared-sqs, внутренняя сеть без таймаутов.
Managed Node.js = полный rewrite (aedes, Express, @aws-sdk/client-sqs, pg) — запасной.
- Открытый риск контейнера: поддержка WebSocket upgrade на платформенном ingress
не проверена (устройства ходят по wss).
- Устройства в любом варианте ходят через внешний шлюз Nubes — проблемы шлюза
(5.5% таймаутов, MSS) остаются риском для wss в обоих вариантах.
## 5. Чтение SQS-репозитория (`/home/naeel/nubes/SQS-service`) — выяснено
- **План Node.js фактически отменён.** `doc/2026-08-13-migration-to-nubes.md`:
«Код не меняется вообще. Только инфраструктура».
- shared-sqs мигрировал в **«Простой HTTP контейнер» (nubes_http)** через deck-UI,
realm `iot-naeel`. Домен `sqs.containerk8s.dev.nubes.ru`, `/health` OK.
- Технические факты платформы (из HISTORY 1314.08):
- Контейнер = k8s deployment `containerk8s`, namespace = UUID инстанса
(`f1ffb134-7d16-45bd-8bef-69f6ec8ab33c`), контейнер в поде называется `app`, порт 4100.
- Внутренний DNS: `containerk8s.<uuid-inst>.svc.cluster.local:4100`.
- Внешний: `*.containerk8s.dev.nubes.ru`, ingress/TLS встроены в контейнер.
- Образ — ТОЛЬКО публичный Docker Hub (Gitea/GHCR приватные платформа не тянет).
- Обновление образа — уникальным тегом (`latest` кэшируется зеркалом
`mirror.k8s.ngcloud.ru`); через `kubectl set image deployment/containerk8s app=...`
(правки через kubectl не откатываются — ownerReferences нет).
- Env vars — в deck-UI; Managed Redis — `redisk8s.<uuid>.svc.cluster.local:6379`.
- JWT-валидация: `https://lk-api-gateway.ngcloud.ru/api/v1/svc`
(deck-api*.ngcloud.ru — ЛЕГАСИ, не использовать).
- Long-running работает: SQS long-poll 20с; HTTP-соединение через ingress
держится ~150с (не режется).
- **WebSocket в SQS-истории не упоминается вообще** — поддержка wss на платформе
не проверялась. Единственный реальный риск контейнерного пути для IoT.
## 6. Вывод и следующий шаг
- Путь миграции IoT (кандидат): «Простой HTTP контейнер» в realm `iot-naeel`,
Go без переписывания; убрать operator/CRD → таблица `iot_devices` в Postgres;
bridge и consumer остаются Go.
- Первая проверка перед стартом: WebSocket upgrade (wss) через платформенный
ingress — тестовый контейнер с WS-эндпоинтом + `wscat`.
- ЖДЁМ решения пользователя по пути миграции.
---
## 7. Создан ws-probe — тестовый контейнер для проверки wss
**Команда пользователя:** «делай» (создать пробник + залить образ на Docker Hub;
для запуска в deck-UI нужен путь до образа).
**Что сделано** (все шаги):
1. Создан каталог `ws-probe/` в IoT-репозитории:
- `main.go` — Go echo-сервер на `gorilla/websocket`: `GET /health` (JSON
status+version), `GET /ws` (echo + ping каждые 20с, read-deadline 60с,
сбрасывается pong'ами — мёртвые соединения закрываются), `GET /` (HTML).
Без кредов, без внешних зависимостей.
- `Dockerfile` — multi-stage golang:1.25 → distroless/static:nonroot,
порт 8080 (меняется env `PORT`), версия через ldflags.
- `Makefile``docker-build`/`docker-push`, `VERSION=v0.1.0`, `IMAGE=naeel/iot-ws-probe`.
- `go.mod`/`go.sum` — отдельный модуль (gorilla/websocket v1.5.3), чтобы
не тащить зависимость в основной go.mod IoT.
2. `.gitignore` — добавлен `ws-probe/ws-probe` (сборочный бинарник).
3. Локальные проверки: `go vet` OK; smoke: `/health``{"status":"ok","version":"v0.1.0"}`;
WS-апгрейд → `HTTP/1.1 101 Switching Protocols` (Sec-WebSocket-Accept корректный).
4. Docker: логин в Docker Hub под `naeel` активен (проверен `docker info`).
5. Образ собран и запушен (публичный):
- **`naeel/iot-ws-probe:v0.1.0`** (и `:latest`),
- digest `sha256:dc029be3e79fb37f15ce23477096e340ab181b96c0132dc2e443f341e36f0688`,
- размер 10.9MB.
6. Коммиты: `27cb40e` (ws-probe), `2a7ecd2` (HISTORY). Пуш НЕ делался (не просили).
**Инструкция для deck-UI («Простой HTTP контейнер»):**
- Образ: **`naeel/iot-ws-probe:latest`** — стабильный путь БЕЗ уникального тега.
⛔ ПРАВИЛО (от пользователя): в deck-UI путь к образу фиксируется при создании
и НЕ редактируется — с тегом `:v0.1.0` мы не смогли бы поменять версию.
Обновление версии — новым пушем в `latest` + redeploy.
⚠️ Оговорка: зеркало платформы кэширует `latest` (случай shared-sqs) — если
redeploy подтянет старый digest, запасной путь: `kubectl set image
deployment/containerk8s app=naeel/iot-ws-probe:<новый-уникальный-тег>`.
- Env vars: не нужны (порт 8080 — дефолт; при необходимости `PORT`).
- Никаких кредов не требуется.
**Критерии проверки wss после деплоя** (будут применены):
- `curl -i -H "Connection: Upgrade" -H "Upgrade: websocket" ...` → 101;
- `wscat -c wss://<домен>/ws` → подключение, echo, соединение живёт 5–10 мин;
- переподключения стабильны; ping/pong проходят.
**Следующий шаг:** пользователь создаёт контейнер в deck-UI с этим образом —
далее тест wss по критериям выше.
---
## 8. Контейнер задеплоен в deck-UI — тест wss (16.08, ~07:26 GMT+03)
**Контейнер:** `https://testiot.containerk8s.dev.nubes.ru/` (создан пользователем
в deck-UI, образ `naeel/iot-ws-probe:latest`).
**Результаты проверок (первые):**
1. `GET /health``{"status":"ok","version":"v0.1.0"}` — под стянул наш образ.
2. `GET /` → HTML-страница ws-probe (200).
3. **WebSocket upgrade через платформенный ingress → `HTTP/1.1 101 Switching Protocols`**,
`Sec-WebSocket-Accept` корректный, HSTS на месте. (В выводе curl первая строка
`HTTP/1.1 200 Connection established` — локальный CONNECT-прокси машины,
ответ 101 пришёл от платформы.)
4. **`wscat -c wss://.../ws -x "probe-echo-1"` → echo вернулось, exit 0.**
Двусторонний обмен работает.
**КЛЮЧЕВОЙ ВЫВОД:** платформа Nubes («Простой HTTP контейнер») пропускает
WebSocket upgrade — wss для устройств возможен, путь контейнера подтверждён.
**Длительный тест стабильности** (запущен 08:26:10 по `date` = 07:26 GMT+03):
- скрипт `/tmp/ws-stability-test.py`: соединение 10 мин, echo каждые 10с,
ping/pong обе стороны (клиент ping_interval=25, сервер ping 20с),
лог с таймстемпами. Результат будет дописан ниже.
### РЕЗУЛЬТАТ длительного теста (16.08, обрыв на 173.1с) — ПРОБЛЕМА
Соединение оборвалось на **173.1с** (08:29:03 по `date` = 07:29 GMT+03).
Хронология (полный лог выше в терминале, 08:26:1008:29:03):
| t (с) | Событие |
|---|---|
| 0.4 | CONNECTED |
| 10120 | echo 1..12 — все возвращаются ✓ |
| 20.4140.4 | PING сервера каждые 20с — доходят ✓ |
| 50.5125.5 | PONG сервера на ping клиента каждые 25с — доходят ✓ |
| ~150 | ping клиента (25с-каденс) — БЕЗ pong; echo 1617 пропали; PING сервера 160.4 не пришёл |
| 170.1 | клиент: ERROR ping/pong timed out |
| 173.1 | CLOSED; итог sent=17, echoed=15 |
**Анализ:** первые 140с — идеально (двусторонний обмен, ping/pong обе стороны).
Деградация началась ~145–155с, обрыв ~150–173с. Похоже на платформенный
**лимит жизни соединения на внешнем пути ~150с** (перекликается с квирком
shared-sqs: «GET /ui через ingress не закрывает соединение ~150с»).
**Что это значит:** если лимит подтвердится — wss-устройства будут отваливаться
каждые ~2.5–3 мин независимо от MQTT keepalive (обрыв при активном трафике,
не по idle). Миграция на контейнер НЕ отменяется, но перед стартом нужно
выяснить и снять лимит (тикет Nubes).
**Следующие диагностические шаги (предложены, ждут «делай»):**
1. Повторить тест 3 раза — воспроизводимость и точное время обрыва.
2. Логи пода пробника на платформе (`kubectl logs` на ВМ) — что видит сервер
в момент обрыва (read error / ping error).
3. Тест WS изнутри кластера на внутренний `containerk8s.<uuid>.svc.cluster.local:8080`
— если держится >10 мин: обрыв на внешнем шлюзе/ingress; если тоже рвётся:
проблема ближе к поду.
4. Держать обычное HTTP-соединение через ingress >150с — сравнение с WS.
5. По результатам — тикет в Nubes (лимит соединений на ingress/шлюзе).
---
## 9. Диагностика обрыва (16.08, команда «делай тесты»)
### 9.1 Инфраструктура пробника на платформе (через ВМ, kubeconfig живой)
- Namespace пробника: `2d9b96ff-0a50-41e5-a5d5-1cd9e5621555`, deployment
`containerk8s`, под `containerk8s-5dc7dcbfcf-d4ch6` (IP 172.16.2.227),
service `containerk8s` ClusterIP 10.106.45.135:8080.
- Рядом стандартные: pod vmagent (метрики), ns shared-sqs `f1ffb134-...` (2d9h),
старый чужой ns `df36c8af-...` (30d) — не трогали.
- Ingress платформы: ns `ingress`, `shturval-ingress-controller-controller`
(2 пода: 172.16.0.73 / 172.16.3.149), LB 185.247.187.151 (kube-vip).
DNS `testiot.containerk8s.dev.nubes.ru` → 185.247.187.151.
- В логах пробника источник WS-подключений = IP ingress-подов (172.16.0.73 /
172.16.3.149) — соединения терминируются на nginx.
### 9.2 Серверная сторона обрыва (логи пода)
- Тест 1: сервер получал echo каждые 10с до 04:28:40 UTC, затем ПАУЗА ~24с,
в 04:29:04 пришли СРАЗУ ДВА сообщения (echo-15 задержан на ~14с, echo-16 на
~4с) и сразу `ws read error: close 1000 (normal)` — закрыл КЛИЕНТ после
своего ping-timeout. Серверных ошибок нет.
- Вывод: сервер и под ни при чём — соединение застряло между клиентом и nginx.
### 9.3 Nginx (ingress) — чисто
- Access-логи обоих подов: `/ws` → 101, `request_time` = 173.851с / 173.850с
(полная жизнь соединения), статус 101, ошибок нет. Nginx НЕ закрывает
соединение и не видит аномалий.
- X-Forwarded-For показывает клиентов как 192.168.255.10 / 192.168.255.71 —
перед LB есть краевой шлюз/NAT (внешний путь Nubes).
- В error-логах за окно обрыва — только фоновые SSL-сканеры и посторонние
POST /graphql — к нашему обрыву отношения не имеют.
### 9.4 Воспроизводимость (внешний путь, 3 прогона)
- RUN1: обрыв **ровно на 173.1с** (ERROR ping timeout 170.1с, CLOSED 173.1с,
sent=17, echoed=15) — идентично тесту 1. Детерминированно.
- RUN2: запущен, ожидание (ожидаемый обрыв ~08:38:39 по `date`).
- Прогоны идут на РАЗНЫЕ ingress-поды — обрыв не зависит от пода.
### 9.5 Внутренний путь (port-forward с ВМ) — ИДЁТ
- `kubectl -n 2d9b96ff-... port-forward svc/containerk8s 18080:8080` (ВМ),
клиент node+ws (модуль `ws` установлен на ВМ в /tmp/wstest).
- Тест 600с, старт 07:34:55 MSK. На 155с — ЗДОРОВ: PING сервера каждые 20с,
PONG клиента каждые 25с (с 25-й секунды!), ECHO ok[12] на 120с.
- Различие с внешним путём видно уже по pong-каденсу: внутри pong приходит
с 25с, снаружи первый pong только на 50с — внешний шлюз «съедает» что-то
уже на 25-й секунде.
- Если внутренний тест доживёт 600с — обрыв окончательно локализуется на
внешнем краевом шлюзе (вне кластера), как и таймауты 3133с shared-sqs.
### 9.6 Предварительный вывод
- Контейнер, под, сервис, nginx — здоровы. Детерминированный обрыв внешних
WS-соединений на ~150–173с происходит на краевом шлюзе Nubes.
- Для IoT это значит: wss-устройства будут переподключаться каждые ~2.5–3 мин,
пока шлюз не починят (тикет Nubes, объединить с таймаутами 31–33с и MSS).
### 9.7 Финальные результаты всех тестов
| Тест | Путь | Клиент | Результат |
|---|---|---|---|
| Тест 1 | станция → внешний wss | python | обрыв 173.1с |
| RUN1 | станция → внешний wss | python | обрыв **ровно 173.1с** |
| RUN2 | станция → внешний wss | python | обрыв 163.4с (stall на ~150с) |
| RUN3 | станция → внешний wss | python | обрыв 163.7с (stall на ~150с) |
| VM-ext | ВМ → внешний wss | node+ws | **чисто 300.2с, 29/29 echo** |
| VM-int | ВМ → port-forward → под | node+ws | **чисто 600.1с, 59/59 echo** |
| echo.org | станция → публичный WS-echo | python | **чисто 200с+ (тест 300с)** |
| proxy | станция → прокси 172.17.192.1:10808 → wss | python | обрыв 163.9с (как прямой) |
**Выводы:**
1. Под/сервис/nginx платформы — полностью здоровы (VM-ext 300с чисто,
VM-int 600с чисто; nginx request_time = полное время жизни, ошибок нет).
2. Обрыв воспроизводится ТОЛЬКО с рабочей станции, причём и напрямую, и через
локальный прокси (172.17.192.1:10808) — 5/5 обрывов, точка сбоя ~150с.
3. Публичный WS-echo (echo.websocket.org) с той же станции — ЧИСТО. Значит
сеть станции в целом здорова; страдает именно маршрут станция → Nubes
(185.247.187.151).
4. ВМ (другой маршрут до платформы, p50 6–8мс по данным shared-sqs) — чисто.
5. ⚠️ Нюанс для вывода «шлюз Nubes»: станция и ВМ ходят разными маршрутами.
Если ВМ обходит внешний шлюз Nubes (короткий путь/пиринг) — то «таймер
~150с на внешнем шлюзе Nubes» подтверждается. Если ВМ проходит тот же
шлюз — тогда проблема в маршруте станции (ISP/прокси-сеть). Точно
различить может только тест с третьей независимой точки входа
(другой ISP, мобильная сеть).
6. Взаимосвязь с shared-sqs: таймауты 3133с (HTTP) и MSS 1448 наблюдались
с той же станции — вероятно, тот же проблемный маршрут/шлюз. Наши данные
добавляют к картине «~150с на долгоживущие соединения».
### 9.8 Следующие шаги (предложены, ждут «делай»)
1. Тест wss с третьей независимой точки (другой ISP/мобильная сеть) — решает
вопрос «шлюз Nubes vs ISP станции».
2. HTTP-цикл с той же станции на /health (запрос каждые 10с, 5 мин) — связать
с таймаутами 3133с shared-sqs.
3. Тикет в Nubes: wss-соединения с интернета рвутся на ~150с (детерминированно),
+ таймауты 31–33с и MSS 1448 (данные shared-sqs).
4. При проектировании IoT: устройствам нужен MQTT keepalive ≤60с + автореконнект,
а до фикса шлюза — считать интернет-подключения устройств нестабильными
(~2.5–3 мин жизни).
### 9.9 Тест с третьей точки (Vultr, 95.179.252.111) — ГИПОТЕЗА ПОДТВЕРЖДЕНА
Доступ: `root@95.179.252.111`, ключ `~/.ssh/vultr_openssh` (на ВМ-машине).
Python 3.10, websocket-client 1.9.0 установлен. Тот же скрипт, 300с.
| Тест | Результат |
|---|---|
| Vultr → testiot wss | **обрыв 163.3с** (stall ~150с) — ИДЕНТИЧНО станции |
| Vultr → echo.websocket.org | чисто (175с+, pong 150.1с) |
**ИТОГОВЫЙ ВЫВОД (доказано с трёх точек):**
- Обрыв долгоживущих WS-соединений на ~150с — **на краевом шлюзе платформы
Nubes** (внешний вход 185.247.187.151), а не в поде/ingress и не в ISP клиента:
два независимых интернет-маршрута (станция, Vultr) дают идентичный обрыв,
контрольные публичные WS-echo с обеих точек чисты.
- ВМ 5.172.178.213 ходит коротким маршрутом (p50 6–8мс) и шлюза не касается —
поэтому чиста.
- Внутри платформы (port-forward) всё идеально (600с, 59/59).
- Для IoT: wss-устройства из интернета будут отваливаться каждые ~2.5–3 мин.
Это блокер для прода; тикет Nubes обязателен (объединить с таймаутами 31–33с
и MSS 1448 из shared-sqs — тот же шлюз).
### 9.10 Рекомендации для тикета Nubes (готовы данные)
- Симптом: WS-соединение (wss через edge) детерминированно рвётся на ~150с
при активном трафике (echo каждые 10с, ping/pong 2025с). Доказано:
станция 5/5, Vultr 1/1; nginx внутри видит полную жизнь соединения
(request_time 173.85с), под чист; изнутри кластера и с ВМ — чисто.
- Воспроизведение: `wscat -c wss://testiot.containerk8s.dev.nubes.ru/ws`
(или python websocket-client, ping 25с / echo 10с) — обрыв на 150–173с.
- Связь с shared-sqs: таймауты 3133с (~5.5% HTTP-запросов), MSS=1448 при
MTU 1400 — вероятно, тот же внешний шлюз.
---
## 10. Консультация Sonnet (промпт → ответ) — критическая оценка
**Что сделано:** составлен промпт для Sonnet (жёсткий список файлов: HISTORY
2026-08-16, migration-to-managed, sqs-integration, architecture/current-v0.2.3,
deployment-v0.2.3; 9 вопросов; формат «Ответ/Рекомендация/Риски», без кода).
Ответ получен, оценён ниже.
**Ключевые рекомендации Sonnet (приняты):**
- Компоновка: EMQX-контейнер + Go-монолит (API+bridge+consumer в одном бинарнике).
- EMQX оставить (не mochi-mqtt), если настроится на платформе.
- SQS в середине потока СОХРАНИТЬ (буфер при падении PG, admin-stats).
- Auth: перенос Secrets→таблица iot_devices ДО переключения; закрыть
authTestMode и ADMIN_STATS_TOKEN до прода.
- 3 фазы миграции; откат — DNS обратно; критерий готовности переключения
устройств: wss ≥600с с 3 независимых интернет-точек.
- Вопросы в техподдержку Nubes: кастомный домен, лимит ~150с, таймауты
3133с, MSS, subprotocol mqtt, multi-port, PVC, latest-pull, лимиты.
**ПОПРАВКИ к ответу Sonnet (важно):**
1. **PG НЕ меняется.** Managed PostgreSQL уже используется текущим consumer'ом
(IOT_PG_DSN → postgresqlk8s…svc.cluster.local). Утверждение Sonnet «новые
данные пишутся в новую PG» — ОШИБКА. Телеметрию переносить не нужно вовсе;
переносятся только CRD-объекты (устройства+пароли).
2. **Платформенные контейнеры живут в ТОМ ЖЕ кластере**, что и старый деплой
IoT: ноды `iot-naeel-*`, ingress `shturval` — один кластер. Значит старый
вход `iot.kube5s.ru` (185.247.187.147), скорее всего, идёт через ТОТ ЖЕ
краевой шлюз — вариант Sonnet «EMQX на старом кластере как точка входа»
проблему не обходит (и его сомнение в обратную сторону неверно).
Проверяемо тестом: держать wss к `wss://iot.kube5s.ru/mqtt` 5+ мин —
если stall ~150с есть и там, текущие устройства уже страдают.
3. **Multi-port** у Sonnet заявлен как факт в п.1 («1883 внутренний для bridge»),
но в п.9.6 сам же помечен НЕИЗВЕСТНО — противоречие. Вероятный реальный
сценарий: один порт на контейнер → EMQX наружу только 8083 (wss), bridge
к EMQX тоже по wss внутри платформы.
4. **Конфиг EMQX через env — не блокер.** EMQX 5 поддерживает env vars
(EMQX_*); плюс можно собрать свой образ на базе emqx/emqx с вшитым
emqx.conf (публичный Docker Hub) — снимает ограничение полностью.
5. **/metrics уже есть** в текущем операторе (порт 8080, controller-runtime)
— переносится в монолит, а не «отсутствует» (ошибка Sonnet).
6. **bcrypt для паролей** — ок для НОВЫХ устройств, но для переноса старых
нужен скрипт: прочитать все Secrets `iot-{deviceId}` в ns sless и вставить
в таблицу с сохранением работоспособности (хэшировать при вставке).
7. Дубли при at-least-once: нужен dedup-ключ в InsertTelemetry
(например уникальный индекс по (namespace, device_id, received_at) или
message-id) — сейчас отсутствует.
**Следующий шаг (предложен, ждёт «делай»):**
- Тест wss к СТАРОМУ прод-домену `wss://iot.kube5s.ru/mqtt` (5 мин, станция +
Vultr) — определить, страдают ли текущие устройства от того же шлюза.
---
## 11. Статус старого деплоя IoT — «зомби» (проверено 16.08)
> ⛔⛔⛔ ЛЕГАСИ — НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ при принятии решений. Раздел оставлен
> только как фиксация факта. Старый IoT для миграции значения не имеет.
**Проверка по вопросу «старый прод жив?» (факты):**
1. ns `sless` жив (125d). Все 4 deployment'а Running: `emqx`, `iot-mqtt-bridge`,
`iot-operator`, `iot-sqs-consumer` (поды 117–125 дней без рестартов).
2. **Внешний вход мёртв:** `iot.kube5s.ru` не резолвится ни с ВМ (getent пусто),
ни со станции (getent exit 2); curl → 000/exit 35. DNS-запись снята —
устройства через него не ходят.
3. **Конвейер телеметрии мёртв с 14.08:** секрет `iot-sqs-credentials` в sless
содержит старый `SQS_ENDPOINT=https://qu.kube5s.ru` (удалён 14.08 вместе со
старым shared-sqs). Логи consumer (16.08, прямо сейчас): каждые 5с
`ERROR SQS ReceiveMessage failed ... lookup qu.kube5s.ru: no such host`.
Bridge подключён к EMQX, но сообщений нет (нет устройств).
**Вывод:** старый IoT — «зомби»: поды формально Running, но внешнего входа нет
(DNS снят), а SQS-звено указывает на удалённый сервис. Работающих устройств на
старом кластере нет → риск миграции «не сломать прод-устройства» минимален,
переносить по сути нечего (только CRD-объекты/пароли при желании).
**Отмена теста:** wss-тест к старому домену бессмыслен — точки входа нет.
Старый деплой можно выключать после переключения (или по команде пользователя
— сейчас НЕ трогаем).
---
## 12. Пометка ЛЕГАСИ всего, что относится к старому IoT (16.08)
**Команда пользователя:** «УБЕРИ ВСЁ, что касается старого IoT; стирать пока
не надо; ЧЁТКО пропиши везде, что это ЛЕГАСИ и НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ».
**Сделано:**
1. В шапку каждого файла старого IoT добавлена пометка
«⛔⛔⛔ ЛЕГАСИ (2026-08-16) — СТАРЫЙ IoT ... НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ»:
- 15 документов в `doc/` (кроме `doc/sqs-integration.md` — актуален);
- `2026-08-13-migration-to-managed.md` (старый Node.js-план — отменён);
- все 8 манифестов `deployments/k8s/`;
- `config/crd/bases/iot.kube5s.ru_iotdevices.yaml`;
- `cmd/iot-operator/main.go`, `controllers/iotdevice_controller.go`,
`api/v1alpha1/device_types.go`, `api/v1alpha1/groupversion_info.go`;
- `Makefile` (старые k8s-цели);
- `examples/` (README.md, main.tf, handler.py).
2. Создан корневой `LEGACY.md` — реестр: что легаси, что актуально.
3. Раздел 11 этого журнала помечен «НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ».
4. Удалений НЕ производилось.
**Актуальные файлы (НЕ легаси):** `HISTORY/`, `doc/sqs-integration.md`,
`ws-probe/`, Go-код `cmd/mqtt-bridge/`, `cmd/sqs-consumer/`, `internal/`
(переиспользуется в монолите).
---
## 13. Аудит вмешательства старого IoT + план Фазы 1 (16.08)
**Команда пользователя:** «НЕ ДОПУСКАТЬ, чтобы старые данные и файлы от прежнего
IoT вмешивались в проект. ПРОВЕРЬ.»
**Результаты аудита (факты):**
1. **Код:** k8s-зависимости есть ТОЛЬКО в `internal/api/handler/`:
- `handler.go` — controller-runtime client, runtime;
- `iot_device_handler.go` — corev1, apimachinery, metav1, CRD-типы
`iotv1alpha1` (CRUD устройств через K8s + MQTT-auth через Secrets);
- `iot_admin_stats_handler.go` — corev1, controller-runtime.
Всё остальное ЧИСТО: `router.go`, `middleware/`, UI-embeds, `iotpg/`,
`cmd/mqtt-bridge/`, `cmd/sqs-consumer/`.
2. **go.mod:** тяжёлые k8s-депы (k8s.io/api, apimachinery, client-go v0.26.0,
controller-runtime v0.14.1 + ~40 indirect) — держатся только из-за этих
трёх файлов. После переписывания хендлеров — `go mod tidy` их уберёт.
3. **Dockerfile (корень)** — собирал 3 старых бинарника; НЕ был помечен.
→ помечен ЛЕГАСИ (новый монолит получит собственный Dockerfile).
4. **CI:** в `.github/` только правила (copilot-instructions, pravila) —
старых пайплайнов нет. `bin/` в git не отслеживается.
5. **Данные:** старый shared-sqs (qu.kube5s.ru) удалён полностью — старых
очередей в новом shared-sqs нет. В managed PG остались старые тестовые
tenant-DB (изолированы; очистка — только по отдельной команде). Старые
k8s Secrets новым кодом не читаются.
**Правило для монолита (зафиксировано):**
- Новый код НЕ импортирует `controllers/`, `api/v1alpha1/`, k8s-клиент.
- Хендлеры устройств переписываются на PG (`iot_devices`), MQTT-auth — SQL.
- `LEGACY.md` дополнен разделом аудита.
**План Фазы 1 (согласован в обсуждении, ждёт «делай»):**
1. Новый `cmd/iot-service/main.go` + структура по пакетам (config, api, bridge,
consumer, storage/iotpg) — много мелких файлов, без god-файлов.
2. Хендлеры устройств/админки — на PG вместо K8s; authTestMode=false.
3. Bridge+consumer — переиспользуются как есть (MQTT-подписка, SQS long-poll).
4. Новый Dockerfile (один бинарник), образ `naeel/iot-service` (имя — на
подтверждении пользователя), env-дефолты вшиты, секреты — в deck.
5. Сборка → пуш latest → пользователь создаёт контейнер в deck-UI.
---
## 14. Перенос старых файлов в legacy/ (16.08, команда «делай»)
Всё старое физически вынесено из активного дерева (`git mv`, история сохранена):
- `deployments/``legacy/deployments/`
- `config/``legacy/config/`
- `examples/``legacy/examples/`
- старые доки `doc/` (api, architecture, decisions, thinking, deployment*,
iot-mvp-plan, progress, run-and-test, testing-v0.2.6, errors, infrastructure)
`legacy/doc/`
- `2026-08-13-migration-to-managed.md``legacy/`
- `Dockerfile``legacy/Dockerfile.old`
- `Makefile``legacy/Makefile.old`
В корне осталось активное: `cmd/` (bridge, consumer; оператор — до Фазы 1),
`internal/`, `doc/sqs-integration.md`, `HISTORY/`, `ws-probe/`, `go.mod`,
`LEGACY.md`.
Проверка: `go build ./...` — OK (компиляция не сломана, Go-код не переносился).
---
## 15. Фаза 1: монолит iot-service (16.08, команда «делай»)
**Структура кода (много мелких файлов, без god-файлов):**
- `cmd/iot-service/main.go` — точка входа: config → PG → SQS-клиент → API + горутины bridge/consumer; graceful shutdown.
- `internal/service/config/` — env-конфигурация: config.go (Load + проверка секретов), defaults.go (все дефолты).
- `internal/service/store/` — устройства в PG (таблица iot_devices): open.go (схема), device.go (модель+ошибки), devices.go (CRUD + auth-lookup).
- `internal/service/sqsclient/` — общий SQS-клиент (endpoint override, внутренний адрес shared-sqs).
- `internal/service/api/` — router.go (маршруты + CORS + /health), middleware/ (auth.go — authTestMode из env, logging.go), handler/ (devices, mqtt_auth, telemetry, admin, sqsstats), admin_embed/console_embed + ui/ (HTML скопирован).
- `internal/service/bridge/` — bridge.go (подключение к EMQX, подписка +/telemetry/+), handler.go (envelope → SQS).
- `internal/service/consumer/` — consumer.go (long-poll, VisibilityTimeout), processor.go (envelope → iotpg).
**Ключевые решения:**
1. Устройства — таблица iot_devices в PG (PRIMARY KEY namespace+name, UNIQUE
namespace+device_id); пароль генерится crypto/rand 32B hex при создании;
MQTT-auth — SQL + constant-time compare; last_connected обновляется.
2. k8s-зависимостей в новом коде НЕТ: старые хендлеры не тронуты, старый
оператор компилируется. k8s-депы в go.mod остаются до удаления легаси.
3. Admin-stats: PG (iotpg.GetAdminStats) + SQS (атрибуты очереди) + runtime
(version/uptime) — вместо статусов k8s-подов.
4. Env: дефолты вшиты в Dockerfile (порт 9090, внутренний SQS endpoint
f1ffb134...:4100, очередь iot-telemetry, регион us-east-1, long-poll 20с,
visibility 30с, clientID iot-bridge, MQTT 8083//mqtt, AUTH_TEST_MODE=false).
Секреты обязательны: IOT_PG_DSN, SQS_ACCESS_KEY, SQS_SECRET_KEY,
MQTT_USERNAME, MQTT_PASSWORD (+ ADMIN_STATS_TOKEN для статистики).
5. CORS "*" (консоль на домене платформы), /health и /healthz отдают
{"status":"ok","version":...}.
**Проверки:** go build ./... OK; go vet OK; gofmt чистый; go test ./... OK;
smoke: без env бинарник падает с перечнем недостающих переменных.
**Образ:** `naeel/iot-service:v0.1.0` (+latest), digest
`sha256:808b537fcbd1ca23e2c389a735162370645ed8188fec7ef0d6e09327b78cb568`, 15.2MB,
запушен на Docker Hub. Коммит `9d08473`.
**Следующий шаг:** пользователь создаёт контейнер в deck-UI (образ
`naeel/iot-service:latest`, секреты в env). Затем — EMQX-контейнер и проверки.
---
## 16. EMQX-образ для платформы + порядок деплоя контейнеров (16.08)
**Создан `emqx/`** (коммит `b5333b1`): Dockerfile на базе `emqx/emqx:5.5.1`
с вшитым `emqx.conf`:
- node.name/cookie/data_dir (обязательные поля EMQX 5);
- HTTP auth/ACL → монолит (`/internal/mqtt/auth`, `/internal/mqtt/acl`);
- listeners: ws 8083 (mqtt_path=/mqtt) — единственный порт контейнера,
tcp 1883 и dashboard 18083 — для диагностики, наружу не экспонируются;
- auth/ACL URL переопределяются env: `EMQX_AUTHENTICATION__1__URL`,
`EMQX_AUTHORIZATION__SOURCES__1__URL` (в образе — заглушки).
**Образ:** `naeel/iot-emqx:latest` (+v0.1.0), digest
`sha256:6c8a12188e673fdd243cd926187c9207cb2837dfe617997e6478c99a7bc1bc84`, 476MB.
**Порядок деплоя (env после создания не меняются):**
1. EMQX-контейнер: образ `naeel/iot-emqx:latest`, порт 8083. Узнать UUID
namespace EMQX (kubectl get ns / из deck).
2. iot-service-контейнер: образ `naeel/iot-service:latest`, env:
- секреты: IOT_PG_DSN, SQS_ACCESS_KEY, SQS_SECRET_KEY, MQTT_USERNAME,
MQTT_PASSWORD, ADMIN_STATS_TOKEN;
- MQTT_HOST = `containerk8s.<uuid-EMQX>.svc.cluster.local`.
3. Патч EMQX через kubectl (один раз, платформа не откатывает — прецедент
shared-sqs): `kubectl -n <ns-emqx> set env deployment/containerk8s
EMQX_AUTHENTICATION__1__URL=http://containerk8s.<ns-iot>.svc.cluster.local:9090/internal/mqtt/auth
EMQX_AUTHORIZATION__SOURCES__1__URL=http://containerk8s.<ns-iot>.svc.cluster.local:9090/internal/mqtt/acl`
4. ДО создания iot-service: креды тенанта IoT в shared-sqs (консоль /ui →
Credentials) + очередь `iot-telemetry`.
---
## 17. ПРОВЕРКА PostgreSQL (16.08, команда «проверь ПГ») — старый PG МЁРТВ
> ⛔ ДИСЦИПЛИНА (напоминание пользователя): всё от старого IoT НЕ УЧИТЫВАТЬ —
> ни старые DSN, ни старые креды, ни старые БД. Ниже — только фиксация факта
> проверки. Для нового проекта используются ТОЛЬКО новые ресурсы.
**Поправка к прошлым утверждениям:** «PG уже работает, тот же, что у старого
consumer» — НЕВЕРНО. Проверено фактически:
1. Namespace `dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5` (из старого DSN) —
**НЕ СУЩЕСТВУЕТ** (`NotFound`). Старый DSN `postgresqlk8s-master.dc5db45d-…`
не резолвится — мёртв.
2. **Живой managed PG есть** — другой инстанс:
- ns `509145c3-ba71-4687-a084-d9dcbc88d3d2` (125d),
- под `postgresqlk8s-0` (3/3 Running, 19d), Zalando postgres-operator,
- svc `postgresqlk8s-master` ClusterIP 10.104.214.32:5432,
- секреты кредов: postgres / standby / superuser / testuser (Zalando).
3. Проверка подключения (port-forward 15432 → master):
- `pg_isready` → accepting connections;
- старые креды IoT (`super`/<старый пароль>) → **FATAL password authentication failed**;
- суперюзер инстанса (`superuser` из секрета) → OK;
- БД инстанса: autotest, postgres, template0, template1, testdb.
**`sqsdb` ОТСУТСТВУЕТ** (старых баз IoT нет).
4. Вывод: для монолита нужен НОВЫЙ DSN на инстанс 509145c3-…:
`postgresql://superuser:<пароль>@postgresqlk8s-master.509145c3-….svc.cluster.local:5432/sqsdb`
+ предварительно создать БД `sqsdb` (owner superuser). iotpg сам создаст
tenant_credentials и per-tenant БД.
5. Осталось на ВМ: port-forward PG (процесс на 15432) — остановить по команде
(`pkill -f "port-forward.*15432"`).
---
## 18. PG подготовлен для монолита (16.08)
**Факты из консоли платформы (пользователь):** инстанс `postgresqlk8s`,
instanceUid `509145c3-ba71-4687-a084-d9dcbc88d3d2`,
internalMaster `postgresqlk8s-master.509145c3-….svc.cluster.local`.
Пользователи `superuser`/`testuser` — роль `ddl_user` (createdb, createrole),
НЕ настоящие PG-суперюзеры (поэтому CREATE DATABASE ... OWNER требует
GRANT роли — ошибка «must be able to SET ROLE» в первом прогоне).
**Создано (новые ресурсы, к старому IoT отношения не имеют):**
1. Роль `iot_service` (LOGIN, сгенерированный пароль).
2. БД `iotdb` (OWNER iot_service) — через GRANT→CREATE→REVOKE.
3. Проверка подключения под `iot_service` → OK (user=iot_service db=iotdb).
**DSN для deck-UI (пароль НЕ хранится в git — только в чате/деке):**
`postgresql://iot_service:<пароль>@postgresqlk8s-master.509145c3-ba71-4687-a084-d9dcbc88d3d2.svc.cluster.local:5432/iotdb?sslmode=disable`
iotpg при старте сам создаст tenant_credentials и per-tenant БД (как и раньше),
store сам создаст таблицу iot_devices.
---
## 19. Очередь iot-telemetry создана в shared-sqs (16.08)
- Создана через внешний endpoint `https://sqs.containerk8s.dev.nubes.ru`,
тенант `t-fec713a719ad0b33` (креды — в SQS-репо, HANDOFF-NEW-CHAT.md).
- QueueUrl: `http://us-east-1.goaws.com:4100/t-fec713a719ad0b33/iot-telemetry`,
list-queues подтверждает.
- Монолит использует те же креды тенанта + внутренний endpoint
(дефолт вшит в образ: containerk8s.f1ffb134-…:4100).
**Готово к созданию контейнеров в deck-UI (порядок):**
1. EMQX: образ `naeel/iot-emqx:latest` → узнать UUID namespace.
2. iot-service: образ `naeel/iot-service:latest` + env (см. ниже).
3. kubectl-патч auth/ACL URL в EMQX (команда в разделе 16).
---
## 20. EMQX-контейнер: ошибки deck и 502 — разбор (16.08)
### Ошибка 1: «Не указан applicationUsePort, expose содержит несколько портов»
- **Факт:** в форме deck-UI ПОЛЯ порта НЕТ — платформа берёт порт из EXPOSE образа.
- **Причина:** базовый `emqx/emqx:5.5.1` имеет 7 EXPOSE-портов
(18083, 1883, 4370, 5369, 8083, 8084, 8883), которые невозможно убрать из
дочернего Dockerfile (`docker inspect` подтверждает — порты наследуются).
- **Решение:** образ пересобран от чистого `debian:bookworm-slim` с deb-пакетом
EMQX 5.5.1: единственный `EXPOSE 8083`.
v0.1.1, digest `sha256:7de0ec8a…01dd08f`. Проверка: `{"8083/tcp":{}}`.
- **Урок:** для платформы «Простой HTTP контейнер» образ обязан иметь РОВНО один
EXPOSE-порт; наследуемые порты базового образа — блокер.
### Ошибка 2: 502 Bad Gateway (домен https://exqx.containerk8s.dev.nubes.ru/)
- **Диагностика:** deployment `containerk8s` в ns `2fdd9658-4995-494f-892a-d88dda7da1ee`,
READY 0/1, рестарты; лог пода:
`/usr/bin/emqx: line 457: ps: command not found`
- **Причина:** launcher-скрипт EMQX использует `ps`, а `debian:bookworm-slim`
не содержит пакета `procps`.
- **Решение:** добавлен `procps` в установку; v0.1.2, digest
`sha256:d3722528…debb30`, запушен (latest + v0.1.2). EXPOSE по-прежнему один.
- **Действие пользователя:** redeploy контейнера EMQX в deck-UI (образ
`naeel/iot-emqx:latest` — платформа подтянет новый digest по latest).
### Состояние
- EMQX-контейнер пока работает на старом сломанном образе — после redeploy
проверить: под Ready 1/1, лог без `ps: command not found`, wss-подключение.
- Образы: iot-emqx v0.1.2 (latest), iot-service v0.1.0 (latest) — оба на Docker Hub.
---
## 21. EMQX: mqtt_path исправлен, но на платформе boot висит (16.08)
**Ошибка 3 (найдена локальным запуском):** `unknown_fields path=listeners.ws.default`
— поле `mqtt_path` в EMQX 5.5.1 живёт в `listeners.ws.default.websocket.mqtt_path`.
Исправлено в emqx.conf → v0.1.3/0.1.4.
**Локальная проверка v0.1.4 (docker run на станции):** ПРОЙДЕНА — boot ~50с,
`emqx ctl listeners` OK (tcp:1883 running), wscat с subprotocol `mqtt` подключается
(curl без subprotocol получает 400 от Cowboy — так и должно: fail_if_no_subprotocol).
**На платформе (наш кластер, ns 2fdd9658-…):** v0.1.4 поставлен kubectl set image,
под Running 1/1, НО:
- нода НЕ поднимается 5+ мин: beam запущен (config сгенерирован в 09:43:36),
CPU всего ~5с (почти idle), /var/log/emqx ПУСТ, `emqx ctl status/listeners`
возвращают пустоту (нет даже ошибки);
- множество зомби `inet_gethost <defunct>` (DNS-хелперы Erlang);
- DNS в поде работает (getent OK), resolv.conf: search …svc.cluster.local, ndots:5;
- память 257/512Mi, рестартов 0;
- снаружи — 502 Bad Gateway (wscat: Unexpected server response: 502).
- PID1 = `su - emqx -c DEBUG=0 "/usr/bin/emqx" foreground` (поведение deb-пакета),
HOME=/var/lib/emqx, node name emqx@127.0.0.1, -start_epmd false, -proto_dist ekka.
**Статус:** локально образ рабочий, на платформе boot висит. По договорённости
с пользователем — вопрос к Sonnet (диагностика + правильный способ запуска EMQX
в контейнере: без su, логи в stdout, env-конфигурация).
---
## 22. EMQX РАБОТАЕТ на платформе (16.08, ~10:21 MSK) — развязка
**Диагностика по ответу Соннета + эксперименты (хронология):**
1. Гипотеза Соннета «DNS/ndots:5 зависание на резолве заглушки» — ОПРОВЕРГНУТА
замером в поде: `getent hosts iot-service` = 103мс (быстрый NXDOMAIN).
2. deb-сборки (v0.1.4–0.1.6) на платформе «висели»: PID1=beam, CPU ~5с, логов
нет. Эксперимент: **официальный emqx/emqx:5.5.1** в тот же под — заводится,
логи в stdout есть → проблема не платформа, а наша deb-сборка.
3. **Пересборка от официальной базы:** `FROM emqx/emqx:5.5.1 + COPY emqx.conf`
(v0.2.0). Заглушки auth/ACL URL заменены на РЕЗОЛВИМЫЙ адрес shared-sqs
(containerk8s.f1ffb134-…:4100), чтобы пулы HTTP-auth не спотыкались на
nxdomain при старте.
4. **Настоящая причина «зависаний»:** с debug-логами видно — нода НЕ виснет,
а грузится ОЧЕНЬ МЕДЛЕННО при CPU-квоте 500m: каждое приложение ~5–7с
(long_schedule warnings), полная загрузка 5–10 мин. Плюс dashboard-listener
падает с таймаутами (swagger-генерация под троттлингом) — на wss не влияет,
но убрать из конфига в следующей версии.
**Проверка (внешняя, домен exqx.containerk8s.dev.nubes.ru):**
- curl без subprotocol → 400 Bad Request от Cowboy (EMQX отвечает через edge,
fail_if_no_subprotocol — норма);
- `wscat -n -c wss://…/mqtt -s mqtt` → соединение устанавливается и держится.
- **502 устранён.**
**Состояние:** EMQX-контейнер рабочий (ns `2fdd9658-…`, образ
`naeel/iot-emqx:v0.2.0`, digest `sha256:e53f9b29…`). Auth/ACL URL пока указывают
на заглушку shared-sqs — после создания iot-service заменить на реальный адрес
монолита (kubectl set env + rollout).
**Следующие шаги:**
1. Создать контейнер iot-service в deck-UI (env: MQTT_HOST=
containerk8s.2fdd9658-4995-494f-892a-d88dda7da1ee.svc.cluster.local + секреты).
2. kubectl set env в EMQX: EMQX_AUTHENTICATION__1__URL /
EMQX_AUTHORIZATION__SOURCES__1__URL → http://containerk8s.<ns-iot>:9090/…
3. End-to-end: устройство (mqtt over wss) → EMQX auth → bridge → SQS → consumer
→ PG → REST API.
4. Позже: убрать dashboard из emqx.conf (v0.2.1), CPU-квоту ≥1000m для EMQX
в deck при создании (в нашем кластере можно kubectl patch).
---
## 23. Монолит iot-service поднят на платформе (16.08, ~10:50 MSK)
**Ошибки по пути (задокументированы):**
1. Первая попытка deck: под падал `required env vars not set: IOT_PG_DSN, …`
env-переменные из deck не применились. После пересоздания пользователем env
применились (config.Load прошёл).
2. Затем: `pg_hba.conf rejects connection ... no encryption (28000)` — PG
требует SSL. DSN заменён на `sslmode=require` (kubectl set env в НАШЕМ
кластере). ⚠️ Для чужого кластера: в deck указывать DSN с sslmode=require.
**Состояние (ns монолита `4504e05b-7b7f-4689-a61f-fbf6234dd0ed`):**
- Под 1/1 Running, логи: devices store ready, iotpg connected, telemetry store
ready, API :9090, bridge/consumer резолвят очередь iot-telemetry (внутренний
SQS endpoint работает).
- Внешне: `https://iot.containerk8s.dev.nubes.ru/health`
`{"status":"ok","version":"v0.1.0"}`, `/console` → 200.
- EMQX: set env EMQX_AUTHENTICATION__1__URL и EMQX_AUTHORIZATION__SOURCES__1__URL
→ адрес монолита; rollout, ожидание медленной загрузки (~5–10 мин).
- Bridge подключится к EMQX после перезагрузки EMQX (auth теперь идёт в монолит).
**Следующий шаг:** end-to-end проверка: создать устройство через REST API →
MQTT CONNECT (wss) с кредами устройства → publish телеметрии → SQS →
consumer → PG → чтение через /v1/.../telemetry.
---
## 24. END-TO-END ПРОЙДЕН (16.08, ~14:02 GMT+03) ✅
**Полная цепочка проверена на платформе:**
1. Устройство создано через API: POST /v1/namespaces/test/iot/devices
(dev1, device_id=dev-001) → mqtt_username `test_dev-001`, пароль получен.
2. MQTT-клиент (paho, wss://exqx.containerk8s.dev.nubes.ru:443/mqtt,
transport=websockets) — CONNECT принят (rc=0), publish в
`test/telemetry/dev-001` (rc=0, QoS1).
3. EMQX HTTP-auth → монолит: bridge `iot-bridge` подключён (лог EMQX:
clientid=iot-bridge, WS-MQTT, PINGRESP; монолит: `bridge: MQTT connected`).
4. Bridge → SQS: `forwarded telemetry to SQS` (внутренний endpoint).
5. Consumer → PG: `telemetry saved namespace=test device=dev-001`;
GET /v1/namespaces/test/iot/telemetry → count=1, payload {"e2e":true,"temp":23.5}.
**Ошибки по пути (исправлены, задокументированы):**
1. `permission denied to create role (42501)` — роль iot_service не имела прав
для per-tenant БД. Исправлено: `ALTER ROLE iot_service CREATEROLE CREATEDB`.
⚠️ Для чужого кластера: при подготовке БД давать роли эти права.
2. pg_hba SSL — DSN с sslmode=require (см. раздел 23).
**Открытые хвосты:**
- AUTH_TEST_MODE=true включён для теста (kubectl set env) — вернуть false
(kubectl set env AUTH_TEST_MODE=false или дефолт образа при пересоздании).
- EMQX: dashboard-listener падает с таймаутами (CPU 500m) — убрать из конфига
(v0.2.1) и/или квота CPU ≥1000m при создании в deck.
- Процедура для чужого кластера без kubectl: PG-auth в EMQX (вместо HTTP),
порядок деплоя EMQX→iot-service, UUID из консоли deck, DSN с sslmode=require,
роль с CREATEROLE/CREATEDB, один EXPOSE-порт в образе EMQX.
---
## 25. ПОДРОБНАЯ СВОДКА: что развёрнуто на платформе (16.08)
### Контейнеры (realm iot-naeel, кластер iot-naeel)
| Компонент | ns (instanceUid) | Домен | Образ |
|---|---|---|---|
| EMQX | `2fdd9658-4995-494f-892a-d88dda7da1ee` | exqx.containerk8s.dev.nubes.ru | naeel/iot-emqx:v0.2.0 |
| iot-service | `4504e05b-7b7f-4689-a61f-fbf6234dd0ed` | iot.containerk8s.dev.nubes.ru | naeel/iot-service:v0.1.0 |
| shared-sqs | `f1ffb134-7d16-45bd-8bef-69f6ec8ab33c` | sqs.containerk8s.dev.nubes.ru | (чужой) |
| ws-probe (тест) | `2d9b96ff-…` | testiot.containerk8s.dev.nubes.ru | (тестовый) |
### Managed-сервисы
- PostgreSQL: инстанс `postgresqlk8s`, instanceUid `509145c3-ba71-4687-a084-d9dcbc88d3d2`,
internalMaster `postgresqlk8s-master.509145c3-….svc.cluster.local:5432`.
Роль `iot_service` (CREATEROLE, CREATEDB), БД `iotdb`. DSN:
`postgresql://iot_service:<пароль>@…:5432/iotdb?sslmode=require` (SSL обязателен!).
- shared-sqs: тенант `t-fec713a719ad0b33`, очередь `iot-telemetry`,
внутренний endpoint `containerk8s.f1ffb134-….svc.cluster.local:4100`.
### Env iot-service (заданы/пропатчены)
IOT_PG_DSN (sslmode=require), SQS_ACCESS_KEY, SQS_SECRET_KEY, MQTT_USERNAME,
MQTT_PASSWORD, ADMIN_STATS_TOKEN, MQTT_HOST=containerk8s.2fdd9658-….svc.cluster.local,
AUTH_TEST_MODE=true (ТЕСТ! вернуть false).
### Env EMQX (пропатчены kubectl)
EMQX_AUTHENTICATION__1__URL=http://containerk8s.4504e05b-….svc.cluster.local:9090/internal/mqtt/auth
EMQX_AUTHORIZATION__SOURCES__1__URL=http://containerk8s.4504e05b-….svc.cluster.local:9090/internal/mqtt/acl
EMQX_LOG__CONSOLE_HANDLER__LEVEL=debug (диагностика)
### Правила платформы (выстраданы)
1. Путь к образу в deck фиксируется при создании → только latest + redeploy;
зеркало кэширует latest → в НАШЕМ кластере kubectl set image уникальным тегом.
2. Env в deck фиксируются при создании (формат JSON в форме — выяснить!).
3. Порт приложения берётся из EXPOSE образа; поле порта в форме отсутствует →
образ обязан иметь РОВНО один EXPOSE.
4. Загрузка EMQX на CPU-квоте 500m — 5–10 мин (не зависание!); dashboard
падает с таймаутами; для deck задавать CPU ≥1000m.
5. PG требует SSL: pg_hba no encryption → sslmode=require.
### Хвосты (текущий план работ)
1. AUTH_TEST_MODE=false.
2. EMQX v0.2.1: убрать dashboard из конфига; CPU-квота (наш кластер — patch,
deck — рекомендация).
3. PG-auth в EMQX (без kubectl для чужого кластера): authentication postgres
(devices + bridge) + authorization postgres + http-фолбэк; монолит создаёт
bridge-строку в iot_devices (EnsureBridgeDevice).
4. Процедура деплоя для чужого кластера (документ).
---
## 26. Tail 2 выполнен + найден и исправлен баг реконнекта бриджа (14:30 GMT+03)
### 26.1 EMQX v0.2.1 — dashboard выключен
- `emqx/emqx.conf`: `dashboard { listeners.http { enable = false } }`.
Причина: на CPU-квоте 500m swagger-генерация dashboard падает с таймаутами
(crash-loop emqx_dashboard_listener), засоряет логи.
- `emqx/Makefile`: VERSION → v0.2.1. Сборка локально, smoke-тест
(docker run -p 18083:8083 + wscat -s mqtt — соединение держится), пуш
`naeel/iot-emqx:v0.2.1` + latest (digest 16974eac84e0).
- Деплой на платформу: `kubectl -n 2fdd9658-… set image deployment/containerk8s
app=naeel/iot-emqx:v0.2.1` + patch CPU: limits cpu=2, memory=1Gi
(deck-рекомендация: задавать CPU ≥1000m при создании).
- Проверка v0.2.1: под Running, bridge-подключение живо (PINGREQ/PINGRESP),
внешний wss: wscat держит соединение, curl без subprotocol → 400 (норма),
dashboard-падений в логах нет.
### 26.2 БАГ: бридж не переподписывался после реконнекта (найден в 26.1)
- Симптом: живая публикация (paho, rc=0) → в EMQX PUBLISH принят,
authorization_permission_allowed, publish_to — но до PG не дошёл;
в логах монолита после реконнекта нет второго "bridge: subscribed".
- Хронология: монолит v0.1.0 рестартован в 11:11 (AUTH_TEST_MODE=false),
"bridge: subscribed" в 11:11:18; EMQX перезапущен (v0.2.1), бридж
реконнект в 11:14:08 — БЕЗ повторной подписки; сессия EMQX потеряна →
подписчиков на +/telemetry/+ нет → сообщения молча теряются.
- Причина в коде: `SetCleanSession(false)` + подписка один раз в Run();
paho при реконнекте полагается на возобновление сессии сервером,
resubscribe не выполнялся.
- ФИКС `internal/service/bridge/bridge.go`: подписка перенесена в
OnConnectHandler (выполняется при КАЖДОМ подключении); handler передаётся
в connectMQTT; Run() больше не подписывается вручную.
- Монолит: Makefile VERSION → v0.1.1; go build/vet OK; образ
`naeel/iot-service:v0.1.1` + latest (digest ba120dff63d7) собраны и запушены.
- Деплой: `kubectl -n 4504e05b-… set image deployment/containerk8s
app=naeel/iot-service:v0.1.1`. В логах нового пода: connected + subscribed
(11:22:25), после вытеснения старого пода (close 1000 — EMQX кикает
дубль clientid) реконнект 11:22:25.109 + ПОВТОРНЫЙ subscribed — фикс работает.
### 26.3 Живой e2e после фикса (PASS)
- paho-mqtt (websockets, path=/mqtt) → wss://exqx.containerk8s.dev.nubes.ru,
client e2e-paho-2, username test_dev-001 → publish test/telemetry/dev-001
{"e2e_v011":true,"temp":42.0}.
- API (JWT структурный, Bearer): GET /v1/namespaces/test/iot/telemetry →
count=2: id=2 ts=2026-08-16T14:23:22+03:00 payload {"temp":42.0,"e2e_v011":true}.
- Цепочка подтверждена: устройство → wss → EMQX → bridge → SQS iot-telemetry →
consumer → PG (tenant_test) → API.
### 26.4 Инструментальные заметки
- Сырые MQTT-пакеты через python websocket-client снаружи → EMQX рвёт с
{error,bad_encoding} (оба теста); wscat и paho работают штатно →
для e2e-тестов использовать paho или wscat, не велосипеды на сырых кадрах.
- JWT для тестов API: структурный (3 части, payload с sub+exp), подпись не
проверяется (validateJWT) — минтить python-ом.
- EMQX-логика e2e-диагностики: grep EMQX-логов по clientid → CONNECT/auth →
authorization_permission_allowed → publish_to; монолит: auth/acl 200,
bridge subscribed; PG: API telemetry.
---
## 27. Tail 3 выполнен: PG-auth в EMQX (v0.2.4) + EnsureBridgeDevice (v0.1.2) (15:20 GMT+03)
### 27.1 Цель и итог
EMQX авторизует устройства и бридж НАПРЯМУЮ в PostgreSQL (таблица
iot_devices) — без HTTP-вызовов монолита и без kubectl на чужом кластере.
Порядок деплоя: EMQX первым (нужны только PG-env), монолит вторым.
Итог: живой e2e PASS (строка id=4, 15:17:40 GMT+03), негативные auth-тесты PASS.
### 27.2 Монолит v0.1.2 — EnsureBridgeDevice
- `internal/service/store/bridge.go` (новый): EnsureBridgeDevice upsert-ит
строку бриджа в iot_devices: namespace='__bridge', name='iot-bridge',
device_id=<MQTT username>, enabled=true, mqtt_password=<MQTT_PASSWORD>.
ON CONFLICT (namespace,name) DO UPDATE device_id/password/enabled.
- `cmd/iot-service/main.go`: вызов после store.Open (timeout 10s), при ошибке
os.Exit(1). Лог "bridge device ensured" username=iot-bridge (11:29:06).
- Makefile VERSION → v0.1.2; образ naeel/iot-service:v0.1.2 (digest
e269796c8f81) запушен и задеплоен. HTTP-хендлеры /internal/mqtt/auth|acl
ОСТАВЛЕНЫ в коде (fallback/локальные тесты), EMQX их больше не зовёт.
### 27.3 EMQX PG-auth — ВЫСТРАДАННАЯ СХЕМА 5.5.1 (важно для будущего!)
OSS-образ emqx/emqx:5.5.1 СОДЕРЖИТ postgres authn/authz
(emqx_auth_postgresql-0.1.1), НО:
1. Тип в конфиге — `postgresql`, НЕ `postgres`
(`backend = postgresql`, `type = postgresql`). `postgres` → валидация
падает: authentication.1 unsupported_mechanism / authorization
unknown_authz_type. Проверено: v0.2.2 (crash-loop на платформе).
2. `${VAR}`-интерполяция в emqx.conf НЕ работает (значения попадают
буквально). Только официальные env-переопределения:
EMQX_AUTHENTICATION__1__{SERVER,DATABASE,USERNAME,PASSWORD},
EMQX_AUTHORIZATION__SOURCES__1__{SERVER,DATABASE,USERNAME,PASSWORD}.
В конфиге — плейсхолдеры. Проверено: emqx ctl conf show — env
перекрывает конфиг (v0.2.3, локальный docker-тест).
3. Оставшиеся от HTTP-auth env `EMQX_AUTHENTICATION__1__URL` /
`EMQX_AUTHORIZATION__SOURCES__1__URL` → unknown_fields "url" →
CrashLoop. Удалены kubectl set env VAR-.
4. В authz-postgresql НЕ поддерживаются плейсхолдеры ${action}/${topic}
(${action} подставлялся литералом "{action}", epgsql падал
lists:zip function_clause). action/topic возвращаются КОЛОНКАМИ
результата: permission + action + topic. Устройствам — ДВЕ строки
(publish и subscribe) через (VALUES ('publish'),('subscribe')).
5. password_hash_algorithm = {name=plain, salt_position=disable} — пароли в
iot_devices хранятся открытым hex, сравнение прямое.
6. SSL обязателен: ssl { enable=true, verify=verify_none } (pg_hba платформы).
7. ENV для смены уровня лога: EMQX_LOG__CONSOLE_HANDLER__LEVEL=warning
(дефолт в проде; debug — только диагностика).
Итоговый authn-запрос:
SELECT mqtt_password AS password_hash FROM iot_devices
WHERE enabled=true AND (namespace || '_' || device_id = ${username}
OR (namespace='__bridge' AND ${username} = device_id)) LIMIT 1
Итоговый authz-запрос:
SELECT 'allow' AS permission, 'subscribe' AS action, '+/telemetry/+' AS topic
FROM iot_devices WHERE enabled=true AND namespace='__bridge'
AND ${username}=device_id
UNION ALL
SELECT 'allow' AS permission, a.action,
namespace || '/telemetry/' || device_id AS topic
FROM iot_devices, (VALUES ('publish'),('subscribe')) AS a(action)
WHERE enabled=true AND namespace || '_' || device_id = ${username}
### 27.4 Версии и деплой
- v0.2.2 (типы postgres) — CrashLoop: unsupported_mechanism/unknown_authz_type.
- v0.2.3 (типы postgresql + ${VAR} env) — CrashLoop: unknown_fields "url"
(старые URL-env). После удаления URL-env под поднялся, НО ACL-запрос
с ${action}/${topic} падал (см. 27.3.4) → бридж отваливался close 1005.
- v0.2.4 (ACL через колонки результата) — РАБОТАЕТ. digest cf80ec73973a.
- EMQX env на деплойменте (kubectl set env): EMQX_AUTHENTICATION__1__SERVER=
postgresqlk8s-master.509145c3-….svc.cluster.local:5432, DATABASE=iotdb,
USERNAME=iot_service, PASSWORD=<из IOT_PG_DSN монолита>; то же для
AUTHORIZATION__SOURCES__1__*; URL-env удалены; лог=warning.
- EMQX CPU-квота 2/1Gi сохранилась после всех деплоев (загрузка ~2 мин).
### 27.5 Проверка authn (paho, НЕ по rc connect()!)
⚠ paho connect() возвращает 0 ДАЖЕ при CONNACK reason≠0 — результат только
через on_connect rc! Первые «все приняты» — артефакт теста, не баг EMQX.
Корректные результаты (on_connect):
- test_dev-001 + неверный пароль → CONNACK rc=4 (bad username/password) — отказ.
- nosuch_user → CONNACK rc=5 (not authorized) — отказ.
- test_dev-001 + верный пароль → rc=0 — приём.
- EMQX-лог: authentication_result {ok,{error,not_authorized}} →
CONNACK ReasonCode=5 → websocket_terminated not_authorized.
### 27.6 e2e и бридж
- Бридж (username iot-bridge, пароль из строки __bridge) подключился и
подписался на +/telemetry/+ при PG-auth (12:13:11, повторно после каждого
рестарта EMQX — фикс resubscribe из секции 26 работает).
- Финальный e2e: e2e-final-1 CONNACK rc=0 → publish test/telemetry/dev-001
{"e2e_pg_final":true,"temp":42.0} → API: id=4 ts=2026-08-16T15:17:40+03:00.
- Цепочка: устройство → wss → EMQX (authn+authz=PG) → bridge → SQS →
consumer → PG → API. Без HTTP-зависимости EMQX↔монолит.
### 27.7 Неожиданные события платформы (задокументированы)
- ~11:33-11:40 kubectl с ВМ: "You must be logged in to the server
(Unauthorized)" на ВСЕ вызовы (get ns тоже). Самовосстановилось.
Диагностика велась через внешние каналы (curl/wss/paho) и локальный docker.
- Во время окна недоступности бридж завис в одной попытке дозвона (тишина
в логах) → вылечено rollout restart монолита. Для будущего: в paho
выставить SetConnectTimeout явно (кандидат на hardening).
---
## 28. Tail 4 выполнен: процедура деплоя без kubectl (15:35 GMT+03)
- Создан `doc/deployment-nubes-production.md` — полная процедура деплоя IoT
на чужом кластере Nubes БЕЗ kubectl:
1. PG: роль iot_service (LOGIN, CREATEROLE, CREATEDB), БД iotdb owner-ом,
SSL обязателен (sslmode=require / EMQX ssl.enable).
2. EMQX ПЕРВЫМ (naeel/iot-emqx:v0.2.4): env только PG
(EMQX_AUTHENTICATION__1__*/EMQX_AUTHORIZATION__SOURCES__1__*),
CPU ≥1000m; предупреждение про запрещённые URL-env и ${VAR}.
3. iot-service ВТОРЫМ (naeel/iot-service:v0.1.2): MQTT_HOST=
containerk8s.<uuid-emqx>.svc.cluster.local (UUID из консоли deck),
DSN sslmode=require, SQS-креды, MQTT_USERNAME/PASSWORD.
4. Устройства: регистрация через API (JWT структурный), username
<ns>_<device_id>, wss на домен EMQX, топик <ns>/telemetry/<device_id>;
негативные auth-тесты (CONNACK rc=4/5) — чек-лист.
5. Обновления образов без kubectl — только пересоздание контейнера
(путь к образу фиксируется при создании, latest кэшируется зеркалом).
6. Диагностика и известные ограничения платформы (wss ~150с, SQS
внешний путь, фиксированные env).
- Версии зафиксированы в шапке документа.
## 29. Итог сессии (хвосты 1–4 закрыты)
| Хвост | Результат |
|---|---|
| 1. AUTH_TEST_MODE=false | Выполнено (kubectl set env, 11:11). |
| 2. EMQX без dashboard + CPU | v0.2.1 (dashboard off), CPU 2/1Gi patch. |
| 2а. Баг resubscribe бриджа | Найден и исправлен (v0.1.1, OnConnectHandler). |
| 3. PG-auth в EMQX | v0.2.4 (postgresql authn+authz) + EnsureBridgeDevice v0.1.2. |
| 4. Процедура деплоя | doc/deployment-nubes-production.md. |
Версии: iot-service v0.1.2, iot-emqx v0.2.4 (Docker Hub, latest обновлены).
---
## 30. Ресурсы кластера + новая директива (15:55 GMT+03)
### 30.1 Замер ресурсов кластера (kubectl, фактические цифры)
Ноды (4): control-plane-xb699, workers-6f74n, workers-bhbvs, workers-v8zq4.
Факт (kubectl top nodes): control-plane 33% CPU / 53% RAM; workers 1419% CPU /
24–40% RAM. Свободных ресурсов много.
Аллокация requests/limits: workers ~3.54.0 CPU requests, limits 11.514.3 CPU
(оверкоммит), память 16–31% / 4577%.
Наши контейнеры:
- EMQX: requests 10m/12Mi, limits 2 CPU/1Gi; факт 24m/202Mi.
- iot-service: requests 10m/12Mi, limits 200m/512Mi; факт 1m/4Mi.
- ResourceQuota в наших namespace НЕТ.
Вывод: повышать ресурсы не требуется; при желании «на вырост» через deck —
EMQX 4 CPU/2Gi, iot-service 1 CPU/1Gi.
### 30.2 Новая директива пользователя
- «всё документируй» — принято, этот файл ведётся.
- «надо сочинить тесты — МНОГО РАЗНЫХ нагрузочных тестов» — создаётся набор
нагрузочных тестов (отдельный бинарь/скрипты, см. секцию 31).
- «попросим соннет сначала код ревью провести и про тесты пусть расскажет» —
Sonnet-ревью кода и рекомендации по тестам запрошены (см. 30.3).
### 30.3 Запрос Sonnet (код-ревью + план тестов)
Статус: ЗАПРОШЕН — результат в секции 31.
---
## 31. Sonnet-ревью + набор нагрузочных тестов (16:10 GMT+03)
### 31.1 Ревью Sonnet — ключевые находки (полный отчёт сохранён в чате)
20 находок. ВАЖНЕЙШИЕ (проверено по коду):
1. **CRITICAL** `internal/service/bridge/handler.go:50` — `SendMessage` в SQS
синхронный внутри MQTT-колбэка paho (сериализованный диспатч). При
недоступности SQS (таймаут ~30с) блокируется приём ВСЕХ MQTT-сообщений
→ потери. ФИКС: канал + worker-пул, дроп при переполнении. ПОДТВЕРЖДЕНО.
2. **HIGH** consumer: VisibilityTimeout=30с vs EnsureTenantDB (CREATE DATABASE
5-10с + user + grant + table) → дубли при медленной обработке. ФИКС:
VisibilityTimeout 120-180с или раздельная обработка.
3. **HIGH** consumer: нет backoff при падении PG → лавина ретраев после
восстановления. ФИКС: exponential backoff + jitter, DLQ.
4. **MEDIUM** main.go: shutdown — HTTP гаснет раньше bridge/consumer →
liveness-килл. ФИКС: сначала bridge/consumer, потом HTTP.
5. **MEDIUM** iotpg.InsertTelemetry без батчинга (1000 msg/s = 1000 INSERT).
ФИКС: буфер + batch INSERT.
Прочие: гонка getTenantDB (singleflight), лимит tenant-БД (whitelist),
defer в цикле AdminStats, MaxOpenConns=5 (devices/admin), List без пагинации,
JWT без подписи (архитектурно), валидация MQTT_BROKER_URL, топик в bridge
(пустой ns), CREATE USER через Sprintf (%I/%L), логирование пустых SQS-body,
молчаливый дроп битого JSON, subscribe-таймаут не фатален.
СТАТУС: НИЧЕГО НЕ ИСПРАВЛЕНО — ждём команду «делай» по фиксам.
### 31.2 Набор нагрузочных тестов — создан `loadtests/`
Python-пакет (paho-mqtt + stdlib), запуск: `python3 -m loadtests.run <сценарий>`.
10 сценариев: baseline, burst, large-payload, reconnect-storm, multitenant,
acl-violation, auth-neg, api-crud, telemetry-query, soak.
Механика: тест сам создаёт устройства (namespace lt_*) через API, публикует
payload {run_id, seq, sent_at, pad}, верификатор сверяет доставку через API:
delivered/lost/duplicates/latency (p50/p95/p99, ts(PG)-sent_at, NTP).
Документация и критерии успеха — loadtests/README.md; ручные SQS/PG-outage
тесты (через kubectl) описаны там же.
Грабли: POST /devices НЕ возвращает пароль (только GET по имени — учтено);
paho connect() rc=0 даже при CONNACK≠0 (on_connect обязателен); ts парсится
через datetime.fromisoformat (таймзона!); API отдаёт ≤1000 строк.
### 31.3 Смоук-прогон (прод, минимальная нагрузка)
- auth-neg: wrong_password rc=4, unknown_user rc=5 — PASS.
- baseline (2 устройства, 0.5 msg/s, 10с): sent 10, delivered 10/10,
lost 0, duplicates 0, latency p50≈208мс, первое сообщение ≈1.25с
(consumer long-poll). PASS.
### 31.4 Дальше
- Ждём «делай» по фиксам из 31.1 (минимум №1 и №2 перед большими нагрузками).
- Прогон полных сценариев — по команде.
---
## 32. Фиксы ревью внедрены (v0.1.6) + нагрузочные прогоны (17:00 GMT+03)
### 32.1 Внедрённые фиксы (код)
| # | Что | Где |
|---|---|---|
| 1 | Асинхронный SQS-диспатчер: канал 10000 + 4 worker'а, ретраи 1/2/4с, дроп при переполнении | bridge/sender.go (новый), handler.go, bridge.go |
| 2 | VisibilityTimeout 120с (config дефолт + Dockerfile + env на деплойменте) | config.go, Dockerfile |
| 3 | Backoff (1с→60с, x2) при ошибках обработки PG | consumer.go |
| 4 | Shutdown: bridge+consumer → flush → HTTP последним | main.go |
| 5 | Батч-INSERT (100 строк / 200мс flusher, final flush в Close) | iotpg |
| 6 | Disconnect(2000) вместо 250мс при остановке | bridge.go |
| 7 | (whitelist tenant-БД) — ОТЛОЖЕНО, отдельная фича | — |
| 8 | Пулы: devices 20 (env IOT_PG_MAX_CONNS), admin 10 | store/open.go, iotpg |
| 9 | JWT: опциональная HS256-проверка (env JWT_HMAC_SECRET) | middleware/auth.go |
| 10 | Close rows вместо defer в цикле | iotpg AdminStats |
| 11 | Пустое SQS-body: лог + удаление из очереди | consumer.go |
| 12 | Валидация топика (ns/deviceID непустые, "telemetry") | bridge/handler.go |
| 13 | CREATE USER: проверка роли + QuoteIdentifier/QuoteLiteral (не Sprintf) | iotpg |
| 14 | Валидация MQTT_BROKER_URL (ws:// или wss://) | config.go |
| 15 | generateMQTTPassword: 3 попытки crypto/rand | handler/devices.go |
| 16 | Баг DO-блока: параметры $n в DO недопустимы (найден прогоном) | iotpg |
| 17 | Пагинация: List limit/offset; telemetry limit+offset | devices.go, telemetry.go, iotpg |
| 18 | Admin-пул 10 | iotpg |
| 19 | Лог битого JSON — уже был | — |
| 20 | Subscribe-таймаут/ошибка → принудительный реконнект | bridge.go |
| + | Оversize >250KB → явный дроп с логом (лимит SQS 256KB) | sender.go |
| + | Гонка getTenantDB → per-ns мьютекс | iotpg |
### 32.2 Баги, всплывшие при выкатке
- DO-блок с $n-параметрами («got 2 parameters but statement requires 0») —
старые сообщения крутились с backoff; исправлено (32.1 №16).
- Зеркало платформы кэширует теги: повторный push v0.1.3 не подтянулся
(rollout restart) → правило: КАЖДОЕ изменение = НОВЫЙ тег (v0.1.4).
- SQS-лимит 256KB: 300KB-сообщения дропались SendMessage 400
(InvalidParameterValue) → явный пре-чек 250KB + документировано.
- Шлюз платформы рвёт ответы API >~15КБ (эмпирика: limit=50 OK 15KB/0.27с,
limit=200 завис на 15.6KB) → добавлен offset в API, верификатор ходит
страницами по 50; large-payload верификация только по логам consumer.
### 32.3 Результаты нагрузочных прогонов (прод, GMT+03)
| Сценарий | Итог |
|---|---|
| baseline 20 уст-в × 2 msg/s × 60с | sent 2390, delivered 2390, lost 0 (среди sent), dup 0; p50 466мс / p95 785мс / p99 1486мс |
| burst x5, 20с | sent_high 1958, delivered 1948, dup 0, recovery 112.3с; латентность пика p50 17.4с |
| large-payload 200KB | доставка по логам consumer: 6/6 сохранено, oversize-дропов 0 |
| reconnect-storm 30с-циклы, 120с | 30 форс-реконнектов, 1191/1191, dup 0 |
| multitenant 5 тенантов | first-msg: p50 1376мс / p95 3085мс (создание БД) |
| acl-violation | 11/11 чужих публикаций разорвали сессию, accepted 0 |
| auth-neg | rc=4 / rc=5 (отказы) |
| api-crud 5×10 | POST/GET/DELETE p50 ~240-255мс, 0 ошибок |
| telemetry-query 5×20 | 17.1 rps, p50 241мс, 0 ошибок |
### 32.4 Версии и артефакты
- iot-service v0.1.6 (digest 5868e9481cfd) + latest — задеплоен.
- loadtests: API-ретраи (3×), парсинг 204-пустого тела, offset-пагинация,
burst считает по фактическому sent, large-payload без API-верификации.
- Repo-память: /memories/repo/iot.md (факты платформы, правила деплоя).
- ОТЛОЖЕНО: tenant-whitelist (#7), DLQ в shared-sqs, soak 24ч.
---
## 33. Лендинг-инструкция в дизайне Nubes (v0.1.8) (19:00 GMT+03)
- Задание: страницы-инструкции «что это, зачем, как пользоваться» в дизайне
из ~/nubes/design (лого + favicon), образец — https://sqs.containerk8s.dev.nubes.ru/.
- Скопирована структура и CSS shared-sqs-лендинга (topbar/hero/badge/card/
steps/kv/hint-box/footer, цвета дизайн-системы: #2563eb, #f3f4f6, #d1d5db).
- Новое: `internal/service/api/ui/landing.html` (лендинг "/"), `ui/static/
logo.svg` + `favicon.svg` (из ~/nubes/design), `landing_embed.go`
(embed + ServeLanding + StaticHandler), маршруты "/" и "/static/" в router.
- Содержание лендинга: что это (конвейер устройство→MQTT→SQS→PG→API),
3 шага (регистрация устройства с curl-примерами; подключение MQTT с paho-
примером; чтение телеметрии), карточка «Подключение» (endpoint, протокол,
auth MQTT/REST, топик, консоль, health), секция «MQTT endpoint (exqx)»
(объяснение 400 и ограничений: 250KB, wss ~150с).
- favicon добавлен в /console и /iot-admin.
- ⚠ exqx.containerk8s.dev.nubes.ru — EMQX без возможности отдавать HTML
(нет статики в OSS, dashboard отключён, один EXPOSE 8083 занят WS) →
инструкция по MQTT-endpoint живёт на iot-лендинге, секция «MQTT endpoint».
- Грабли: embed FS требует StripPrefix("/static/"); версия в чипе
подставляется через {{VERSION}} (без лишней "v").
- v0.1.8 (digest db648725dfe6) задеплоен; проверка: "/" 200, favicon/logo
200 (image/svg+xml), chip v0.1.8.