В 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 управления сервисами
- Ложная живость.
lsof -i :5050говорит, что что-то на этом порту. Не говорит, что это ваш REST API. - Неполное завершение.
pgrep -P $pidнаходит прямых детей, но не внуков — Node, порождающий esbuild, порождающий воркер. Дерево процессов переживает вашу команду stop. - Нет проверок здоровья. «Процесс запущен» и «сервис здоров» — разные утверждения.
- Гонки. Два терминала с
dev.sh startодновременно? Дублированные процессы, конфликты портов, испорченные PID-файлы. - Накладные расходы login shell.
bash -lc "node server.js"добавляет 200–500мс на каждый запуск сервиса.
Как 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: postgreskb-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 этой проблемы нет.