АйТи Фреш
Главная / Статьи / Серверы и хостинг
Серверы и хостинг

Как привязать службу systemd к отдельному тому

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~16 мин чтения
Как привязать службу systemd к отдельному тому
Иллюстрация к статье «Как привязать службу systemd к отдельному тому».

Знакомая авария: диск не подключился, каталог остался, приложение спокойно записало десятки гигабайт в корневой раздел. Я закрываю эту проблему тремя рубежами: systemd сначала монтирует том, останавливает службу при его исчезновении, а права на пустую точку монтирования физически запрещают запись мимо диска.

Почему существующий каталог ничего не доказывает

Точка монтирования — это обычный каталог на корневой файловой системе. Пока отдельный том подключён, его корень закрывает содержимое этого каталога. После размонтирования старый каталог снова становится видимым. Путь тот же, поэтому приложение не замечает подмены и продолжает писать. Именно так системный диск заполняется файлами, которые затем загадочно исчезают после повторного подключения тома: они не удалены, а спрятаны под новым монтированием.

Проверка через test -d бесполезна: каталог существует в обоих состояниях. Команда df -h /srv/archive тоже требует внимания — без тома она честно покажет корневую файловую систему, но скрипт должен ещё проверить источник и точку монтирования. Для однозначной диагностики я использую findmnt или mountpoint.

findmnt --mountpoint /srv/archive --output TARGET,SOURCE,FSTYPE,OPTIONS
mountpoint -q /srv/archive
echo $?

Код возврата 0 у mountpoint означает, что путь действительно является точкой монтирования. Но это только моментальный снимок. Через секунду устройство может исчезнуть, поэтому одной проверки перед запуском мало.

Я не строю решение вокруг After=local-fs.target, network-online.target или задержки через sleep 30. After= задаёт порядок, но само по себе не требует успешного запуска указанного юнита. Таймер всего лишь переносит гонку. Проверка ConditionPathIsMountPoint= лучше таймера, но выполняется только при активации службы и не останавливает уже работающий процесс. Нужна настоящая зависимость от конкретного mount-юнита.

Наличие каталога и наличие смонтированной файловой системы — разные состояния. Никогда не используйте `test -d` как проверку готовности хранилища.

Моя схема: требование, привязка и запрет записи

Первый рубеж — RequiresMountsFor=/srv/archive. systemd вычисляет все mount-юниты, необходимые для доступа к пути, добавляет к ним зависимости Requires= и After= и пытается поднять их до запуска приложения. Директива учитывает даже запись noauto в fstab. Если диск отсутствует или файловая система не монтируется, основной процесс службы не стартует.

Второй рубеж — пара BindsTo=srv-archive.mount и After=srv-archive.mount. Обычный Requires= хорошо обрабатывает явную остановку зависимого юнита, но не гарантирует реакцию на любое неожиданное исчезновение. BindsTo= сильнее: если mount-юнит внезапно становится неактивным, зависимая служба тоже останавливается. After= здесь принципиален — официальная семантика systemd именно для этой пары требует, чтобы привязанный юнит оставался активным всё время работы службы.

Третий рубеж не относится к графу systemd и потому особенно важен: пустую точку монтирования я оставляю владельцу root:root с режимом 000, а приложение запускаю отдельным непривилегированным пользователем без capabilities. На подключённом томе каталог spool имеет права пользователя приложения. После размонтирования проявляется закрытый каталог системного диска, и запись получает Permission denied, даже если остановка процесса заняла небольшое время. Зависимости управляют жизненным циклом, а права обеспечивают отказобезопасность.

systemd-зависимость — не механизм разграничения доступа. Короткое окно между размонтированием и остановкой возможно, поэтому я всегда закрываю права на нижележащий каталог.
Как привязать службу systemd к отдельному тому — схема
Схема к статье. Открыть схему в полном размере

Настраиваем том и защищаем пустую точку

На сервере я сначала получаю UUID через blkid, затем создаю точку монтирования до подключения тома. UUID в примере условный: на своём сервере подставьте значение из вывода blkid, копировать его вслепую нельзя. Режим 000 не мешает root смонтировать файловую систему поверх каталога.

sudo blkid /dev/mapper/vg_archive-lv_archive
sudo install -d -o root -g root -m 000 /srv/archive

В /etc/fstab запись выглядела так:

