94 KiB
Журнал сессии — 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. - Проблемы ПЛАТФОРМЫ (не сервиса):
- Таймауты каждые 31–33с на внешнем пути (~5.5% запросов без ответа 30с). Доказано tcpdump; внутри кластера потерь нет (port-forward 24977 раундов, 0 сбоев).
- 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], AccessKeySSAK-+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, namespacesless, API185.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,/healthOK. - Технические факты платформы (из 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с (не режется).
- Контейнер = k8s deployment
- 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 нужен путь до образа).
Что сделано (все шаги):
- Создан каталог
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 (меняется envPORT), версия через 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.
.gitignore— добавленws-probe/ws-probe(сборочный бинарник).- Локальные проверки:
go vetOK; smoke:/health→{"status":"ok","version":"v0.1.0"}; WS-апгрейд →HTTP/1.1 101 Switching Protocols(Sec-WebSocket-Accept корректный). - Docker: логин в Docker Hub под
naeelактивен (проверенdocker info). - Образ собран и запушен (публичный):
naeel/iot-ws-probe:v0.1.0(и:latest),- digest
sha256:dc029be3e79fb37f15ce23477096e340ab181b96c0132dc2e443f341e36f0688, - размер 10.9MB.
- Коммиты:
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).
Результаты проверок (первые):
GET /health→{"status":"ok","version":"v0.1.0"}— под стянул наш образ.GET /→ HTML-страница ws-probe (200).- WebSocket upgrade через платформенный ingress →
HTTP/1.1 101 Switching Protocols,Sec-WebSocket-Acceptкорректный, HSTS на месте. (В выводе curl первая строкаHTTP/1.1 200 Connection established— локальный CONNECT-прокси машины, ответ 101 пришёл от платформы.) 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).
Следующие диагностические шаги (предложены, ждут «делай»):
- Повторить тест 3 раза — воспроизводимость и точное время обрыва.
- Логи пода пробника на платформе (
kubectl logsна ВМ) — что видит сервер в момент обрыва (read error / ping error). - Тест WS изнутри кластера на внутренний
containerk8s.<uuid>.svc.cluster.local:8080— если держится >10 мин: обрыв на внешнем шлюзе/ingress; если тоже рвётся: проблема ближе к поду. - Держать обычное HTTP-соединение через ingress >150с — сравнение с WS.
- По результатам — тикет в Nubes (лимит соединений на ingress/шлюзе).
9. Диагностика обрыва (16.08, команда «делай тесты»)
9.1 Инфраструктура пробника на платформе (через ВМ, kubeconfig живой)
- Namespace пробника:
2d9b96ff-0a50-41e5-a5d5-1cd9e5621555, deploymentcontainerk8s, подcontainerk8s-5dc7dcbfcf-d4ch6(IP 172.16.2.227), servicecontainerk8sClusterIP 10.106.45.135:8080. - Рядом стандартные: pod vmagent (метрики), ns shared-sqs
f1ffb134-...(2d9h), старый чужой nsdf36c8af-...(30d) — не трогали. - Ingress платформы: ns
ingress,shturval-ingress-controller-controller(2 пода: 172.16.0.73 / 172.16.3.149), LB 185.247.187.151 (kube-vip). DNStestiot.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с (как прямой) |
Выводы:
- Под/сервис/nginx платформы — полностью здоровы (VM-ext 300с чисто, VM-int 600с чисто; nginx request_time = полное время жизни, ошибок нет).
- Обрыв воспроизводится ТОЛЬКО с рабочей станции, причём и напрямую, и через локальный прокси (172.17.192.1:10808) — 5/5 обрывов, точка сбоя ~150с.
- Публичный WS-echo (echo.websocket.org) с той же станции — ЧИСТО. Значит сеть станции в целом здорова; страдает именно маршрут станция → Nubes (185.247.187.151).
- ВМ (другой маршрут до платформы, p50 6–8мс по данным shared-sqs) — чисто.
- ⚠️ Нюанс для вывода «шлюз Nubes»: станция и ВМ ходят разными маршрутами. Если ВМ обходит внешний шлюз Nubes (короткий путь/пиринг) — то «таймер ~150с на внешнем шлюзе Nubes» подтверждается. Если ВМ проходит тот же шлюз — тогда проблема в маршруте станции (ISP/прокси-сеть). Точно различить может только тест с третьей независимой точки входа (другой ISP, мобильная сеть).
- Взаимосвязь с shared-sqs: таймауты 31–33с (HTTP) и MSS 1448 наблюдались с той же станции — вероятно, тот же проблемный маршрут/шлюз. Наши данные добавляют к картине «~150с на долгоживущие соединения».
9.8 Следующие шаги (предложены, ждут «делай»)
- Тест wss с третьей независимой точки (другой ISP/мобильная сеть) — решает вопрос «шлюз Nubes vs ISP станции».
- HTTP-цикл с той же станции на /health (запрос каждые 10с, 5 мин) — связать с таймаутами 31–33с shared-sqs.
- Тикет в Nubes: wss-соединения с интернета рвутся на ~150с (детерминированно),
- таймауты 31–33с и MSS 1448 (данные shared-sqs).
- При проектировании 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 (важно):
- PG НЕ меняется. Managed PostgreSQL уже используется текущим consumer'ом (IOT_PG_DSN → postgresqlk8s…svc.cluster.local). Утверждение Sonnet «новые данные пишутся в новую PG» — ОШИБКА. Телеметрию переносить не нужно вовсе; переносятся только CRD-объекты (устройства+пароли).
- Платформенные контейнеры живут в ТОМ ЖЕ кластере, что и старый деплой
IoT: ноды
iot-naeel-*, ingressshturval— один кластер. Значит старый входiot.kube5s.ru(185.247.187.147), скорее всего, идёт через ТОТ ЖЕ краевой шлюз — вариант Sonnet «EMQX на старом кластере как точка входа» проблему не обходит (и его сомнение в обратную сторону неверно). Проверяемо тестом: держать wss кwss://iot.kube5s.ru/mqtt5+ мин — если stall ~150с есть и там, текущие устройства уже страдают. - Multi-port у Sonnet заявлен как факт в п.1 («1883 внутренний для bridge»), но в п.9.6 сам же помечен НЕИЗВЕСТНО — противоречие. Вероятный реальный сценарий: один порт на контейнер → EMQX наружу только 8083 (wss), bridge к EMQX тоже по wss внутри платформы.
- Конфиг EMQX через env — не блокер. EMQX 5 поддерживает env vars (EMQX_*); плюс можно собрать свой образ на базе emqx/emqx с вшитым emqx.conf (публичный Docker Hub) — снимает ограничение полностью.
- /metrics уже есть в текущем операторе (порт 8080, controller-runtime) — переносится в монолит, а не «отсутствует» (ошибка Sonnet).
- bcrypt для паролей — ок для НОВЫХ устройств, но для переноса старых
нужен скрипт: прочитать все Secrets
iot-{deviceId}в ns sless и вставить в таблицу с сохранением работоспособности (хэшировать при вставке). - Дубли при 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 для миграции значения не имеет.
Проверка по вопросу «старый прод жив?» (факты):
- ns
slessжив (125d). Все 4 deployment'а Running:emqx,iot-mqtt-bridge,iot-operator,iot-sqs-consumer(поды 117–125 дней без рестартов). - Внешний вход мёртв:
iot.kube5s.ruне резолвится ни с ВМ (getent пусто), ни со станции (getent exit 2); curl → 000/exit 35. DNS-запись снята — устройства через него не ходят. - Конвейер телеметрии мёртв с 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; стирать пока не надо; ЧЁТКО пропиши везде, что это ЛЕГАСИ и НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ».
Сделано:
- В шапку каждого файла старого 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).
- 15 документов в
- Создан корневой
LEGACY.md— реестр: что легаси, что актуально. - Раздел 11 этого журнала помечен «НЕ ПРИНИМАТЬ ВО ВНИМАНИЕ».
- Удалений НЕ производилось.
Актуальные файлы (НЕ легаси): HISTORY/, doc/sqs-integration.md,
ws-probe/, Go-код cmd/mqtt-bridge/, cmd/sqs-consumer/, internal/
(переиспользуется в монолите).
13. Аудит вмешательства старого IoT + план Фазы 1 (16.08)
Команда пользователя: «НЕ ДОПУСКАТЬ, чтобы старые данные и файлы от прежнего IoT вмешивались в проект. ПРОВЕРЬ.»
Результаты аудита (факты):
- Код: 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/.
- go.mod: тяжёлые k8s-депы (k8s.io/api, apimachinery, client-go v0.26.0,
controller-runtime v0.14.1 + ~40 indirect) — держатся только из-за этих
трёх файлов. После переписывания хендлеров —
go mod tidyих уберёт. - Dockerfile (корень) — собирал 3 старых бинарника; НЕ был помечен. → помечен ЛЕГАСИ (новый монолит получит собственный Dockerfile).
- CI: в
.github/только правила (copilot-instructions, pravila) — старых пайплайнов нет.bin/в git не отслеживается. - Данные: старый 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 (согласован в обсуждении, ждёт «делай»):
- Новый
cmd/iot-service/main.go+ структура по пакетам (config, api, bridge, consumer, storage/iotpg) — много мелких файлов, без god-файлов. - Хендлеры устройств/админки — на PG вместо K8s; authTestMode=false.
- Bridge+consumer — переиспользуются как есть (MQTT-подписка, SQS long-poll).
- Новый Dockerfile (один бинарник), образ
naeel/iot-service(имя — на подтверждении пользователя), env-дефолты вшиты, секреты — в deck. - Сборка → пуш 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.oldMakefile→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).
Ключевые решения:
- Устройства — таблица iot_devices в PG (PRIMARY KEY namespace+name, UNIQUE namespace+device_id); пароль генерится crypto/rand 32B hex при создании; MQTT-auth — SQL + constant-time compare; last_connected обновляется.
- k8s-зависимостей в новом коде НЕТ: старые хендлеры не тронуты, старый оператор компилируется. k8s-депы в go.mod остаются до удаления легаси.
- Admin-stats: PG (iotpg.GetAdminStats) + SQS (атрибуты очереди) + runtime (version/uptime) — вместо статусов k8s-подов.
- 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 для статистики).
- 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 после создания не меняются):
- EMQX-контейнер: образ
naeel/iot-emqx:latest, порт 8083. Узнать UUID namespace EMQX (kubectl get ns / из deck). - 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.
- Патч 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 - ДО создания iot-service: креды тенанта IoT в shared-sqs (консоль /ui →
Credentials) + очередь
iot-telemetry.
17. ПРОВЕРКА PostgreSQL (16.08, команда «проверь ПГ») — старый PG МЁРТВ
⛔ ДИСЦИПЛИНА (напоминание пользователя): всё от старого IoT НЕ УЧИТЫВАТЬ — ни старые DSN, ни старые креды, ни старые БД. Ниже — только фиксация факта проверки. Для нового проекта используются ТОЛЬКО новые ресурсы.
Поправка к прошлым утверждениям: «PG уже работает, тот же, что у старого consumer» — НЕВЕРНО. Проверено фактически:
- Namespace
dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5(из старого DSN) — НЕ СУЩЕСТВУЕТ (NotFound). Старый DSNpostgresqlk8s-master.dc5db45d-…не резолвится — мёртв. - Живой managed PG есть — другой инстанс:
- ns
509145c3-ba71-4687-a084-d9dcbc88d3d2(125d), - под
postgresqlk8s-0(3/3 Running, 19d), Zalando postgres-operator, - svc
postgresqlk8s-masterClusterIP 10.104.214.32:5432, - секреты кредов: postgres / standby / superuser / testuser (Zalando).
- ns
- Проверка подключения (port-forward 15432 → master):
pg_isready→ accepting connections;- старые креды IoT (
super/<старый пароль>) → FATAL password authentication failed; - суперюзер инстанса (
superuserиз секрета) → OK; - БД инстанса: autotest, postgres, template0, template1, testdb.
sqsdbОТСУТСТВУЕТ (старых баз IoT нет).
- Вывод: для монолита нужен НОВЫЙ DSN на инстанс 509145c3-…:
postgresql://superuser:<пароль>@postgresqlk8s-master.509145c3-….svc.cluster.local:5432/sqsdb- предварительно создать БД
sqsdb(owner superuser). iotpg сам создаст tenant_credentials и per-tenant БД.
- предварительно создать БД
- Осталось на ВМ: 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 отношения не имеют):
- Роль
iot_service(LOGIN, сгенерированный пароль). - БД
iotdb(OWNER iot_service) — через GRANT→CREATE→REVOKE. - Проверка подключения под
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 (порядок):
- EMQX: образ
naeel/iot-emqx:latest→ узнать UUID namespace. - iot-service: образ
naeel/iot-service:latest+ env (см. ниже). - 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, digestsha256:7de0ec8a…01dd08f. Проверка:{"8083/tcp":{}}. - Урок: для платформы «Простой HTTP контейнер» образ обязан иметь РОВНО один EXPOSE-порт; наследуемые порты базового образа — блокер.
Ошибка 2: 502 Bad Gateway (домен https://exqx.containerk8s.dev.nubes.ru/)
- Диагностика: deployment
containerk8sв ns2fdd9658-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, digestsha256: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) — развязка
Диагностика по ответу Соннета + эксперименты (хронология):
- Гипотеза Соннета «DNS/ndots:5 зависание на резолве заглушки» — ОПРОВЕРГНУТА
замером в поде:
getent hosts iot-service= 103мс (быстрый NXDOMAIN). - deb-сборки (v0.1.4–0.1.6) на платформе «висели»: PID1=beam, CPU ~5с, логов нет. Эксперимент: официальный emqx/emqx:5.5.1 в тот же под — заводится, логи в stdout есть → проблема не платформа, а наша deb-сборка.
- Пересборка от официальной базы:
FROM emqx/emqx:5.5.1 + COPY emqx.conf(v0.2.0). Заглушки auth/ACL URL заменены на РЕЗОЛВИМЫЙ адрес shared-sqs (containerk8s.f1ffb134-…:4100), чтобы пулы HTTP-auth не спотыкались на nxdomain при старте. - Настоящая причина «зависаний»: с 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).
Следующие шаги:
- Создать контейнер iot-service в deck-UI (env: MQTT_HOST= containerk8s.2fdd9658-4995-494f-892a-d88dda7da1ee.svc.cluster.local + секреты).
- kubectl set env в EMQX: EMQX_AUTHENTICATION__1__URL / EMQX_AUTHORIZATION__SOURCES__1__URL → http://containerk8s.:9090/…
- End-to-end: устройство (mqtt over wss) → EMQX auth → bridge → SQS → consumer → PG → REST API.
- Позже: убрать dashboard из emqx.conf (v0.2.1), CPU-квоту ≥1000m для EMQX в deck при создании (в нашем кластере можно kubectl patch).
23. Монолит iot-service поднят на платформе (16.08, ~10:50 MSK)
Ошибки по пути (задокументированы):
- Первая попытка deck: под падал
required env vars not set: IOT_PG_DSN, …— env-переменные из deck не применились. После пересоздания пользователем env применились (config.Load прошёл). - Затем:
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) ✅
Полная цепочка проверена на платформе:
- Устройство создано через API: POST /v1/namespaces/test/iot/devices
(dev1, device_id=dev-001) → mqtt_username
test_dev-001, пароль получен. - MQTT-клиент (paho, wss://exqx.containerk8s.dev.nubes.ru:443/mqtt,
transport=websockets) — CONNECT принят (rc=0), publish в
test/telemetry/dev-001(rc=0, QoS1). - EMQX HTTP-auth → монолит: bridge
iot-bridgeподключён (лог EMQX: clientid=iot-bridge, WS-MQTT, PINGRESP; монолит:bridge: MQTT connected). - Bridge → SQS:
forwarded telemetry to SQS(внутренний endpoint). - Consumer → PG:
telemetry saved namespace=test device=dev-001; GET /v1/namespaces/test/iot/telemetry → count=1, payload {"e2e":true,"temp":23.5}.
Ошибки по пути (исправлены, задокументированы):
permission denied to create role (42501)— роль iot_service не имела прав для per-tenant БД. Исправлено:ALTER ROLE iot_service CREATEROLE CREATEDB. ⚠️ Для чужого кластера: при подготовке БД давать роли эти права.- 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, instanceUid509145c3-ba71-4687-a084-d9dcbc88d3d2, internalMasterpostgresqlk8s-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, внутренний endpointcontainerk8s.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 (диагностика)
Правила платформы (выстраданы)
- Путь к образу в deck фиксируется при создании → только latest + redeploy; зеркало кэширует latest → в НАШЕМ кластере kubectl set image уникальным тегом.
- Env в deck фиксируются при создании (формат JSON в форме — выяснить!).
- Порт приложения берётся из EXPOSE образа; поле порта в форме отсутствует → образ обязан иметь РОВНО один EXPOSE.
- Загрузка EMQX на CPU-квоте 500m — 5–10 мин (не зависание!); dashboard падает с таймаутами; для deck задавать CPU ≥1000m.
- PG требует SSL: pg_hba no encryption → sslmode=require.
Хвосты (текущий план работ)
- AUTH_TEST_MODE=false.
- EMQX v0.2.1: убрать dashboard из конфига; CPU-квота (наш кластер — patch, deck — рекомендация).
- PG-auth в EMQX (без kubectl для чужого кластера): authentication postgres (devices + bridge) + authorization postgres + http-фолбэк; монолит создаёт bridge-строку в iot_devices (EnsureBridgeDevice).
- Процедура деплоя для чужого кластера (документ).
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=, 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), НО:
- Тип в конфиге —
postgresql, НЕpostgres(backend = postgresql,type = postgresql).postgres→ валидация падает: authentication.1 unsupported_mechanism / authorization unknown_authz_type. Проверено: v0.2.2 (crash-loop на платформе). ${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-тест).- Оставшиеся от HTTP-auth env
EMQX_AUTHENTICATION__1__URL/EMQX_AUTHORIZATION__SOURCES__1__URL→ unknown_fields "url" → CrashLoop. Удалены kubectl set env VAR-. - В authz-postgresql НЕ поддерживаются плейсхолдеры ${action}/${topic} (${action} подставлялся литералом "{action}", epgsql падал lists:zip function_clause). action/topic возвращаются КОЛОНКАМИ результата: permission + action + topic. Устройствам — ДВЕ строки (publish и subscribe) через (VALUES ('publish'),('subscribe')).
- password_hash_algorithm = {name=plain, salt_position=disable} — пароли в iot_devices хранятся открытым hex, сравнение прямое.
- SSL обязателен: ssl { enable=true, verify=verify_none } (pg_hba платформы).
- 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:- PG: роль iot_service (LOGIN, CREATEROLE, CREATEDB), БД iotdb owner-ом, SSL обязателен (sslmode=require / EMQX ssl.enable).
- EMQX ПЕРВЫМ (naeel/iot-emqx:v0.2.4): env только PG (EMQX_AUTHENTICATION__1__/EMQX_AUTHORIZATION__SOURCES__1__), CPU ≥1000m; предупреждение про запрещённые URL-env и ${VAR}.
- iot-service ВТОРЫМ (naeel/iot-service:v0.1.2): MQTT_HOST= containerk8s..svc.cluster.local (UUID из консоли deck), DSN sslmode=require, SQS-креды, MQTT_USERNAME/PASSWORD.
- Устройства: регистрация через API (JWT структурный), username _<device_id>, wss на домен EMQX, топик /telemetry/<device_id>; негативные auth-тесты (CONNACK rc=4/5) — чек-лист.
- Обновления образов без kubectl — только пересоздание контейнера (путь к образу фиксируется при создании, latest кэшируется зеркалом).
- Диагностика и известные ограничения платформы (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 находок. ВАЖНЕЙШИЕ (проверено по коду):
- CRITICAL
internal/service/bridge/handler.go:50—SendMessageв SQS синхронный внутри MQTT-колбэка paho (сериализованный диспатч). При недоступности SQS (таймаут ~30с) блокируется приём ВСЕХ MQTT-сообщений → потери. ФИКС: канал + worker-пул, дроп при переполнении. ПОДТВЕРЖДЕНО. - HIGH consumer: VisibilityTimeout=30с vs EnsureTenantDB (CREATE DATABASE 5-10с + user + grant + table) → дубли при медленной обработке. ФИКС: VisibilityTimeout 120-180с или раздельная обработка.
- HIGH consumer: нет backoff при падении PG → лавина ретраев после восстановления. ФИКС: exponential backoff + jitter, DLQ.
- MEDIUM main.go: shutdown — HTTP гаснет раньше bridge/consumer → liveness-килл. ФИКС: сначала bridge/consumer, потом HTTP.
- 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.
34. Лендинг переписан — «понятно, без воды» (v0.1.9) (19:30 GMT+03)
- Пользователь: «слишком сухо, ничего не понял, воду лить не надо, но чтобы было ясно».
- Переработано содержимое landing.html (дизайн/структура сохранены):
- hero: «Это база телеметрии. Устройства шлют показания по MQTT...»;
- «Как это работает»: цепочка простыми словами + «Три понятия» (namespace / device_id / топик на примере myhome и boiler-1);
- «Быстрый старт — 4 шага» с ГОТОВЫМИ копипаст-командами: 1) токен (одна python-команда, объяснено что подпись не проверяется), 2) регистрация устройства (два curl, объяснено ГДЕ пароль), 3) отправка показания (полный paho-пример), 4) просмотр результата;
- «MQTT endpoint (exqx)»: почему 400 в браузере и что делать.
- v0.1.9 (digest fb488a8666ab) задеплоен, страница проверена в браузере (структура отрисована корректно).
35. SVG-схема пайплайна на лендинге (v0.1.12) (20:10 GMT+03)
- Две попытки Gemini дали негодные схемы (первая — всё сжато в одну строку с обрезками; вторая — «бридж»/«потребитель» стали отдельными узлами, дублирование, оторванные подписи). Решение: схема нарисована ВРУЧНУЮ.
internal/service/api/ui/static/pipeline.svg— 1440x300, 5 блоков (Устройство | EMQX | Очередь | PostgreSQL | API/Консоль), подложка «ПЛАТФОРМА» пунктиром вокруг трёх внутренних блоков, 4 стрелки с ярлыками (MQTT over wss / бридж / потребитель / REST) и подписями переходов, подвал «бридж и потребитель — части сервиса iot-service». Палитра дизайн-системы (#2563eb/#eff6ff/#bfdbfe/#d1d5db/#6b7280).- landing.html: схема вставлена через
(по правилу дизайн-системы — файлом, НЕ инлайн), CSS-флоу удалён.
- v0.1.12 (digest b6a44303f9f5) задеплоен; проверка: landing 200, pipeline.svg 200 (image/svg+xml, 4591 байт).
36. Схема перерисована ВЕРТИКАЛЬНО, крупно (v0.1.13) (20:25 GMT+03)
- Пользователь: горизонтальная схема «слишком мелко», требование «не в один
ряд» повторено. Схема перерисована вертикально:
- viewBox 640x1060, блоки 420x110, заголовки 20px, подписи 14px;
- поток сверху вниз: Устройство → (MQTT over wss, топик) → EMQX → (бридж) → Очередь → (потребитель) → PostgreSQL → (REST) → API / Консоль;
- ярлык перехода слева от стрелки, пояснение справа;
- подложка «ПЛАТФОРМА» пунктиром вокруг EMQX/Очередь/PostgreSQL.
- На странице ширина схемы ограничена 640px (вся ширина карточки).
- v0.1.13 (digest 0450fd91b8f5) задеплоен; отдаваемый SVG идентичен локальному (4595 байт, viewBox 640x1060). При просмотре в браузере — жёсткое обновление (Ctrl+F5), т.к. браузер может держать старый SVG в кэше.
37. Отчёт для DevOps Nubes (20:50 GMT+03)
- Создан
doc/thinking/nubes-devops-report-2026-08-16.md:- Часть 1 — что не работает: wss ~150с (доказательства 5/5 станций, Vultr, 600с внутри), MTU/51с (три контура: drhider MTU 1450+Geneve 50 vs pod 1500 → дроп → ретрансмиссия 51с; SQS >1.4KB; IoT >15KB, MSS 1448/MTU 1400), таймауты малых тел ~5.5% (512B, 31-33с, внутри 24977 раундов 0 сбоев).
- Часть 2 — как исправить: MSS-clamping (TCPMSS clamp-to-pmtu), MTU подов 1400, проверка ICMP fragmentation needed, idle-timeout edge-шлюза ≥600с + ws ping/pong, критерии приёмки.
- Отдельно: что мы уже обошли сами и что нужно от DevOps.
38. Диагностика с DevOps Nubes (17.08, утренняя)
- Представитель DevOps Nubes: «если находите косяки — рассказывайте глазами пользователя», готов тыкать свой инструмент в инстанс, чтобы не «придумывать, как в ингрес передавать».
- Ключевые слова собеседника: у них «с ингресом пара нюансов, которые разруливаем»; большие файлы = 500 МБ; граница проблем у нас ~64 КБ (плавает, зависит от ингресса) — уже правили MTU в подах, «отчасти проблемы ушли».
- Наш периметр: NSX Edge + AVI ALB (4 VS), SNAT, routed-сеть; кластер Штурвал 2.13.1, k8s 1.34.x, Cilium. Ресурсы vDC не узкое место (cpuGuaranteed 20 / memGuaranteed 80 ГБ; узлы 14–33% CPU).
- drhider: 20 МБ прошло без сбоя (баг нестабилен); IoT: вчера limit=200 виснул на 15 672 байт, сегодня limit=75/200 без прокси — 22 751 байт за 0.11с (прошло). Вывод: баг ПЛАВАЕТ по маршруту/времени, стабильного воспроизведения на данный момент нет.
- Для диагностики включён AUTH_TEST_MODE=true (любой Bearer-токен, короткий «123») на деплойменте iot-service. ⚠ ВЕРНУТЬ false после диагностики.
- Дожидаемся, какой инстанс отдать на диагностику (drhider vs IoT) и файл какого размера.