Устойчивость переноса мессенджеров на удалённый VPS
Назначение документа
Документ фиксирует инцидент 10 июля 2026 года с удалённым запуском WhatsApp и Telegram в ModuleCTIClient, выполненное оперативное восстановление и проект полного усиления механизма remote offload.
Цель доработки: после любого одиночного сбоя система автоматически сходится либо к полностью локальному, либо к полностью удалённому размещению каждого канала. Один messenger area UUID никогда не должен одновременно работать на MikoPBX и VPS.
Архитектурный контекст
В штатной схеме:
- локальный
monitord управляет локальными демонами и SSH-туннелем;
WorkerRemoteTunnel выполняет провижининг VPS, запускает удалённый monitord и продвигает state machine миграции;
- локальный
{spool}/custom_config.json хранит фактическое размещение area (local или remote) и изменяется Go-кодом monitord;
{spool}/remote_state.json хранит durable cursor PHP-оркестратора;
- удалённый
monitord запускает chatsd, tgd и maxd на VPS;
- reverse-forward
127.0.0.1:4222 и 127.0.0.1:8222 предоставляют удалённым демонам локальные NATS и license API;
- local-forward на
127.0.0.1:8226 предоставляет локальному monitord удалённый manager API 127.0.0.1:8225.
Наблюдаемые симптомы
Пользователь включил remote_telegram, но Telegram продолжал отображаться и работать локально. Web-интерфейс показывал длительную миграцию и блокировал удалённые настройки. Затем появилась ошибка SSH-туннеля:
forwards: remote-listen 127.0.0.1:4222: ssh: tcpip-forward request denied by peer
Архив логов показал дополнительные симптомы:
remote provision: ssh failed (uname): rc=255
remote provision: scp push failed: chatsd
ssh_tunnel: local-forward dial 127.0.0.1:8225 failed:
ssh: rejected: connect failed (Connection refused)
Ошибки SSH и провижининга повторялись длительное время. В отдельные периоды локальные и удалённые экземпляры tgd одновременно обслуживали одинаковые UUID.
Подтверждённое состояние до восстановления
Telegram
Локальный custom_config.json содержал 10 актуальных Telegram-area и указывал для них location: local (у одной legacy-записи поле отсутствовало, что также нормализуется в local). Локально работали 10 процессов tgd.
remote_state.json содержал:
{
"tg": {
"migrating": false,
"resuming": false,
"parked": true,
"fail_count": 0,
"last_error": "migration failed (VPS unreachable), still on local"
}
}
Из-за parked: true reconcileMigrations() намеренно пропускал Telegram до ручного повторного переключения опции.
На VPS в удалённом custom_config.json оставались шесть Telegram-area. Их реальные UUID удалены из публичного документа; ниже приведены обезличенные обозначения:
telegram-area-01
telegram-area-02
telegram-area-03
telegram-area-04
telegram-area-05
telegram-area-06
Это были orphan-записи незавершённой миграции. Локальный rollback вернул сервис в local, но не смог очистить удалённую сторону, поскольку VPS в тот момент был недоступен.
WhatsApp и SSH-туннель
Во время повторного запуска миграции WhatsApp состояние застыло в:
{
"chats": {
"migrating": true,
"resuming": true,
"fail_count": 5,
"last_error": ""
}
}
На MikoPBX одновременно работали два идентичных процесса:
16620 .../ModuleCTIClient/bin/monitord -c /etc/custom_modules/ModuleCTIClient/monitord.json
16622 .../ModuleCTIClient/bin/monitord -c /etc/custom_modules/ModuleCTIClient/monitord.json
Оба процесса имели PPID=1 и одинаковое время старта. Оба пытались владеть одним SSH-туннелем и одинаковыми reverse-forward. Один SSH-сеанс занимал remote listen 127.0.0.1:4222, второй получал tcpip-forward request denied by peer.
Это подтверждённая непосредственная причина ошибки туннеля и зависшей стадии resuming.
Первопричина инцидента
Инцидент возник из сочетания четырёх дефектов защиты:
monitord не обеспечивал строгое singleton-владение процессом и SSH-туннелем. Два экземпляра смогли одновременно стартовать.
- Ошибка reverse-forward не классифицировалась как локальный конфликт владельцев и не приводила к автоматическому устранению дубля.
- Rollback миграции при недоступном VPS не сохранял обязательство очистить удалённые daemon entries после восстановления связи.
- Удалённый
custom_config.json мог продолжать самостоятельно запускать orphan-процессы, хотя локальный authoritative state уже считал сервис локальным.
Продолжительная недоступность SSH и частичный провижининг (scp push failed) были триггерами, но не должны были приводить к split-brain при наличии полных защитных механизмов.
Совпадение SHA-256 локального и удалённого бинарников monitord исключило несовместимость версий как причину данного инцидента.
Выполненное оперативное восстановление
Восстановление выполнялось через промежуточный Mac и SSH-доступ к MikoPBX и VPS.
-
На VPS проверено отсутствие работающих monitord и tgd перед очисткой.
-
Создана резервная копия удалённого runtime state:
{remote_bin_dir}/spool/custom_config.json.backup.{timestamp}
-
Удалённый custom_config.json, содержавший только шесть orphan-записей Telegram, очищен до валидного состояния:
-
На MikoPBX остановлены два конфликтующих экземпляра monitord.
-
Запущен ровно один локальный monitord.
-
Проверен статус туннеля:
{
"configured": true,
"connected": true,
"state": "connected",
"last_error": "",
"attempts": 1
}
-
State machine автоматически завершила WhatsApp и Telegram миграции.
-
После восстановления локально не осталось chatsd и tgd; на VPS работали 8 chatsd и 10 tgd.
-
Для chats и tg подтверждено конечное состояние:
migrating=false
resuming=false
parked=false
fail_count=0
last_error=""
Выбранный подход к доработке
Рекомендуется единое владение жизненным циклом в Go monitord:
- локальный
monitord является единственным владельцем SSH-туннеля;
- PHP публикует desired state и управляет durable migration transaction;
- локальный
custom_config.json является authoritative source размещения;
- состояние VPS является производным и полностью восстанавливается reconciliation-процедурой;
- удалённый
custom_config.json не имеет права сохранять процессы, отсутствующие в локальном desired state.
Точечное усиление только PHP-кода не рекомендуется: оно не устраняет гонку между несколькими Go-процессами. Отдельный coordinator daemon также не рекомендуется из-за избыточной сложности и новой точки отказа.
Требования к реализации
1. Singleton local monitord
- До открытия HTTP/NATS/SSH
monitord должен получать эксклюзивный OS lock.
- Lock должен быть привязан к конкретному config/work directory, чтобы независимые установки не конфликтовали.
- Второй экземпляр должен завершаться с отдельным exit code и структурированной записью
duplicate_instance.
- PID-файл используется только для диагностики; корректность обеспечивается
flock/эквивалентной системной блокировкой, а не проверкой существования PID.
- Все пути запуска (
startAllServices, reconcile restart, watchdog, upgrade) должны использовать один idempotent launcher.
2. Lease владельца SSH-туннеля
- VPS хранит owner ID, owner label, lease timestamp и connection/session identity.
- Новый владелец не захватывает активный lease другого узла.
- Просроченный lease можно забрать только после проверки отсутствия живого SSH-сеанса или успешного fencing старого владельца.
- Конфликт lease отличается от обычной сетевой ошибки в status API и UI.
tcpip-forward request denied by peer должен запускать диагностику владельца порта и controlled recovery, а не бесконечный reconnect storm.
3. Preflight перед миграцией
До SUPPRESS необходимо проверить:
- SSH-аутентификацию тем же ключом и параметрами, которые использует worker;
- архитектуру VPS
x86_64;
- существование и права
remote_bin_dir;
- свободное место для бинарников и session DB;
- возможность создать и удалить тестовый файл;
- доступность либо возможность запуска remote
monitord;
- версии/хеши обязательных бинарников;
- отсутствие конфликтующего tunnel owner;
- готовность reverse-forward 4222/8222 и local-forward 8226;
- наличие remote config для выбранного сервиса.
Неуспешный preflight не должен останавливать локальные messenger daemons.
4. Транзакция миграции по area
Для каждой area сохраняются:
transaction_id
service
area
source
destination
phase
attempt
started_at
updated_at
last_error_code
last_error_detail
cleanup_required
Допустимая последовательность:
PREFLIGHT -> SUPPRESS_SOURCE -> COPY -> CREATE_DESTINATION
-> HEALTHCHECK_DESTINATION -> COMMIT_LOCATION -> CLEANUP_SOURCE
До COMMIT_LOCATION authoritative side остаётся source. Ошибка до commit возвращает source в работу. Ошибка после commit не должна запускать source без отдельного fencing destination.
5. Ownership token и защита от split-brain
- Каждая запущенная messenger area получает generation/ownership token.
- Token хранится в authoritative state и передаётся удалённому manager API.
- Демон может стартовать только при совпадении текущего token.
- При смене стороны generation увеличивается.
- Старый процесс с предыдущим token должен self-fence после потери lease или при следующей проверке.
- Manager API отклоняет создание дубликата area с другим активным owner.
6. Durable cleanup queue
Если rollback или commit не может удалить процессы/файлы на другой стороне, создаётся durable cleanup item:
service, area, side, generation, action, attempts, next_attempt_at, last_error
Очередь обрабатывается после каждого восстановления SSH и периодически в steady state. Cleanup считается завершённым только после проверки отсутствия процесса и daemon entry.
7. Authoritative reconciliation VPS
При подключённом туннеле worker сравнивает:
- локальный authoritative
custom_config;
remote_state и cleanup queue;
- фактический удалённый manager API inventory;
- процессы на VPS.
Reconcile должен:
- удалить remote orphan entries;
- остановить remote orphan processes;
- создать недостающие remote entries только для committed remote area;
- не переносить area самопроизвольно без migration transaction;
- сообщать drift до и после исправления;
- быть идемпотентным.
Удалённый custom_config.json рассматривается как cache/runtime projection, а не самостоятельный источник истины.
8. Состояния ошибок и UI
Вместо одной надписи «Идёт миграция» API и UI должны различать:
- preflight;
- остановку источника;
- копирование;
- запуск назначения;
- ожидание health-check;
- commit;
- rollback;
- cleanup orphan;
- tunnel conflict;
- parked/manual action required.
Для ошибки показываются service, area (если применимо), этап, время, стабильный error code и безопасное рекомендуемое действие. Защищённые настройки блокируются только пока изменение действительно нарушает активную транзакцию; cleanup не должен без необходимости блокировать несвязанные сервисы.
9. Наблюдаемость
- Структурированные события с
transaction_id, service, area, phase, source, destination.
- Отдельные метрики: duplicate process, tunnel lease conflict, migration duration, rollback count, cleanup backlog, drift count.
- Status endpoint возвращает число локальных/удалённых/мигрирующих/orphan area.
- Диагностический bundle включает
remote_state.json, локальный и удалённый inventory, tunnel owner/lease и sanitized configs без приватного ключа.
- Повторяющиеся ошибки логируются с rate limit и счётчиком, а не создают несколько строк в секунду.
10. Самоочистка legacy state
- Area, отсутствующая в authoritative inventory и не имеющая активной transaction/cleanup item, удаляется из
remote_state после grace period.
- Перед удалением выполняется проверка обеих сторон.
- Это устраняет исторические area-записи, которых уже нет в authoritative inventory.
Тестовый план
Unit-тесты
- второй
monitord не может получить singleton lock;
- stale PID без lock не препятствует старту;
- активный lock не считается stale даже при неправильном PID-файле;
- классификация SSH/forward/lease ошибок;
- переходы migration transaction и запрет недопустимых переходов;
- generation token увеличивается только при новой миграции;
- cleanup queue сохраняется после рестарта;
- нормализация legacy
remote_state.
Интеграционные тесты
- два одновременных запуска
monitord;
- порт 4222 занят неизвестным SSH-сеансом;
- обрыв SSH на каждом этапе migration transaction;
- недоступный remote manager до и после copy;
- частично скопированная session DB;
- успешный remote start без ответа health-check;
- crash PHP worker и обоих
monitord на каждом этапе;
- восстановление VPS с orphan
custom_config;
- одновременное изменение WhatsApp и Telegram;
- module upgrade при активном remote размещении;
- повторное включение после
parked;
- отсутствие split-brain при сетевом partition.
Fault-injection сценарий инцидента
Автоматизированный сценарий должен воспроизводить исходный инцидент:
- Запустить миграцию нескольких Telegram-area.
- После создания части remote entries разорвать SSH.
- Перезапустить локальный stack дважды параллельно.
- Восстановить VPS с сохранённым удалённым
custom_config.
- Проверить, что второй local
monitord не стартует.
- Проверить, что source остаётся единственным владельцем до commit.
- Проверить автоматическую очистку orphan entries.
- Восстановить SSH и убедиться в конечном размещении всех area на одной стороне.
Этапы реализации
- Singleton
monitord и единый idempotent launcher.
- Tunnel lease, диагностика владельца reverse-forward и controlled recovery.
- Durable migration transaction с error codes.
- Ownership generation/token и fencing.
- Durable cleanup queue и authoritative VPS reconciliation.
- Расширенные status API, UI и diagnostic bundle.
- Fault-injection и интеграционные тесты.
- Миграция legacy state и эксплуатационная документация.
Каждый этап должен поставляться отдельно и сохранять совместимость с PHP 7.4–8.4 и поддерживаемыми версиями MikoPBX/Phalcon.
Критерии приёмки
- В каждый момент времени существует не более одного локального
monitord для одного work directory.
- Один
area UUID не работает одновременно локально и удалённо.
- Неуспешный preflight не вызывает простой локального сервиса.
- После обрыва SSH state machine автоматически приходит к однозначному состоянию или показывает конкретное manual action.
- После восстановления VPS orphan entries и процессы удаляются автоматически.
remote_state, local inventory и VPS inventory сходятся без ручного редактирования JSON.
- Занятый reverse-forward диагностируется как owner conflict с owner ID, а не как абстрактная ошибка SSH.
- Повторный старт, restart и upgrade идемпотентны.
- UI показывает текущий этап, сервис, число area и конкретную причину блокировки.
- Fault-injection сценарий исходного инцидента проходит без split-brain.
Эксплуатационные ограничения
- Ручное редактирование
custom_config.json допустимо только как аварийная процедура при остановленном владельце файла.
- Приватный SSH-ключ не должен попадать в логи, status API или diagnostic bundle.
- Автоматический cleanup обязан проверять ownership generation, чтобы не остановить актуальный процесс после новой миграции.
- При невозможности доказать безопасное владение система выбирает остановку назначения и сохранение authoritative source, а не параллельный запуск.
Устойчивость переноса мессенджеров на удалённый VPS
Назначение документа
Документ фиксирует инцидент 10 июля 2026 года с удалённым запуском WhatsApp и Telegram в
ModuleCTIClient, выполненное оперативное восстановление и проект полного усиления механизма remote offload.Цель доработки: после любого одиночного сбоя система автоматически сходится либо к полностью локальному, либо к полностью удалённому размещению каждого канала. Один messenger
areaUUID никогда не должен одновременно работать на MikoPBX и VPS.Архитектурный контекст
В штатной схеме:
monitordуправляет локальными демонами и SSH-туннелем;WorkerRemoteTunnelвыполняет провижининг VPS, запускает удалённыйmonitordи продвигает state machine миграции;{spool}/custom_config.jsonхранит фактическое размещениеarea(localилиremote) и изменяется Go-кодомmonitord;{spool}/remote_state.jsonхранит durable cursor PHP-оркестратора;monitordзапускаетchatsd,tgdиmaxdна VPS;127.0.0.1:4222и127.0.0.1:8222предоставляют удалённым демонам локальные NATS и license API;127.0.0.1:8226предоставляет локальномуmonitordудалённый manager API127.0.0.1:8225.Наблюдаемые симптомы
Пользователь включил
remote_telegram, но Telegram продолжал отображаться и работать локально. Web-интерфейс показывал длительную миграцию и блокировал удалённые настройки. Затем появилась ошибка SSH-туннеля:Архив логов показал дополнительные симптомы:
Ошибки SSH и провижининга повторялись длительное время. В отдельные периоды локальные и удалённые экземпляры
tgdодновременно обслуживали одинаковые UUID.Подтверждённое состояние до восстановления
Telegram
Локальный
custom_config.jsonсодержал 10 актуальных Telegram-area и указывал для нихlocation: local(у одной legacy-записи поле отсутствовало, что также нормализуется вlocal). Локально работали 10 процессовtgd.remote_state.jsonсодержал:{ "tg": { "migrating": false, "resuming": false, "parked": true, "fail_count": 0, "last_error": "migration failed (VPS unreachable), still on local" } }Из-за
parked: truereconcileMigrations()намеренно пропускал Telegram до ручного повторного переключения опции.На VPS в удалённом
custom_config.jsonоставались шесть Telegram-area. Их реальные UUID удалены из публичного документа; ниже приведены обезличенные обозначения:Это были orphan-записи незавершённой миграции. Локальный rollback вернул сервис в
local, но не смог очистить удалённую сторону, поскольку VPS в тот момент был недоступен.WhatsApp и SSH-туннель
Во время повторного запуска миграции WhatsApp состояние застыло в:
{ "chats": { "migrating": true, "resuming": true, "fail_count": 5, "last_error": "" } }На MikoPBX одновременно работали два идентичных процесса:
Оба процесса имели
PPID=1и одинаковое время старта. Оба пытались владеть одним SSH-туннелем и одинаковыми reverse-forward. Один SSH-сеанс занимал remote listen127.0.0.1:4222, второй получалtcpip-forward request denied by peer.Это подтверждённая непосредственная причина ошибки туннеля и зависшей стадии
resuming.Первопричина инцидента
Инцидент возник из сочетания четырёх дефектов защиты:
monitordне обеспечивал строгое singleton-владение процессом и SSH-туннелем. Два экземпляра смогли одновременно стартовать.custom_config.jsonмог продолжать самостоятельно запускать orphan-процессы, хотя локальный authoritative state уже считал сервис локальным.Продолжительная недоступность SSH и частичный провижининг (
scp push failed) были триггерами, но не должны были приводить к split-brain при наличии полных защитных механизмов.Совпадение SHA-256 локального и удалённого бинарников
monitordисключило несовместимость версий как причину данного инцидента.Выполненное оперативное восстановление
Восстановление выполнялось через промежуточный Mac и SSH-доступ к MikoPBX и VPS.
На VPS проверено отсутствие работающих
monitordиtgdперед очисткой.Создана резервная копия удалённого runtime state:
Удалённый
custom_config.json, содержавший только шесть orphan-записей Telegram, очищен до валидного состояния:{ "custom_daemons": [] }На MikoPBX остановлены два конфликтующих экземпляра
monitord.Запущен ровно один локальный
monitord.Проверен статус туннеля:
{ "configured": true, "connected": true, "state": "connected", "last_error": "", "attempts": 1 }State machine автоматически завершила WhatsApp и Telegram миграции.
После восстановления локально не осталось
chatsdиtgd; на VPS работали 8chatsdи 10tgd.Для
chatsиtgподтверждено конечное состояние:Выбранный подход к доработке
Рекомендуется единое владение жизненным циклом в Go
monitord:monitordявляется единственным владельцем SSH-туннеля;custom_config.jsonявляется authoritative source размещения;custom_config.jsonне имеет права сохранять процессы, отсутствующие в локальном desired state.Точечное усиление только PHP-кода не рекомендуется: оно не устраняет гонку между несколькими Go-процессами. Отдельный coordinator daemon также не рекомендуется из-за избыточной сложности и новой точки отказа.
Требования к реализации
1. Singleton local
monitordmonitordдолжен получать эксклюзивный OS lock.duplicate_instance.flock/эквивалентной системной блокировкой, а не проверкой существования PID.startAllServices, reconcile restart, watchdog, upgrade) должны использовать один idempotent launcher.2. Lease владельца SSH-туннеля
tcpip-forward request denied by peerдолжен запускать диагностику владельца порта и controlled recovery, а не бесконечный reconnect storm.3. Preflight перед миграцией
До
SUPPRESSнеобходимо проверить:x86_64;remote_bin_dir;monitord;Неуспешный preflight не должен останавливать локальные messenger daemons.
4. Транзакция миграции по area
Для каждой area сохраняются:
Допустимая последовательность:
До
COMMIT_LOCATIONauthoritative side остаётсяsource. Ошибка до commit возвращает source в работу. Ошибка после commit не должна запускать source без отдельного fencing destination.5. Ownership token и защита от split-brain
6. Durable cleanup queue
Если rollback или commit не может удалить процессы/файлы на другой стороне, создаётся durable cleanup item:
Очередь обрабатывается после каждого восстановления SSH и периодически в steady state. Cleanup считается завершённым только после проверки отсутствия процесса и daemon entry.
7. Authoritative reconciliation VPS
При подключённом туннеле worker сравнивает:
custom_config;remote_stateи cleanup queue;Reconcile должен:
Удалённый
custom_config.jsonрассматривается как cache/runtime projection, а не самостоятельный источник истины.8. Состояния ошибок и UI
Вместо одной надписи «Идёт миграция» API и UI должны различать:
Для ошибки показываются service, area (если применимо), этап, время, стабильный error code и безопасное рекомендуемое действие. Защищённые настройки блокируются только пока изменение действительно нарушает активную транзакцию; cleanup не должен без необходимости блокировать несвязанные сервисы.
9. Наблюдаемость
transaction_id,service,area,phase,source,destination.remote_state.json, локальный и удалённый inventory, tunnel owner/lease и sanitized configs без приватного ключа.10. Самоочистка legacy state
remote_stateпосле grace period.Тестовый план
Unit-тесты
monitordне может получить singleton lock;remote_state.Интеграционные тесты
monitord;monitordна каждом этапе;custom_config;parked;Fault-injection сценарий инцидента
Автоматизированный сценарий должен воспроизводить исходный инцидент:
custom_config.monitordне стартует.Этапы реализации
monitordи единый idempotent launcher.Каждый этап должен поставляться отдельно и сохранять совместимость с PHP 7.4–8.4 и поддерживаемыми версиями MikoPBX/Phalcon.
Критерии приёмки
monitordдля одного work directory.areaUUID не работает одновременно локально и удалённо.remote_state, local inventory и VPS inventory сходятся без ручного редактирования JSON.Эксплуатационные ограничения
custom_config.jsonдопустимо только как аварийная процедура при остановленном владельце файла.