UUID=7b15c3d9-00c8-4ca2-b39d-d21bc859f156 /srv/archive ext4 defaults,nofail,noauto,x-systemd.device-bound,x-systemd.rw-only,x-systemd.device-timeout=20s,x-systemd.mount-timeout=30s 0 2

Я использовал noauto, чтобы том поднимался по запросу службы, а не просто потому, что стартовал local-fs.target. nofail позволяет серверу загрузиться без хранилища. Это не ослабляет прямое требование приложения: RequiresMountsFor= всё равно запросит mount-юнит. x-systemd.device-bound явно привязывает mount-юнит к блочному устройству, а x-systemd.rw-only не позволяет незаметно принять резервное монтирование только для чтения. Тайм-ауты ограничивают ожидание отсутствующего устройства.

После изменения fstab я перечитываю конфигурацию, запускаю сгенерированный mount-юнит и создаю рабочий каталог уже внутри подключённой файловой системы. Владельцем всей точки делать пользователя приложения не требуется — достаточно выдать ему нужный подкаталог.

sudo systemctl daemon-reload
sudo systemctl start srv-archive.mount
sudo findmnt --mountpoint /srv/archive
sudo install -d -o archiver -g archiver -m 0750 /srv/archive/spool
systemd-escape --path --suffix=mount /srv/archive

Последняя команда возвращает srv-archive.mount. Для путей с дефисами и специальными символами имя нельзя угадывать: systemd-escape выполнит правильное экранирование.

Перед изменением прав убедитесь через `findmnt`, что том не подключён. Иначе `chmod 000 /srv/archive` изменит права корня отдельной файловой системы, а не защитного каталога под ней.
Порядок действий: Настраиваем том и защищаем пустую точку — схема
Порядок действий: Настраиваем том и защищаем пустую точку. Открыть схему в полном размере

Drop-in для службы: готовая конфигурация

Vendor unit я не редактирую: обновление пакета затрёт изменения. Создаю drop-in командой systemctl edit dispatch-archiver.service. Для нашего пути итоговое дополнение было таким:

[Unit]
RequiresMountsFor=/srv/archive
BindsTo=srv-archive.mount
After=srv-archive.mount

[Service]
User=archiver
Group=archiver
ExecStartPre=/usr/bin/mountpoint -q /srv/archive
ProtectSystem=strict
ReadWritePaths=/srv/archive
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=

ExecStartPre здесь не заменяет зависимости, а служит последней понятной проверкой. Если конфигурацию кто-нибудь сломает, в журнале останется явный отказ до запуска приложения.

ProtectSystem=strict переводит видимую службе файловую систему в режим только для чтения, а ReadWritePaths=/srv/archive оставляет разрешённое окно для данных. Если программа отдельно пишет PID, сокет или файловый лог, соответствующие пути надо оформить через RuntimeDirectory=, StateDirectory= и LogsDirectory= либо добавить точечные исключения. Не советую разрешать целиком /var — такое исключение быстро превращает изоляцию в декорацию.

Пустые CapabilityBoundingSet= и AmbientCapabilities= убирают capabilities, включая возможность обходить обычные ограничения доступа. Это подходит не каждой программе: демон, который сам монтирует файловые системы, меняет сетевые настройки или открывает привилегированный ресурс, потребует отдельного разбора. Но архиватору, загрузчику документов или обработчику файлов root обычно не нужен. Если поставщик требует запуск от root без убедительной причины, я сначала исправляю это, а не пытаюсь компенсировать риск десятком условий systemd.

Не добавляйте `Restart=always` в надежде, что служба сама дождётся диска. Частые попытки запуска маскируют причину, засоряют журнал и могут упереться в start limit.

Практика: архив «СкороДома» без скрытой записи в root

Покажу обезличенный сценарий, технические величины округлены. Служба доставки «СкороДом» — 23 рабочих места: диспетчеры, логисты, бухгалтерия и курьеры с мобильным приложением. Внутренняя система складывала в архив электронные накладные, фотографии вручения и подписи получателей. Виртуальная машина имела 4 vCPU, 8 Гбайт RAM, системный ext4-раздел 60 Гбайт и отдельный LVM-том ext4 объёмом 1 Тбайт. ОС — Ubuntu Server 24.04 LTS, systemd 255. Служба dispatch-archiver создавала в среднем 3–5 Гбайт данных в сутки, но после восстановления связи с курьерскими терминалами могла быстро выгрузить накопившуюся очередь.

