Перейти к содержанию

Runbook по эксплуатации

Процедурные инструкции для дежурных инженеров и SRE. На каждую типовую проблему — симптомы, диагностика, устранение.

Аудитория: on-call инженеры, DevOps, SRE. Связанные документы: DEPLOYMENT.ru.md, SERVER_MANAGEMENT.ru.md, ARCHITECTURE.ru.md.


Содержание

  1. Роли и эскалация
  2. Быстрые команды диагностики
  3. Runbook: ЦЛС не отвечает
  4. Runbook: устройство OFFLINE
  5. Runbook: команды отваливаются по TIMEOUT
  6. Runbook: очередь облака растёт / DEAD_LETTER
  7. Runbook: деплой прошивки упал
  8. Runbook: база PostgreSQL перегружена
  9. Runbook: потеря данных / восстановление
  10. Runbook: компрометация секрета
  11. Runbook: оператор забыл пароль / утрачен admin
  12. Runbook: массовый rollback прошивки
  13. Runbook: миграция на новую площадку
  14. Профилактическое обслуживание
  15. Скрипты и инструменты

1. Роли и эскалация

Роль Ответственность Эскалация
On-call Tier 1 Первичная диагностика по алертам, простые перезапуски → Tier 2 если проблема > 15 минут
On-call Tier 2 Углублённая диагностика, БД, код-хотфиксы → Инженер модуля
Инженер модуля Патчи, релизы, архитектурные решения → Tech Lead
Tech Lead Major incidents, коммуникация с OKTO Cloud → CTO

Контакты в runbook.internal wiki / PagerDuty.

SLA

Severity Реакция Resolution target
P1 (production down) 15 минут 4 часа
P2 (major degradation) 30 минут 24 часа
P3 (minor) 4 часа 1 неделя
P4 (low) 1 рабочий день По плану

2. Быстрые команды диагностики

Проверка здоровья ЦЛС

# Живой ли процесс
sudo systemctl status okto-factory
sudo journalctl -u okto-factory -n 100 --no-pager

# Health endpoint
curl -sf https://factory.okto.ru/api/v1/health | jq .

