1206 lines
88 KiB
Markdown
1206 lines
88 KiB
Markdown
# Журнал сессии — 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 13–14.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:10–08:29:03):
|
||
|
||
| t (с) | Событие |
|
||
|---|---|
|
||
| 0.4 | CONNECTED |
|
||
| 10–120 | echo 1..12 — все возвращаются ✓ |
|
||
| 20.4–140.4 | PING сервера каждые 20с — доходят ✓ |
|
||
| 50.5–125.5 | PONG сервера на ping клиента каждые 25с — доходят ✓ |
|
||
| ~150 | ping клиента (25с-каденс) — БЕЗ pong; echo 16–17 пропали; 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с — обрыв окончательно локализуется на
|
||
внешнем краевом шлюзе (вне кластера), как и таймауты 31–33с 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: таймауты 31–33с (HTTP) и MSS 1448 наблюдались
|
||
с той же станции — вероятно, тот же проблемный маршрут/шлюз. Наши данные
|
||
добавляют к картине «~150с на долгоживущие соединения».
|
||
|
||
### 9.8 Следующие шаги (предложены, ждут «делай»)
|
||
1. Тест wss с третьей независимой точки (другой ISP/мобильная сеть) — решает
|
||
вопрос «шлюз Nubes vs ISP станции».
|
||
2. HTTP-цикл с той же станции на /health (запрос каждые 10с, 5 мин) — связать
|
||
с таймаутами 31–33с 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 20–25с). Доказано:
|
||
станция 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: таймауты 31–33с (~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с, таймауты
|
||
31–33с, 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 14–19% CPU /
|
||
24–40% RAM. Свободных ресурсов много.
|
||
Аллокация requests/limits: workers ~3.5–4.0 CPU requests, limits 11.5–14.3 CPU
|
||
(оверкоммит), память 16–31% / 45–77%.
|
||
Наши контейнеры:
|
||
- 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.
|