MAATRIX / Блог / Недокументированные связи между сервисами: кто кого держит на этом сервере

Недокументированные связи между сервисами: кто кого держит на этом сервере

MAATRIX

Вы уже составили карту сервисов на унаследованном сервере: какие процессы работают, какие порты открыты, какие сайты и базы есть на машине. Список получился длинный, но безобидный на вид — можно выбирать, что выключить или обновить первым. А потом вы гасите «неиспользуемый» контейнер, и падает совершенно другой сайт. Или обновляете PHP на одном проекте — и у соседнего перестают формироваться отчёты. Дело не в том, что карта была неполной: она показывала сервисы как отдельные острова, а на самом деле часть из них тихо держится друг за друга — через общий файл, общую таблицу в базе или cron-задачу, которая правит чужие данные. Разберём, как находить такие связи до того, как их найдёт продакшен.

Почему обычная карта сервисов не видит зависимостей

Инвентаризация процессов, портов и сайтов отвечает на вопрос «что здесь есть», но не на вопрос «что от чего зависит». Процесс nginx и процесс python manage.py runserver в списке выглядят как два независимых пункта — разные PID, разные пользователи, разные каталоги. Связь между ними не отражается ни в ps aux, ни в списке открытых портов: она живёт на уровне файловой системы (один читает то, что пишет другой), на уровне базы (обе программы стучатся в одну СУБД под разными учётными записями) или на уровне расписания (cron одного проекта трогает каталог другого).

Это шире, чем вопрос «что делает один конкретный процесс»: какие из внешне не связанных сервисов на самом деле образуют одну систему, где поломка одного узла тянет за собой остальные. На унаследованном сервере такие связи почти никогда не документированы — их создавали ситуативно, а через полгода-год об этом решении просто забывали, и следующий админ получал набор процессов без единой пометки о том, что они друг с другом делают.

Ниже — три источника скрытых связей, которые встречаются чаще всего, и конкретные инструменты, которыми их можно найти за разумное время, не читая исходники всех проектов на сервере построчно.

Общий файл: один сервис тихо читает или пишет то, что принадлежит другому

Самый частый и самый незаметный тип связи — файловый. Два приложения формально независимы: разные каталоги, разные виртуальные хосты, возможно разные языки. Но одно в фоне читает CSV, который выгружает другое, или оба пишут в один лог-файл, или сервис держит открытым сокет в каталоге соседнего проекта. Найти это по коду тяжело — грепать репозитории в поисках абсолютных путей ненадёжно, путь мог прийти из конфига, переменной окружения или быть собран на лету.

Надёжнее смотреть не на код, а на то, что процессы реально открывают прямо сейчас. Для этого нужен lsof.

# все открытые файлы конкретного процесса
sudo lsof -p <PID>

# кто вообще держит открытым конкретный файл или каталог
sudo lsof /var/www/project-a/storage/export.csv

# все процессы, у которых есть открытые файлы вне их собственного каталога — вручную сверяете вывод с ожидаемым деревом
sudo lsof -c python -c php-fpm -c node | less

Практическая методика: возьмите PID интересующего вас сервиса, посмотрите его открытые файлы через lsof -p и пройдитесь по путям глазами — всё, что лежит вне «родного» каталога этого сервиса (/var/www/имя-другого-проекта, /opt/чужой-сервис, общий каталог /data или /srv/shared), кандидат на скрытую связь. Отдельно обратите внимание на файлы с пометкой (deleted) — это открытые дескрипторы на уже удалённые файлы, часто признак старой связи, которая давно должна была умереть, но процесс её всё ещё держит.

Если разовый снимок lsof ничего не показал — а разовый снимок ловит только то, что открыто именно в момент вызова, — переходите к трассировке через strace, которая ловит обращения к файлам в динамике, а не только уже открытые дескрипторы.

# трассировать все системные вызовы, связанные с файлами, у работающего процесса
sudo strace -f -e trace=open,openat,read,write -p <PID> 2>&1 | grep -v ENOENT

# то же самое, но только интересующие пути
sudo strace -f -e trace=openat -p <PID> 2>&1 | grep -E '"/var/www/|"/opt/|"/data/'

strace штука тяжёлая — она замедляет процесс, к которому подключена, поэтому на боевом сервисе держите сессию трассировки короткой (десятки секунд, а не часы) и используйте её как точечный диагностический инструмент, а не постоянный мониторинг. Если процесс обрабатывает запросы редко, сохраните вывод в файл и просмотрите его после, а не следите за терминалом вживую.

Отдельно проверьте общие точки монтирования и каталоги, которые по конвенции считаются «общими»: /tmp, /var/spool, NFS- или bind-монтирование. Если несколько проектов независимо пишут во временный каталог с предсказуемыми именами файлов, это тоже форма связи — не такая явная, как прямое чтение чужого файла, но способная всё сломать, если один из сервисов вдруг начнёт чистить каталог агрессивнее, чем раньше.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Общая база данных: когда два «разных» приложения — на самом деле одно

