233 lines
13 KiB
Markdown
233 lines
13 KiB
Markdown
# 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 сек макс ожидания.
|
||
|
||
```go
|
||
// Порядок в 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)
|
||
```
|