Files
sless/doc/thinking/2026-04-06.md
T

13 KiB
Raw Blame History

Thinking Log — 2026-04-06

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


Архитектурные обсуждения перед началом Kafka

Контекст

Пользователь обсуждал будущую prod-архитектуру IoT сервиса. Никакого кода не менялось — чистое планирование.

Итоги обсуждений

Три отдельных кластера (принято):

  1. IoT кластер — EMQX, bridge, Kafka, iot-consumer, Postgres, REST API
  2. Serverless кластер — operator, builder, event-dispatcher, Functions
  3. Infra/Control кластер — Terraform для provisioning кластеров 1 и 2, DNS, TLS, auth, billing

Это классическая схема "control plane отдельно от data plane".

Kafka — выбор подтверждён:

  • Сейчас: bridge → Postgres напрямую (синхронно, без буфера)
  • Prod: bridge → Kafka → {consumer → Postgres, event-dispatcher → Functions}
  • Dev/test: Kafka через Helm (bitnami, KRaft mode, 1 нод, PVC)
  • Prod: managed Kafka (Confluent/Aiven) — только меняется KAFKA_BROKERS в Secret

Postgres → managed облачный: легко

  • bridge и API используют DATABASE_URL из env
  • Для переключения: только заменить Secret в кластере
  • Код не трогается

Состояние RabbitMQ для IoT (важное открытие):

  • Bridge сейчас пишет в RabbitMQ очередь iot.{namespace}.telemetry
  • НО event-dispatcher эту очередь не читает — он настроен на serverless functions triggers
  • То есть IoT-сообщения в RabbitMQ лежат мёртвым грузом — никто не читает
  • Kafka заменяет RabbitMQ для IoT-части полностью

Что проверяли в кластере:

  • 2026-04-05: только один активный тенант sless-16367aacb67a4a01 (созданный после инцидента)
  • Устройство device2, одно сообщение: {"msg":"hello1dddd1777"} от 14:34 UTC
  • 2026-04-06: kubeconfig истёк → обновил → тот же один тенант, никто новый не входил

План интеграции Kafka

Анализ текущего bridge

Читал iot/cmd/mqtt-bridge/main.go. Текущая логика в buildMQTTMessageHandler:

  1. Получает MQTT сообщение
  2. Публикует в RabbitMQ (бесполезно — никто не читает)
  3. Пишет напрямую в Postgres через iotpg.Store

С Kafka нужно:

  1. Получает MQTT сообщение
  2. Публикует в Kafka топик iot.telemetry (единый топик, namespace в payload)
  3. Убрать прямой INSERT в Postgres из bridge

Что создаётся заново

iot/cmd/kafka-consumer/main.go — новый сервис:

  • Читает из Kafka топика iot.telemetry
  • Пишет в Postgres (та же логика что сейчас в bridge)
  • Consumer group: iot-pg-consumer

Изменения в bridge:

  • Убрать RabbitMQ
  • Добавить Kafka producer (библиотека github.com/segmentio/kafka-go)
  • Env var: KAFKA_BROKERS вместо RABBITMQ_URL

Новые env vars:

  • bridge: KAFKA_BROKERS=kafka.sless.svc.cluster.local:9092
  • consumer: KAFKA_BROKERS=..., IOT_PG_DSN=...

Что НЕ меняется

  • EMQX, operator, REST API, IoT Console — не трогаются
  • iotpg storage package — используется consumer-ом напрямую
  • ACL, auth, namespace-изоляция — не меняются

Порядок работы

  1. Документация + коммит (сейчас)
  2. Ветка iot-kafka
  3. Helm: установить Kafka в namespace sless
  4. Переписать bridge: убрать RabbitMQ, добавить Kafka producer
  5. Создать iot/cmd/kafka-consumer/main.go
  6. Обновить Dockerfile (добавить сборку consumer)
  7. Обновить deployment манифесты
  8. Сборка v0.1.67, деплой, тест

Риски

  • kafka-go vs confluent-kafka-go — выбираем segmentio/kafka-go (pure Go, без CGO, совместим с alpine)
  • KRaft mode в Helm bitnami — убедиться что включён (без Zookeeper)
  • Topic iot.telemetry — создаётся автоматически при первой публикации (auto.create.topics.enable=true по умолчанию)

Сессия (продолжение) — реализация Kafka pipeline

Что было сделано

Ветка: iot-kafka

1. Kafka StatefulSet (deployments/k8s/kafka.yaml)

Установка через Helm bitnami провалилась — образ bitnami/kafka:4.0.0 заблокирован (paywall с Aug 2025). Переключились на официальный apache/kafka:3.7.0 — бесплатный, полнофункциональный.

Написан кастомный kafka.yaml:

  • KRaft mode (без Zookeeper) — node.id=1, roles=broker+controller
  • ConfigMap монтируется в /tmp/kafka-config (не /etc/kafka — read-only в образе)
  • securityContext.fsGroup=1000 — kafka user (UID 1000) может писать в PVC
  • PVC 1Gi на vcd-disk-ext4 (local-path отказал: not enough disk space)
  • Два Service: kafka:9092 и headless kafka-headless

2. bridge переписан (iot/cmd/mqtt-bridge/main.go)

  • Убран RabbitMQ (amqp091-go)
  • Убрана прямая запись в Postgres через iotpg
  • Добавлен Kafka writer (segmentio/kafka-go)
  • Топик: iot.telemetry, ключ = namespace (партиционирование по тенанту)
  • Async: false, RequiredAcks: RequireOne — синхронная запись, подтверждение от лидера