Второй частый источник — база данных, которую формально держит один сервис, а фактически читают или пишут несколько. Классическая ситуация: интернет-магазин на одном движке и «личный кабинет» на другом, написанные в разное время разными подрядчиками, но кто-то решил не дублировать данные о заказах и подключил вторую систему к той же базе напрямую, в обход API первой. Внешне это два разных сайта. По факту — одна база, изменение схемы в которой уронит оба.

Начать стоит с самой СУБД — она честно покажет, кто к ней подключён прямо сейчас, независимо от того, что написано в документации (если она вообще есть).

-- MySQL/MariaDB: кто подключён и из какого процесса/хоста
SHOW PROCESSLIST;
-- подробнее, с запросами и временем выполнения
SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist;
-- PostgreSQL: активные подключения с указанием клиентского приложения и адреса
SELECT pid, usename, datname, client_addr, application_name, state, query
FROM pg_stat_activity;

Колонка host или client_addr покажет, откуда пришло подключение — если это 127.0.0.1 с разных портов, порт стоит сопоставить с процессом через ss (см. следующий раздел). Колонка user часто выдаёт связь напрямую: если в базе интернет-магазина есть учётная запись reports_readonly или legacy_admin, которую использует явно другой сервис, — вот она, недокументированная зависимость, найденная без единой строчки кода.

Смотрите не только на активные подключения в моменте, но и на общий список пользователей и их права — SELECT user, host FROM mysql.user; в MySQL или \du в psql: учётная запись может существовать и подключаться редко, например раз в сутки при выгрузке, и вы просто не поймаете её в SHOW PROCESSLIST в случайный момент.

Отдельно проверьте права на уровне таблиц, а не только базы целиком — часто «отдельный» сервис имеет доступ лишь к двум-трём таблицам общей базы, и понимание, к каким именно, сильно сужает зону риска при последующих изменениях.

Cron одного сервиса чинит данные другого

Третий источник — расписание. Если вы уже разбирали crontab и системные таймеры на этом сервере, вы знаете, что там редко бывают комментарии. Но помимо очевидных задач вроде бэкапов там встречаются скрипты, формально принадлежащие одному проекту (лежат в его каталоге, запускаются от его пользователя), а по факту правящие данные совершенно другого — досчитывают остатки на складе для магазина, синхронизируют файлы между каталогами разных сайтов, чистят таблицу смежного сервиса.

Найти такие задачи стоит не только через crontab -l по каждому пользователю и systemctl list-timers, но и через содержимое самих скриптов — искать в них пути и обращения, которые выходят за пределы «родного» проекта.

# все cron-задачи всех пользователей системы
for user in $(cut -f1 -d: /etc/passwd); do
  crontab -u "$user" -l 2>/dev/null && echo "-- у пользователя $user выше --"
done

# системные таймеры (если используется systemd)
systemctl list-timers --all

# в самом скрипте ищем обращения за пределы собственного каталога
grep -rnE '/var/www/[a-z0-9_-]+|/opt/[a-z0-9_-]+' /path/to/cron-script.sh | grep -v "$(dirname /path/to/cron-script.sh)"

Если найденный скрипт обращается к другому проекту, полезно на время, не отключая сам cron, добавить в него подробное логирование — что именно он прочитал и что записал, — и понаблюдать за реальной работой пару циклов, прежде чем делать выводы, можно ли его трогать. Иногда скрипт исторически трогал чужой каталог, но перестал находить там нужные файлы ещё год назад и молча завершается без ошибки — такую связь можно считать мёртвой, но подтвердить это стоит логами, а не догадкой.

Отдельная категория риска — задачи, которые не читают, а именно чинят чужие данные: досчитывают то, что основной сервис недосчитал, перекладывают файлы туда, куда основной сервис не смог достучаться сам, ставят заплатки поверх известного бага в другом приложении. Такая cron-задача формально является частью инфраструктуры одного проекта, а фактически — недостающим костылём в логике другого, похожим на безымянный скрипт, который держит продажи, только маскирующимся под расписание, а не под процесс. Отключить её, не разобравшись, — значит вернуть тот самый баг, который она годами тихо закрывала.

Сетевые связи между локальными портами

Кроме файлов и базы, сервисы на одной машине часто разговаривают друг с другом напрямую по сети — через localhost или через локальный сокет — и это тоже связь, которую не видно в статичном списке открытых портов. Один сервис слушает порт, второй к нему стучится как клиент; в списке ss -tlnp вы увидите только того, кто слушает, а того, кто ходит в гости, нужно найти отдельно.

# кто слушает и на каком порту (замена устаревшему netstat)
sudo ss -tlnp

# все установленные TCP-соединения с указанием процессов по обе стороны, локальные в том числе
sudo ss -tnp

# конкретно связи между процессами на 127.0.0.1 — часто именно так общаются
# внутренние API, локальные брокеры очередей и кеши
sudo ss -tnp | grep 127.0.0.1