# Сколько устройств онлайн
JWT=$(curl -sX POST https://factory.okto.ru/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d "{\"username\":\"admin\",\"password\":\"$ADMIN_PW\"}" | jq -r .data.token)

curl -sH "Authorization: Bearer $JWT" https://factory.okto.ru/api/v1/devices/connected | jq '.data | length'

# Глубина очереди в облако
curl -sH "Authorization: Bearer $JWT" https://factory.okto.ru/api/v1/dashboard/sync/queue | jq .

# Последние 10 алертов
curl -sH "Authorization: Bearer $JWT" "https://factory.okto.ru/api/v1/alerts?limit=10" | jq .

Проверка edge-устройства

ssh okto@edge-01

sudo systemctl status okto-edge
tail -n 100 /var/log/okto/edge-service.log
curl -s http://localhost:8080/api/v1/health
curl -s http://localhost:8080/api/v1/queue/stats

Проверка БД

sudo -u postgres psql okto_factory <<SQL
-- Активные соединения
SELECT datname, usename, state, count(*) FROM pg_stat_activity GROUP BY datname, usename, state;

-- Размер таблиц
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;

-- Долгие запросы
SELECT pid, age(clock_timestamp(), query_start), state, query
FROM pg_stat_activity WHERE state != 'idle' ORDER BY query_start LIMIT 10;

-- Состояние очереди
SELECT status, count(*) FROM cloud_sync_queue GROUP BY status;
SQL

3. Runbook: ЦЛС не отвечает

Симптомы: - Алерт FactoryServerDown. - Curl /api/v1/health → timeout / connection refused. - Центральная консоль показывает «Центральный локальный сервер недоступен». - Edge-устройства логируют Failed to connect to factory WS.

Шаг 1 — проверьте процесс

sudo systemctl status okto-factory
  • active (running) → процесс жив, смотрите Шаг 3.
  • failed или inactiveШаг 2.

Шаг 2 — процесс упал

sudo journalctl -u okto-factory -n 200 --no-pager | tail -100

Ищите: - OutOfMemoryError → увеличьте -Xmx в systemd unit. - Cannot connect to database → проверьте PostgreSQL (systemctl status postgresql). - Address already in use :8081 → другой процесс держит порт (sudo lsof -i :8081). - JWT secret misconfigured → проверьте env var OKTO_JWT_SECRET.

Рестарт:

sudo systemctl restart okto-factory
sleep 5
curl -sf https://factory.okto.ru/api/v1/health

Шаг 3 — JVM сам не справляется

# PID
PID=$(systemctl show -p MainPID --value okto-factory)

# Thread dump
sudo -u okto jstack $PID > /tmp/threads.txt
cat /tmp/threads.txt | grep BLOCKED -A 10

# Heap dump (при подозрении на leak)
sudo -u okto jmap -dump:live,format=b,file=/tmp/heap.hprof $PID

# GC статистика
sudo -u okto jstat -gcutil $PID 1000 10

Если heap постоянно > 90%, сделайте heap dump и рестартуйте. Анализ в Eclipse MAT.

Шаг 4 — проблема в reverse-proxy

sudo systemctl status caddy            # или nginx
sudo journalctl -u caddy -n 50 --no-pager
curl -sf http://127.0.0.1:8081/api/v1/health   # минуя proxy

Если backend прямо отвечает, а через proxy нет — перезапустите proxy.

Шаг 5 — БД недоступна

sudo systemctl status postgresql
sudo -u postgres psql -c "SELECT 1"

Если БД упала — смотрите Шаг 8.

Recovery

После восстановления:

  1. Убедитесь, что endpoints отвечают.
  2. Проверьте количество онлайн-устройств — оно должно подняться в течение 1–2 минут (edge реконнектятся).
  3. Проверьте глубину очереди — всплеск ожидаем, должен спасть за 5–10 минут.
  4. Напишите post-mortem в Linear.

4. Runbook: устройство OFFLINE

Симптомы: - В центральной консоли устройство в «Не в сети». - Алерт DeviceHeartbeatStale сработал.

Шаг 1 — это реальная потеря связи?

curl -sH "Authorization: Bearer $JWT" "https://factory.okto.ru/api/v1/devices/edge-05" | jq '.data.status'
ping -c 3 edge-05
ssh okto@edge-05 systemctl status okto-edge
  • SSH ok, systemd active → WS-сессия отвалилась, но процесс жив. Шаг 2.
  • SSH ok, systemd failed → процесс упал. Шаг 3.
  • SSH недоступен → проблема на сетевом / физическом уровне. Шаг 4.

Шаг 2 — WS-сессия не устанавливается

На edge:

tail -f /var/log/okto/edge-service.log | grep -i "ServerConnection\|ws"

Частые причины: - 401 INVALID_TOKEN → device JWT истёк. Решение: systemctl restart okto-edge (edge перевыпустит токен через enrollment). - 403 DEVICE_DISABLED → устройство отключено на сервере. Включите: POST /api/v1/devices/{id}/commands {type:enable_device}. - Connection refused → factory unreachable. Проверьте сеть / DNS.

Принудительное пересоздание WS:

sudo systemctl restart okto-edge

Шаг 3 — edge-процесс упал

sudo journalctl -u okto-edge -n 200 --no-pager

Действия аналогичны §3 Шаг 2: найти причину, рестарт, эскалация.

Шаг 4 — физическая или сетевая проблема

Запросите у локального сайт-менеджера: - Есть ли электричество у терминала? - Не отключён ли LAN-кабель? - Работает ли Wi-Fi / VLAN?

Если проблема физическая — устройство вернётся в онлайн автоматически после восстановления.


5. Runbook: команды отваливаются по TIMEOUT

Симптомы: - Алерт CommandTimeoutsHigh. - Пользователь жалуется: «force_sync не проходит».

Шаг 1 — сколько timeout-ов?

SELECT device_id, count(*)
FROM device_commands
WHERE status = 'TIMEOUT' AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY device_id ORDER BY count DESC;
  • Один-два устройства → локальная проблема на edge.
  • Массово всех устройств → проблема на ЦЛС.

Шаг 2 — проверьте одно устройство

# Последние 10 команд
curl -sH "Authorization: Bearer $JWT" \
  "https://factory.okto.ru/api/v1/devices/edge-05/commands?limit=10" | jq '.data[] | {type, status, createdAt, dispatchedAt, completedAt}'
  • dispatched_at = null → команда не ушла по WS. Устройство либо offline, либо WS backpressure.
  • dispatched_at ≠ null, completed_at = null → TIMEOUT → устройство получило, но не ответило. Причина на edge.

Шаг 3 — проверьте edge

ssh okto@edge-05
sudo journalctl -u okto-edge -n 200 | grep -i "command\|exception"

Ищите uncaught exceptions в CommandHandlerService.

Временное средство:

sudo systemctl restart okto-edge

Шаг 4 — массовая проблема

Если timeout-ы на всех устройствах: - Проверьте, не упал ли DeviceConnectionRegistry (перезагрузка ЦЛС)? - Проверьте CommandDispatchService coroutine:

curl -sH "Authorization: Bearer $JWT" http://127.0.0.1:9091/metrics | grep okto_commands_pending
Высокий pending → dispatcher захлебнулся. - Thread dump ЦЛС (см. §3 Шаг 3) покажет, где блокировка.


6. Runbook: очередь облака растёт / DEAD_LETTER

Симптомы: - Алерт CloudQueueDeadLetter. - Dashboard показывает рост cloud queue.

Шаг 1 — кто виноват?

-- Что висит
SELECT status, count(*), min(created_at), max(created_at)
FROM cloud_sync_queue GROUP BY status;

-- Последние ошибки
SELECT id, type, retry_count, last_error, created_at
FROM cloud_sync_queue
WHERE status IN ('FAILED','DEAD_LETTER')
ORDER BY created_at DESC LIMIT 20;

Шаг 2 — типичные причины

  • 401 Unauthorized от облака → cloudSync.authToken истёк. Обновите в env, рестарт ЦЛС.
  • 409 Conflict → дубликат (excise_duty_number уже в облаке). Запись корректно уйдёт в DEAD_LETTER, нужно вручную подтвердить consistency.
  • 500 Internal Error от облака → эскалируйте OKTO Cloud support.
  • Connection refused / timeout → сетевая проблема. Проверьте DNS, TLS:
    curl -v https://app.okto.ru/api/v1/version
    

Шаг 3 — Re-queue DEAD_LETTER

После устранения причины — переведите DEAD_LETTER обратно в PENDING:

UPDATE cloud_sync_queue
SET status = 'PENDING', retry_count = 0, last_error = NULL, scheduled_at = now()
WHERE status = 'DEAD_LETTER'
  AND created_at > NOW() - INTERVAL '24 hours';

Осторожно: делайте это только если уверены, что проблема устранена. Иначе быстро снова уйдёт в DEAD_LETTER.

Шаг 4 — Temporary outbound pause

Если облако массово деградировало, остановите cloud sync, чтобы не забивать БД:

UPDATE global_config SET value = 'false' WHERE key = 'cloud_sync_enabled';

(требует соответствующей поддержки в коде — см. feature flag в CloudSyncService).

Либо systemctl stop okto-factory-cloudsync если вынесли в отдельный unit.


7. Runbook: деплой прошивки упал

Симптомы: - FirmwareDeployment.status = FAILED или TIMEOUT. - UI показывает «X/N устройств успешно».

Шаг 1 — какая ошибка?

SELECT device_id, status, error_message, started_at, completed_at
FROM firmware_deployments
WHERE release_id = '<release-id>' AND status != 'SUCCESS';

Шаг 2 — типичные причины

error_message Причина Решение
sha256 mismatch Proxy сжал / повредил artifact Отключите gzip для /artifact, перезалейте релиз
Command timed out Медленный canal, большой JAR Увеличьте timeout в deploy-request до 300_000 ms
sudo systemctl start ... denied Нет sudoers-записи на edge Залейте /etc/sudoers.d/okto вручную
Download failed: 401 Device JWT не имеет доступа к artifact Проверьте artifact-эндпоинт — он принимает device JWT
No space left on device Переполнен /var/lib/okto/firmware/staging Удалите старые staged артефакты

Шаг 3 — ручной rescue

Перезапустите деплой на проблемное устройство:

curl -X POST "https://factory.okto.ru/api/v1/firmware/deployments" \
  -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d "{\"releaseId\":\"$RELEASE_ID\",\"deviceIds\":[\"edge-05\"]}"

Или непосредственно на edge:

ssh okto@edge-05
sudo systemctl stop okto-edge
sudo cp /backup/edge-service-1.2.2.jar /opt/okto/edge-service.jar
sudo systemctl start okto-edge

Шаг 4 — Rollback

См. §12 Runbook: массовый rollback прошивки.


8. Runbook: база PostgreSQL перегружена

Симптомы: - Высокая latency на REST-ручках. - pg_stat_activity показывает много waiting-сессий. - Алерт PostgresHighConnections.

Шаг 1 — что делает БД?

-- Текущие запросы
SELECT pid, age(clock_timestamp(), query_start) AS age, state, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start ASC;

-- Блокировки
SELECT blocked_locks.pid AS blocked_pid,
       blocked_activity.usename AS blocked_user,
       blocking_locks.pid AS blocking_pid,
       blocking_activity.query AS blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
    AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

Шаг 2 — убейте проблемный запрос

SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = <problematic_pid>;

Шаг 3 — типичные hot-spots

  • device_logs растёт бесконтрольно:
    DELETE FROM device_logs WHERE ts < NOW() - INTERVAL '30 days';
    VACUUM ANALYZE device_logs;
    
  • cloud_sync_queue много DEAD_LETTER: см. §6.
  • Нехватка индексов: проверьте EXPLAIN на медленных запросах.

Шаг 4 — переполнен диск

df -h /var/lib/postgresql
du -sh /var/lib/postgresql/16/main/*

WAL накопились → проверьте archive_command или уменьшите max_wal_size.

Шаг 5 — масштабирование

Временно:

shared_buffers → +50%
max_connections → +30

Стратегически — PgBouncer (см. DEPLOYMENT.ru.md §15).


9. Runbook: потеря данных / восстановление

Сценарий: случайный DROP TABLE

Немедленно:

  1. Остановите запись в БД:
    sudo systemctl stop okto-factory
    
  2. Не запускайте ничего, пока не оцените масштаб.
  3. Скопируйте current state:
    pg_dump -Fc -U okto okto_factory > /backup/emergency-$(date +%s).dump
    

Восстановление из бэкапа:

# Последний ночной бэкап
gpg --decrypt /backup/factory-20260417.dump.gpg > /tmp/restore.dump

sudo -u postgres psql -c "DROP DATABASE okto_factory;"
sudo -u postgres psql -c "CREATE DATABASE okto_factory OWNER okto;"
pg_restore -U okto -d okto_factory /tmp/restore.dump

# Применить WAL для PITR (если доступно)
# … см. pg_archivecleanup + recovery.signal

sudo systemctl start okto-factory

Потеря данных за период между последним бэкапом и инцидентом:

Если WAL archiving включён — PITR до момента DROP:

sudo -u postgres pg_basebackup -D /var/lib/postgresql/16/main.restore -Ft -z -P
# recovery.conf:
#   restore_command = 'cp /backup/wal/%f %p'
#   recovery_target_time = '2026-04-17 14:30:00 MSK'
sudo systemctl start postgresql

Если WAL нет — потеряны данные с последнего pg_dump. Извлеките свежие операции из очередей edge (edge.db):

-- На каждом edge
SELECT * FROM operation_queue WHERE status = 'DONE' AND updated_at > '2026-04-17 02:00:00';

И переотправьте в ЦЛС.


10. Runbook: компрометация секрета

JWT secret скомпрометирован

Немедленно:

  1. Сгенерируйте новый секрет:
    openssl rand -base64 48
    
  2. Обновите /etc/okto/factory-server.env:
    OKTO_JWT_SECRET=<new>
    
  3. Рестарт:
    sudo systemctl restart okto-factory
    
  4. Все существующие JWT становятся невалидными. Все пользователи вынуждены залогиниться заново. Все edge-устройства пройдут новое enrollment (используют enrollment key).

Enrollment key скомпрометирован

Аналогично, но дополнительно:

  1. Сгенерируйте новый OKTO_ENROLLMENT_KEY.
  2. Перед рестартом: через push_config раздайте новый ключ всем edge-устройствам (в application.yaml → factoryServer.enrollmentKey).
  3. Рестарт ЦЛС (старый enrollment ключ больше не работает).
  4. Рестарт edge (они запросят новый device JWT со свежим ключом).

Альтернатива (если не хотите простоя): grace-period — поддерживать оба ключа на сервере, постепенно мигрировать.

DB password компрометирован

-- Под суперюзером
ALTER ROLE okto WITH PASSWORD 'новый пароль';

Обновите env var, рестарт.

Сертификаты TLS компрометированы

  1. Revoke через CA.
  2. Выпустите новые.
  3. Обновите reverse-proxy.
  4. Reload.

11. Runbook: оператор забыл пароль / утрачен admin

Забыл пароль оператора

Под админом:

curl -X PUT https://factory.okto.ru/api/v1/users/<userId> \
  -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
  -d '{"password":"tempPass123"}'

Оператор логинится и сразу меняет пароль (POST /auth/change-password).

Утрачен admin (нет ни одного ADMIN-пользователя)

Непосредственно в БД:

-- 1. Создать нового админа с bcrypt-хэшем пароля
-- Сгенерируйте хэш:
--   htpasswd -bnBC 10 "" "newAdminPass" | tr -d ':\n' | sed 's/$2y/$2b/'

INSERT INTO users (id, username, password_hash, role, disabled, created_at)
VALUES (
  gen_random_uuid()::text,
  'admin-rescue',
  '$2b$10$....',
  'ADMIN',
  false,
  now()
);

Затем войдите под admin-rescue через UI, настройте правильных пользователей.


12. Runbook: массовый rollback прошивки

Сценарий: выкатили 1.3.0, обнаружили баг, хотим вернуть 1.2.3 на всех устройствах.

Шаги

  1. Убедитесь, что релиз 1.2.3 существует в firmware_releases. Если нет — загрузите заново:

    curl -X POST "https://factory.okto.ru/api/v1/firmware/releases?version=1.2.3&channel=stable&filename=edge-service-1.2.3.jar" \
      -H "Authorization: Bearer $JWT" \
      -H "Content-Type: application/octet-stream" \
      --data-binary @/backup/releases/edge-service-1.2.3.jar
    

  2. Деплой на всю группу:

    curl -X POST "https://factory.okto.ru/api/v1/firmware/deployments" \
      -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
      -d '{"releaseId":"<1.2.3 id>","groupId":"all-devices"}'
    

  3. Дождитесь SUCCESS по всем.

  4. Отправьте restart_service (иначе swap произойдёт только при следующем естественном рестарте):

curl -X POST "https://factory.okto.ru/api/v1/device-groups/all-devices/commands" \
  -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"command":{"type":"restart_service","id":"rescue-'$(uuidgen)'"},"timeoutMs":60000}'
  1. Удалите проблемный релиз 1.3.0 (чтобы никто случайно не деплойнул):

    curl -X DELETE "https://factory.okto.ru/api/v1/firmware/releases/<1.3.0 id>" -H "Authorization: Bearer $JWT"
    

  2. Post-mortem.


13. Runbook: миграция на новую площадку

Сценарий: переезд ЦЛС на новый хост / дата-центр.

Шаги

  1. Заранее сделайте pg_dump:

    pg_dump -Fc -U okto okto_factory > /backup/migration.dump
    tar czf /backup/firmware.tgz /var/lib/okto/firmware
    

  2. На новом хосте — полное развёртывание (см. DEPLOYMENT.ru.md).

  3. Восстановите БД:

    pg_restore -U okto -d okto_factory /backup/migration.dump
    

  4. Скопируйте прошивки:

    tar xzf /backup/firmware.tgz -C /
    

  5. Обновите DNS factory.okto.ru → новый IP. TTL должен быть низким (300s) за сутки до миграции.

  6. Edge-устройства автоматически реконнектятся на новый IP (через DNS).

  7. Проверьте, что все устройства онлайн:

    curl -sH "Authorization: Bearer $JWT" https://factory.okto.ru/api/v1/devices/connected | jq '.data | length'
    

  8. Оставьте старый хост как резерв на 7 дней.

Альтернатива: blue-green

  • Поднять новый ЦЛС параллельно.
  • Запустить dual-write (edge шлёт в оба).
  • Убедиться в консистентности.
  • Переключить DNS.
  • Отключить старый.

14. Профилактическое обслуживание

Ежедневно

  • Автоматически: pg_dump (cron 02:00).
  • Автоматически: DELETE FROM device_logs WHERE ts < NOW() - INTERVAL '30 days'.
  • Дежурный: смотрит Grafana dashboard «OKTO Overview» — алерты, аномалии.

Еженедельно

  • Проверить размеры таблиц (pg_total_relation_size).
  • Проверить pg-логи на ERROR.
  • Отзыв неактивных user sessions (DELETE FROM sessions WHERE last_used_at < NOW() - INTERVAL '30 days').
  • Отчёт по firmware-версиям в парке.

Ежемесячно

  • VACUUM FULL ANALYZE на большие таблицы (aggregated_bottles, device_logs).
  • Ротация логов ЦЛС (logrotate проверить).
  • Тест восстановления из бэкапа на staging.
  • Проверка сроков TLS-сертификатов.
  • Обновления security (unattended-upgrades logs).

Ежегодно

  • Поворот JWT secret и enrollment key.
  • Ревизия ролей и пользователей (кто всё ещё ADMIN, нужно ли).
  • Pen-test / security audit.
  • DR-drill: полный restore в изолированной среде.

15. Скрипты и инструменты

Быстрый status-скрипт

scripts/ops/status.sh:

#!/usr/bin/env bash
set -e
BASE="https://factory.okto.ru"
JWT=$(curl -sX POST $BASE/api/v1/auth/login -H 'Content-Type: application/json' \
  -d "{\"username\":\"admin\",\"password\":\"$ADMIN_PW\"}" | jq -r .data.token)

echo "== Health =="
curl -sf $BASE/api/v1/health | jq .

echo ""
echo "== Connected devices =="
curl -sH "Authorization: Bearer $JWT" $BASE/api/v1/devices/connected | jq '.data | length'

echo ""
echo "== Cloud queue =="
curl -sH "Authorization: Bearer $JWT" $BASE/api/v1/dashboard/sync/queue | jq .

echo ""
echo "== Recent alerts =="
curl -sH "Authorization: Bearer $JWT" "$BASE/api/v1/alerts?limit=5" | jq '.data[] | {severity, category, message, createdAt}'

Массовый рестарт edge

scripts/ops/restart-all-edges.sh:

#!/usr/bin/env bash
curl -X POST "https://factory.okto.ru/api/v1/device-groups/all-devices/commands" \
  -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
  -d "{\"command\":{\"type\":\"restart_service\",\"id\":\"mass-$(uuidgen)\"},\"timeoutMs\":60000}" | jq .

Health-dashboard (mini)

Grafana import ID: <tbd> (импортируйте JSON из docs/grafana/okto-overview.json).


Обновлено: апрель 2026. Ответственные: ops@okto.ru, on-call: PagerDuty → «OKTO-production».