3. kafka-consumer создан (iot/cmd/kafka-consumer/main.go)

  • Consumer group: iot-pg-consumer
  • Читает из iot.telemetry, пишет в Postgres через iotpg.Store
  • Offset коммитится ТОЛЬКО после успешной записи (at-least-once)
  • Retry loop при недоступности Kafka

4. Dockerfile обновлён

  • Добавлена сборка iot-kafka-consumer бинаря
  • COPY --from=builder /workspace/iot-kafka-consumer .
  • Итого в образе 3 бинаря: manager, iot-mqtt-bridge, iot-kafka-consumer

5. Манифесты обновлены

  • iot-mqtt-bridge.yaml: убран RABBITMQ_URL, добавлен KAFKA_BROKERS
  • iot-kafka-consumer.yaml: новый deployment

Баги которые встретили и решили

Bug 1: дублирующий package main

create_file вставил package main дважды — в начале и перед import. Фикс: replace_string_in_file удалил дубликат.

Bug 2: kafka-go помечен как // indirect в go.mod

gopls не видел пакет как доступный. Причина: зависимость добавлена без прямого импорта в момент добавления. Фикс: go mod tidy убрал // indirect.

Bug 3: Race condition — consumer зависал при холодном старте

Когда: consumer стартовал одновременно с Kafka (первый деплой, топика нет). Что происходило: consumer JOIN-ил group → Kafka auto-создавала топик в момент JOIN → kafka-go зависал на FetchMessage навсегда. Гипотеза №1: postStart lifecycle hook на Kafka — создать топик сразу после старта брокера. Проблема с гипотезой: kafka-topics.sh --list без таймаута зависает бесконечно → pod застрял в PodInitializing. Попытка с nc — nc не установлен в образе. Попытка с request.timeout.ms через properties — postStart возвращал exit code 1 → Kubernetes убивал контейнер → CrashLoopBackOff. Итоговое решение: ensureKafkaTopic() в consumer — создаёт топик через kafka.DialContext + conn.CreateTopics() ДО создания Reader и JOIN группы. Retry 30 раз × 3 сек = 90 сек макс ожидания.

// Порядок в consumer:
// 1. Connect IoT Postgres
// 2. ensureKafkaTopic() ← создаём топик, ждём брокер
// 3. kafka.NewReader()  ← только теперь join group
// 4. FetchMessage() loop

Почему это решение правильное: race исключён на уровне приложения, не инфраструктуры. Даже если kafka.yaml не имеет никакого init — consumer сам дождётся Kafka и создаст топик.

Bug 4: CrashLoopBackOff после force delete pod-а

Force delete оставил .lock файл на PVC. Kafka падала с: Failed to acquire lock on file .lock in /var/kafka-data/logs Фикс: удалить StatefulSet + PVC (kubectl delete statefulset kafka && kubectl delete pvc kafka-data-kafka-0), пересоздать.

Урок: НИКОГДА не делать kubectl delete pod --force для stateful pod-ов. Только graceful (kubectl delete pod, подождать). Force delete = гарантированная поломка PVC.


Результаты тестирования (v0.1.68)

Тест Условие Результат
Cold start consumer стартует раньше Kafka ✅ ensureKafkaTopic ретраится, дожидается
5 рестартов consumer Kafka работает ✅ каждый раз kafka topic ready
MQTT → Pipeline device2, 1 сообщение ✅ offset=0 в Postgres
Рестарт Kafka consumer живёт ✅ ретраится с ERROR fetch, восстанавливается
10 сообщений параллельно 10 pod-ов mosquitto ✅ offsets 2-11 все в Postgres

Что НЕ тестировалось:

  • Полный холодный старт с нуля (kubectl apply -f на чистый кластер)
  • Consumer стартует одновременно с Kafka (оба новые) — race condition исправлен кодом, но на новом кластере не проверялся

Текущее состояние кластера (2026-04-06 ~17:30 МСК)

sless-operator:v0.1.68 — Running
kafka-0              — Running (после удаления PVC и пересоздания)
iot-mqtt-bridge      — Running, подключён к EMQX и Kafka
iot-kafka-consumer   — Running, waiting for messages
iot-postgres         — Running

Тенант: sless-16367aacb67a4a01, устройство device2. В IoT Postgres: 12+ записей телеметрии (offsets 0-11).


Что нужно сделать ещё

  1. Тест: полный холодный старт — удалить kafka + consumer + PVC, применить всё одновременно, убедиться что race не вылезает
  2. Helm chart — параметризовать KAFKA_BROKERS, IOT_PG_DSN, тег образа, StorageClass для values-dev.yaml / values-prod.yaml
  3. Managed Kafka/Postgres — при переходе только менять values-prod.yaml
  4. Merge iot-kafka в main — после тестов

Архитектурные выводы сессии

Будущая prod-архитектура (принято):

  • 3 кластера: IoT / Serverless / Infra-Control
  • Managed Kafka + Managed Postgres (переключение через env vars, код не меняется)
  • Helm chart для параметризации per-environment

Текущий статус пути данных:

IoT Device
  → MQTT PUBLISH
  → EMQX (sless namespace)
  → iot-mqtt-bridge (подписан на +/telemetry/+)
  → Kafka топик iot.telemetry (key=namespace)
  → iot-kafka-consumer (group iot-pg-consumer)
  → IoT Postgres (per-tenant schema через EnsureTenantDB)
  → GET /v1/{ns}/iot/telemetry (IoT Console)