Если нужно поймать соединение именно в момент, когда оно устанавливается (а не только увидеть уже открытое), полезен tcpdump с фильтром по локальному интерфейсу:

sudo tcpdump -i lo -nn port <порт>

Типичные находки здесь — внутренний API одного сервиса, к которому напрямую, в обход публичного эндпоинта, ходит другой; локальный Redis или Memcached, который считается «кешем сайта А», а на деле в него пишет и сайт Б; очередь сообщений, где продюсер и консьюмер принадлежат формально разным проектам. Сопоставить PID из ss -tnp с проектом можно тем же lsof -p <PID> или через sudo cat /proc/<PID>/cmdline — часто быстрее и однозначнее.

Отдельно проверьте юникс-сокеты — это распространённый способ локального взаимодействия, который вообще не виден в ss -tlnp без флага -x:

sudo ss -xlp

PHP-FPM, слушающий через unix-сокет, к которому обращается nginx другого виртуального хоста, — рабочая, легитимная, но абсолютно недокументированная конфигурация, которую вы иначе найдёте только перечитав все конфиги nginx построчно.

Почему это опасно именно при попытке что-то выключить или обновить

Сама по себе скрытая связь — не проблема, пока никто её не трогает. Опасность возникает в двух конкретных ситуациях: когда вы что-то выключаете и когда вы что-то обновляете, и в обоих случаях механизм провала одинаковый — вы оцениваете риск по документированным зависимостям (той же карте сервисов, которую строили изначально), а реальная зависимость в неё не попала.

При отключении сервиса риск очевиден: вы гасите то, что считали балластом, а на деле это был источник данных, файла или сообщения, на которое молча полагается другой, внешне не связанный сервис. Сбой проявляется не сразу и не в том месте, где вы действовали — файл перестаёт обновляться, второй сервис работает со старыми данными, пока кто-то не заметит расхождение через день или неделю. Отладка такого инцидента медленнее и дороже, чем немедленный явный сбой: связь между причиной (вы выключили сервис А в понедельник) и следствием (в пятницу отчёты сервиса Б не обновлялись всю неделю) уже не очевидна, и её приходится восстанавливать заново теми же методами.

При обновлении риск тоньше: вы меняете версию библиотеки, формат файла, схему таблицы или порт, будучи уверены, что затрагиваете только один проект. Но если формат общего файла изменился, а второй сервис парсит его регулярным выражением под старую структуру, — парсинг начнёт молча возвращать мусор, без единой ошибки в логах. Если схема таблицы в общей базе поменялась, а второй сервис обращается к ней напрямую в обход первого — он либо упадёт с ошибкой SQL, что хотя бы заметно, либо продолжит писать с преобразованием типа «на лету», тихо портя данные.

Практический вывод простой: прежде чем решать, что выключить, а что держит бизнес, стоит потратить час на три проверки из этой статьи — lsof на его открытые файлы, проверку подключений к его базе, если она у него есть, и ss/lsof на его сетевые связи. Это не гарантирует, что вы найдёте всё, но резко снижает шанс сюрприза, который проявится через дни, а не сразу. Если после этой проверки риск всё ещё непонятен, разумная тактика — не выключать сервис резко, а сначала ограничить его (например, запретить новые подключения к порту через firewall, оставив сам процесс работать) и понаблюдать несколько дней за тем, кто пытается достучаться и получает отказ — это даст более честную картину зависимостей, чем любой статический анализ.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Сколько времени занимает такая проверка перед отключением одного сервиса?

Базовый проход — lsof по PID, подключения к БД, ss по портам — обычно час-полтора для одного сервиса, если понимаете, где искать. Полная ревизия всех сервисов на машине займёт заметно дольше и делается не за один присест.

strace можно оставлять работающим долго для мониторинга связей?

Не рекомендуется на нагруженных процессах — трассировка системных вызовов заметно замедляет процесс. Для постоянного наблюдения лучше подходят короткие точечные сессии strace плюс регулярные снимки lsof, а не непрерывная трассировка.

Что делать, если найденная связь выглядит критичной, но автора или документации нет вообще?

Не разрывайте её сразу. Сначала подтвердите логированием, что связь активна прямо сейчас, а не осталась от старой версии проекта, затем изолируйте риск постепенно — сделайте резервную копию затрагиваемых данных и меняйте конфигурацию поэтапно, с возможностью откатить один шаг.

Юникс-сокеты и общие файлы — это единственные типы скрытых связей?

Нет, самые частые, но не единственные. Встречаются также общие очереди сообщений, точки монтирования (NFS, bind mount), разделяемая память, общие сертификаты и ключи, хардкоженные IP-адреса самого сервера в чужих конфигах.

Есть ли способ автоматизировать поиск таких связей?

Частично — можно написать скрипт, который для каждого процесса снимает lsof -p и сравнивает открытые пути с ожидаемым «родным» каталогом сервиса, помечая всё лишнее. Это не заменит проверку сети и базы, но снимает рутину по файловым связям и даёт список кандидатов для ручного разбора.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →