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

233 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```