До исправления в unit стояло только After=network-online.target, а /srv/archive принадлежал пользователю приложения. Во время обслуживания гипервизора диск с архивом подключился к виртуальной машине на 37 минут позже её старта. Архиватор запустился сразу, принял очередь за выходные и записал 14 Гбайт в системный раздел. После подключения тома файлы «исчезли» под точкой монтирования, однако занятое место осталось. Администратор сначала искал удалённые, но открытые файлы через lsof, хотя причина была проще: данные лежали в нижнем каталоге root filesystem.

Я перенёс управление монтированием в fstab, добавил к службе RequiresMountsFor=, BindsTo= и After=, запустил её от пользователя archiver, закрыл пустую точку режимом 000 и включил ProtectSystem=strict. На копии ВМ провели три испытания: загрузка без LUN, штатная остановка mount-юнита под нагрузкой и ленивое размонтирование через umount -l. В первом случае приложение не запустилось; во втором systemd сначала остановил службу и затем том; в третьем служба стала неактивной менее чем за секунду по отметкам журнала, а попытка нового файла на системном диске завершилась отказом в доступе.

После возврата хранилища мы запускали mount-юнит и приложение вручную — сознательно. Автоматический рестарт сразу после появления устройства опасен, если ext4 проходит проверку, LVM собран не полностью или удалённое хранилище отвечает с ошибками. За следующие 60 дней контрольный поиск не обнаружил ни одного файла под защитной точкой, корневой раздел держался в диапазоне 31–36 %, а аварийное заполнение больше не повторялось.

Команду `umount -l` нельзя бездумно испытывать на рабочем хранилище: она отсоединяет путь, но ссылки на файловую систему могут оставаться у процессов. Такой тест проводите на стенде или клоне ВМ.
Цифры и версии: Практика: архив «СкороДома» без скрытой записи в root — схема
Цифры и версии: Практика: архив «СкороДома» без скрытой записи в root. Открыть схему в полном размере

Сетевые и iSCSI-тома: _netdev, automount и AssertPathIsMountPoint=

Если архив лежит не на виртуальном диске, а на iSCSI-LUN или NFS, к зависимостям добавляется сеть. Для NFS и CIFS systemd сам понимает, что файловая система сетевая, по типу из fstab. Для ext4 или XFS поверх iSCSI этого не видно, и без опции _netdev генератор посчитает монтирование локальным: systemd попытается смонтировать его в фазе local-fs.target, когда сеть и iSCSI-сессия ещё не подняты. С _netdev mount-юнит становится сетевым: он упорядочивается между remote-fs-pre.target и remote-fs.target, подтягивает network-online.target и стартует после него и network.target.

UUID=00000000-0000-0000-0000-000000000000 /srv/archive xfs defaults,_netdev,nofail,x-systemd.device-timeout=60s 0 0

nofail работает одинаково для локальных и сетевых точек: монтирование становится желательным, а не обязательным для local-fs.target или remote-fs.target, и загрузка не ждёт его. На требование службы через RequiresMountsFor= это никак не влияет.

x-systemd.automount создаёт рядом с mount-юнитом automount-юнит: точка монтирования перехватывается autofs, а реальное монтирование происходит при первом обращении к пути. Для рабочих станций и редко используемых шар это удобно, но для службы, которая обязана писать только на том, я его не люблю. Во-первых, RequiresMountsFor= всё равно тянет сам .mount, поэтому ленивости для этой службы не будет. Во-вторых, сочетание с x-systemd.idle-timeout= опасно: после простоя systemd размонтирует том, mount-юнит станет неактивным, и BindsTo= законно остановит службу. Если automount нужен другим потребителям, не задавайте ему idle-таймаут на пути, к которому привязан сервис.

Вместо ConditionPathIsMountPoint= иногда уместнее AssertPathIsMountPoint=. Разница в громкости: невыполненное условие просто пропускает запуск, и в мониторинге служба выглядит «спокойно неактивной». Невыполненное утверждение проваливает задание запуска с явной записью в журнале; при этом, по документации, сам юнит не переходит в состояние failed — ошибку получает только задание. Поэтому алерт я строю по журналу и по активности службы, а не только по failed. Ни одна из этих проверок не поднимает том и не отслеживает его дальше, так что основой остаются RequiresMountsFor= и BindsTo=.

[Unit]
RequiresMountsFor=/srv/archive
BindsTo=srv-archive.mount
After=srv-archive.mount
AssertPathIsMountPoint=/srv/archive
`AssertPathIsMountPoint=` при срабатывании не переводит службу в `failed`. Если мониторинг смотрит только на `systemctl --failed`, такой отказ запуска останется незамеченным — добавьте проверку `is-active` и поиск ошибок в журнале.

