Runbook по эксплуатации¶
Процедурные инструкции для дежурных инженеров и SRE. На каждую типовую проблему — симптомы, диагностика, устранение.
Аудитория: on-call инженеры, DevOps, SRE. Связанные документы: DEPLOYMENT.ru.md, SERVER_MANAGEMENT.ru.md, ARCHITECTURE.ru.md.
Содержание¶
- Роли и эскалация
- Быстрые команды диагностики
- Runbook: ЦЛС не отвечает
- Runbook: устройство OFFLINE
- Runbook: команды отваливаются по TIMEOUT
- Runbook: очередь облака растёт / DEAD_LETTER
- Runbook: деплой прошивки упал
- Runbook: база PostgreSQL перегружена
- Runbook: потеря данных / восстановление
- Runbook: компрометация секрета
- Runbook: оператор забыл пароль / утрачен admin
- Runbook: массовый rollback прошивки
- Runbook: миграция на новую площадку
- Профилактическое обслуживание
- Скрипты и инструменты
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 — проверьте процесс¶
Шаг 2 — процесс упал¶
Ищите:
- 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.
Рестарт:
Шаг 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 — БД недоступна¶
Если БД упала — смотрите Шаг 8.
Recovery¶
После восстановления:
- Убедитесь, что endpoints отвечают.
- Проверьте количество онлайн-устройств — оно должно подняться в течение 1–2 минут (edge реконнектятся).
- Проверьте глубину очереди — всплеск ожидаем, должен спасть за 5–10 минут.
- Напишите 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:
Частые причины:
- 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:
Шаг 3 — edge-процесс упал¶
Действия аналогичны §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¶
Ищите uncaught exceptions в CommandHandlerService.
Временное средство:
Шаг 4 — массовая проблема¶
Если timeout-ы на всех устройствах:
- Проверьте, не упал ли DeviceConnectionRegistry (перезагрузка ЦЛС)?
- Проверьте CommandDispatchService coroutine:
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:
Шаг 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, чтобы не забивать БД:
(требует соответствующей поддержки в коде — см. 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 — убейте проблемный запрос¶
Шаг 3 — типичные hot-spots¶
device_logsрастёт бесконтрольно:cloud_sync_queueмного DEAD_LETTER: см. §6.- Нехватка индексов: проверьте EXPLAIN на медленных запросах.
Шаг 4 — переполнен диск¶
WAL накопились → проверьте archive_command или уменьшите max_wal_size.
Шаг 5 — масштабирование¶
Временно:
Стратегически — PgBouncer (см. DEPLOYMENT.ru.md §15).
9. Runbook: потеря данных / восстановление¶
Сценарий: случайный DROP TABLE¶
Немедленно:
- Остановите запись в БД:
- Не запускайте ничего, пока не оцените масштаб.
- Скопируйте current state:
Восстановление из бэкапа:
# Последний ночной бэкап
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 скомпрометирован¶
Немедленно:
- Сгенерируйте новый секрет:
- Обновите
/etc/okto/factory-server.env: - Рестарт:
- Все существующие JWT становятся невалидными. Все пользователи вынуждены залогиниться заново. Все edge-устройства пройдут новое enrollment (используют enrollment key).
Enrollment key скомпрометирован¶
Аналогично, но дополнительно:
- Сгенерируйте новый
OKTO_ENROLLMENT_KEY. - Перед рестартом: через
push_configраздайте новый ключ всем edge-устройствам (вapplication.yaml → factoryServer.enrollmentKey). - Рестарт ЦЛС (старый enrollment ключ больше не работает).
- Рестарт edge (они запросят новый device JWT со свежим ключом).
Альтернатива (если не хотите простоя): grace-period — поддерживать оба ключа на сервере, постепенно мигрировать.
DB password компрометирован¶
Обновите env var, рестарт.
Сертификаты TLS компрометированы¶
- Revoke через CA.
- Выпустите новые.
- Обновите reverse-proxy.
- 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.2.3 существует в
firmware_releases. Если нет — загрузите заново: -
Деплой на всю группу:
-
Дождитесь SUCCESS по всем.
-
Отправьте
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.3.0 (чтобы никто случайно не деплойнул):
-
Post-mortem.
13. Runbook: миграция на новую площадку¶
Сценарий: переезд ЦЛС на новый хост / дата-центр.
Шаги¶
-
Заранее сделайте pg_dump:
-
На новом хосте — полное развёртывание (см. DEPLOYMENT.ru.md).
-
Восстановите БД:
-
Скопируйте прошивки:
-
Обновите DNS
factory.okto.ru→ новый IP. TTL должен быть низким (300s) за сутки до миграции. -
Edge-устройства автоматически реконнектятся на новый IP (через DNS).
-
Проверьте, что все устройства онлайн:
-
Оставьте старый хост как резерв на 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-upgradeslogs).
Ежегодно¶
- Поворот 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».