Как мы написали Go-менеджер сервисов (kb-dev) вместо docker-compose

← All posts

В KB Labs 12 сервисов в локальной разработке: gateway, REST API, workflow daemon, marketplace, state broker, Studio и их зависимости. Месяцами мы управляли ими через 200-строчный bash-скрипт (dev.sh), который проверял порты через lsof, убивал процессы через pgrep -P и надеялся на лучшее.

Работало — пока не сломалось. Переломным моментом стала пятница, когда dev.sh stop оставил трёх зомби-процессов Node, жрущих по 75% CPU, а dev.sh start отрапортовал все сервисы как «запущены» — потому что что-то слушало нужные порты, просто не наши сервисы.

Мы заменили это на kb-dev — Go-бинарь, который управляет сервисами правильно.

Проблемы shell-based управления сервисами

Как kb-dev это исправляет

Трекинг по PID

kb-dev запускает каждый сервис через cmd.Start() и сразу записывает PID. Никакого сканирования портов. PID-файл — это богатый JSON: ID процесса, ID группы процессов, пользователь, временная метка, полная команда. Если PID-файл говорит, что сервис запущен, а процесса нет — сервис мёртв. Никакой двусмысленности.

Завершение через группу процессов

Каждый сервис стартует с Setpgid: true, создавая новую группу процессов. Остановка сервиса посылает SIGTERM всей группе одним системным вызовом: syscall.Kill(-pgid, SIGTERM). Node, esbuild, воркеры — всё дерево умирает за один вызов.

Проверки здоровья с трекингом латентности

Каждый сервис объявляет тип проверки здоровья в devservices.yaml:

services:
  rest-api:
    command: node ./plugins/rest-api/daemon/dist/index.js
    port: 5050
    health_check: http://localhost:5050/health   # HTTP-проба
    depends_on: [gateway, postgres]
 
  postgres:
    type: docker
    health_check: localhost:5432                  # TCP-проба
    container: postgres

kb-dev классифицирует проверку по формату строки: http:// → HTTP-проба, host:port → TCP-проба, всё остальное → выполнение команды. Каждая проба отслеживает латентность, поэтому kb-dev health показывает не просто жив/мёртв, а скорость ответа каждого сервиса.

Авторестарт с экспоненциальным бэкоффом

Горутина-сторож опрашивает каждые 2 секунды. При краше: рестарт с бэкоффом (1с → 2с → 4с → 8с → 16с → 30с, максимум 5 попыток). Если сервис работает стабильно 5+ минут, счётчик попыток сбрасывается. Сторож эмитит структурированные события — crashed, restarting, alive, gave_up — потребляемые и людьми, и агентами.

Кросс-процессная блокировка

flock предотвращает одновременные вызовы kb-dev start из двух терминалов. Просто, надёжно, без конфигурации.

Протокол, ориентированный на агентов

kb-dev проектировался для вызова ИИ-агентами, не только людьми. Каждый JSON-ответ следует одной схеме:

{
  "ok": true,
  "actions": [
    { "service": "rest-api", "action": "started", "elapsed": "1.2s" }
  ],
  "depsState": { "gateway": "alive", "postgres": "alive" },
  "resources": { "cpu": "12%", "memory": "145MB" },
  "hint": "...",
  "logsTail": ["..."]
}

Три агент-специфичных команды: ensure (идемпотентный запуск — живые сервисы пропускаются), ready (блокирует до полной готовности всех целей) и watch (JSONL-поток событий для мониторинга в реальном времени).

Запуск с учётом зависимостей

Сервисы объявляют зависимости. kb-dev топологически сортирует их по слоям и запускает каждый слой параллельно через горутины. Сервис не стартует, пока все его зависимости не станут здоровыми — не просто запущенными, а прошедшими свою health probe.

Почему Go?

Статический бинарь без зависимостей рантайма, настоящий параллелизм через горутины и syscall для управления группами процессов. Менеджер сервисов на Node для управления Node-сервисами создаёт циклы зависимостей, неприятные для отладки. У Go этой проблемы нет.