Как проверить результат и что поставить на мониторинг

Сначала я останавливаю приложение и mount-юнит в согласованном окне, затем проверяю запрет записи от имени сервисного пользователя. Файл появиться не должен.

sudo systemctl stop dispatch-archiver.service
sudo systemctl stop srv-archive.mount
sudo -u archiver touch /srv/archive/must-not-appear
sudo findmnt --mountpoint /srv/archive

Ожидаемый результат touchPermission denied, а findmnt не должен находить точку. Если файл создался, не переходите дальше: права нижнего каталога или пользователь службы настроены неправильно.

Затем проверяю положительный сценарий. Запуск одной службы должен подтянуть том; отдельный ручной mount -a ей не требуется.

sudo systemctl start dispatch-archiver.service
systemctl is-active srv-archive.mount dispatch-archiver.service
findmnt --mountpoint /srv/archive --output TARGET,SOURCE,FSTYPE
journalctl -u srv-archive.mount -u dispatch-archiver.service --since -10min

После этого останавливаю srv-archive.mount и убеждаюсь, что служба также перешла в inactive. Благодаря обратному порядку After= systemd при управляемой остановке сначала завершает приложение, что даёт ему возможность сбросить буферы.

В мониторинге я разделяю три сигнала: mount-юнит неактивен, источник у /srv/archive не соответствует ожидаемому UUID, корневой раздел растёт. Одной метрики свободного места недостаточно. Для проверки источника удобно использовать стабильный вывод findmnt, явно задавая поля, например findmnt -rn --mountpoint /srv/archive -o UUID. Отдельно ставлю тревогу на состояние failed службы, но не включаю бесконечный автоматический рестарт.

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

Правильный критерий — не «каталог доступен», а «нужная файловая система смонтирована, служба связана с её unit и не имеет прав писать в нижний каталог».
Порядок действий: Как проверить результат и что поставить на мониторинг — схема
Порядок действий: Как проверить результат и что поставить на мониторинг. Открыть схему в полном размере

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

Достаточно ли добавить ConditionPathIsMountPoint=/srv/archive?

Нет. Условие проверяется при активации, но не поднимает том и не отслеживает его дальнейшее состояние. Его можно оставить дополнительной проверкой, однако основой должны быть `RequiresMountsFor=` и связка `BindsTo=` с `After=`.

Чем AssertPathIsMountPoint= отличается от ConditionPathIsMountPoint=?

Условие молча пропускает запуск, утверждение проваливает задание запуска с записью в журнале. Юнит при этом в `failed` не переходит — это меняет только результат задания. Оба варианта проверяют путь лишь в момент старта и том не монтируют, поэтому это дополнение к `RequiresMountsFor=` и `BindsTo=`, а не замена.

Почему не ограничиться RequiresMountsFor=?

Директива отлично решает запуск и управляемую остановку, но создаёт зависимости уровня `Requires=`. Для неожиданной деактивации mount-юнита официальная документация рекомендует более сильный `BindsTo=` вместе с `After=`.

Запустится ли служба сама после возвращения диска?

Обычно нет: остановка из-за потери зависимости не означает бесконечный рестарт. Я считаю это безопасным поведением. Сначала проверяю LVM, файловую систему и `findmnt`, затем запускаю `srv-archive.mount` и службу. Автоматизацию возврата добавляю только вместе с отдельной проверкой здоровья хранилища.

Что делать, если приложение обязательно работает от root?

Сначала проверить, действительно ли ему нужен root, и выделить минимальные capabilities. Режим `000` сам по себе не считается надёжной защитой от привилегированного процесса. Для такого приложения нужны отдельное mount namespace, жёсткий `CapabilityBoundingSet=`, `ProtectSystem=strict` и тест фактической невозможности записи после размонтирования.

Подходит ли схема для NFS?

Для порядка запуска и управляемого размонтирования — да, имя mount-юнита формируется так же, а сетевой тип NFS systemd определяет сам и ставит монтирование после `network-online.target`. Для блочных томов по iSCSI то же поведение включает опция `_netdev`. Но недоступный NFS-сервер может оставить файловую систему формально смонтированной. Поэтому дополнительно нужны контроль реальной операции ввода-вывода, сетевые тайм-ауты и мониторинг задержки.

Столкнулись с похожей задачей? Обращайтесь — решим

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

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи