Как подключить storage VPS к основному VPS
Запустите приложение на быстром VPS, а терабайты храните на storage VPS. Настройте NFS, SSHFS или rclone через WireGuard и переживите потерю mount.
Зачем объединять storage VPS с основным VPS
Объедините storage VPS с основным VPS, чтобы каждый сервер выполнял задачу, для которой он оптимизирован по стоимости. Небольшой быстрый сервер запускает приложение, его базу данных и cache. Большой недорогой сервер хранит терабайты данных и обслуживает запросы на чтение. Приватный туннель WireGuard соединяет серверы, а приложение видит удалённый диск как обычный каталог в /mnt.
Главная причина такого разделения — стоимость гигабайта. Storage VPS жертвует производительностью CPU и IOPS ради большой ёмкости, поэтому за те же ежемесячные расходы вы получаете значительно больше места, если не оплачиваете быстрые вычислительные ресурсы рядом с каждым терабайтом. Если вы ещё не выбрали продукт для хранения данных, сначала изучите сравнение storage VPS с block volumes и object storage, поскольку варианты объединения ниже зависят от выбранного продукта.
Цена такого разделения — один сетевой обмен перед каждой файловой операцией. Локальный stat() — это обращение к памяти. Такой же stat() на удалённом mount — это запрос, ожидание и ответ. Ни одна опция mount не устраняет этот сетевой обмен. Можно только уменьшить количество таких операций, которые выполняет приложение.
От двух решений зависит, будет ли такая схема работать вообще. Оба сервера должны находиться в одном регионе. Базы данных, cache и временные файлы для транскодирования должны оставаться на локальном диске. Если ошибиться хотя бы в одном из этих пунктов, дальнейшие инструкции уже не исправят ситуацию.
Почему оба сервера должны находиться в одном регионе
Весь доступный бюджет определяет время прохождения запроса туда и обратно. Внутри одного дата-центра время прохождения запроса между двумя VPS занимает доли миллисекунды. Между двумя континентами этот показатель более чем в 100 раз выше, и это увеличение влияет на каждую операцию, а не только на медленные.
Рассмотрим работу сканера медиатеки. Он читает каталог, затем вызывает stat() для каждой записи и открывает некоторые файлы, чтобы прочитать встроенные метаданные. Поэтому медиатека из 10000 файлов требует как минимум 10000 обменов данными туда и обратно ещё до передачи видеоданных. Проблема не в пропускной способности. Передача одного большого файла по каналу с большой задержкой обычно работает нормально, поскольку операции чтения выполняются большими последовательными блоками, а ядро заранее загружает данные. Проблема возникает при обходе дерева каталогов: каждая небольшая операция ждёт ответа на предыдущую.
Итак: один провайдер, один регион, а в идеале — один дата-центр. Проверьте это до начала миграции. Арендуйте оба сервера на один месяц, настройте туннель и измерьте задержку с помощью ping -c 100 10.9.0.2. Если среднее значение не составляет небольшую долю миллисекунды, не создавайте такую пару. Разместите приложение рядом с данными или используйте объектное хранилище, где доступ выполняется одним большим запросом за раз.
Что нельзя размещать на удалённой стороне
Четыре вида файлов должны находиться на локальном диске сервера приложений.
- Файлы базы данных. SQLite, PostgreSQL и MariaDB требуют, чтобы
fsync()имел именно заявленное значение, а блокировка файлов корректно работала между процессами. При использовании удалённого монтирования это зависит от удалённой стороны и состояния сети. Если соединение задерживается, база данных не приостанавливает работу корректно. Она зависает или повреждается. - Кэши, индексы и миниатюры. Это небольшие файлы, которые постоянно читаются и недороги в восстановлении. Размещать их за сетевым обменом — худшее из возможных решений. Каждая страница галереи превращается в серию небольших удалённых операций чтения.
- Временные файлы транскодирования. Транскодер часто записывает и удаляет небольшие блоки данных. Укажите для временного каталога локальный диск и проверьте, что настройка действительно изменилась.
- Файлы блокировок и Unix-сокеты. Файл
.pidили сокет в общем каталоге ничего не означает для двух разных машин, а некоторые удалённые файловые системы обрабатывают их некорректно.
На удалённой стороне следует хранить большие файлы, которые записываются один раз и читаются целиком: видео, оригиналы фотографий, архивы и репозитории резервных копий.
Для медиасервера разделение очевидно. Jellyfin, работающий на VPS хранит конфигурацию и метаданные в /var/lib/jellyfin на локальном диске, а видео читает с монтирования. Если переместить /var/lib/jellyfin на сервер хранения, каждый щелчок в веб-интерфейсе будет ждать удалённую сторону. Для фотографий действует тот же принцип: при использовании Immich библиотека загрузок и база данных Postgres являются разными компонентами. Библиотека может находиться на монтировании. База данных — нет.
Соединение двух серверов с помощью WireGuard
Не экспортируйте файловую систему на публичный IP-адрес. NFS по умолчанию не имеет шифрования транспорта, а его аутентификация в основном сводится к «доверию числовому идентификатору пользователя, который присылает клиент». Поместите его внутрь туннеля.
Полная настройка описана в руководстве по самостоятельному размещению WireGuard на VPS. Ниже приведена версия для двух узлов. Создайте пару ключей на каждом сервере:
sudo apt update && sudo apt install -y wireguard
sudo install -d -m 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/priv.key'
sudo sh -c 'wg pubkey < /etc/wireguard/priv.key > /etc/wireguard/pub.key'На сервере приложений выполните /etc/wireguard/wg0.conf:
[Interface]
Address = 10.9.0.1/24
ListenPort = 51820
PrivateKey = <application box private key>
[Peer]
PublicKey = <storage box public key>
AllowedIPs = 10.9.0.2/32
Endpoint = <storage box public IP>:51820
PersistentKeepalive = 25На сервере хранения выполните аналогичную настройку: укажите Address = 10.9.0.2/24, в параметре AllowedIPs = 10.9.0.1/32 укажите адрес пира, а в endpoint — публичный адрес сервера приложений. Оба файла должны иметь права доступа 600. Затем поднимите туннель на обоих серверах:
sudo chmod 600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
sudo wg show
ping -c 3 10.9.0.2Команда wg show должна показать пира с недавней строкой latest handshake, а пинг должен отвечать. Если строка handshake отсутствует, значит, пакеты не доходят. Проверьте порт UDP 51820 на брандмауэрах обоих хостов, а также на отдельном сетевом брандмауэре вашего провайдера, который в большинстве панелей управления настраивается отдельно.
В этой схеме могут возникнуть две проблемы с туннелем, о которых стоит знать заранее. Большие последовательные операции чтения, которые зависают, пока ping продолжает работать, указывают на проблему с MTU пути. Решение заключается в поиске реального MTU пути методом бисекции вместо подбора круглых чисел. А если вы обращаетесь к серверу хранения по имени хоста, а не по его адресу в туннеле, разрешение DNS через туннель станет еще одной точкой отказа при загрузке. Используйте буквальный адрес 10.9.0.2 в каждом определении монтирования, и эта проблема никогда не возникнет.
Вариант 1: NFS через туннель
NFS — оптимальный вариант для файловой системы в том же регионе. Клиент ядра кэширует атрибуты, а поведение при сбоях можно настраивать, а не принимать как фиксированное.
На storage box:
sudo apt update && sudo apt install -y nfs-kernel-server
sudo install -d -m 755 /srv/media
sudo chown 1000:1000 /srv/mediaДобавьте одну строку в /etc/exports. В ней укажите tunnel address application box:
/srv/media 10.9.0.1(rw,sync,no_subtree_check,root_squash)Примените изменения и проверьте их:
sudo exportfs -ra
sudo exportfs -vКоманда exportfs -v выводит параметры, которые ядро действительно применило. Они не всегда совпадают с введёнными параметрами, поскольку вокруг указанного списка добавляются значения по умолчанию.
Строка экспорта ограничивает адрес клиента, которому разрешено монтирование. Однако NFS server по-прежнему слушает TCP 2049 на всех интерфейсах. Сканирование портов по public address обнаружит этот порт. Список экспортов не заменяет firewall, поэтому добавьте правило:
sudo ss -lnt | grep 2049
sudo ufw status verbose
sudo ufw allow in on wg0 to any port 2049 proto tcpКоманда ss показывает listener, привязанный ко всем адресам. Именно это и требуется. Команда ufw status verbose должна вывести Default: deny (incoming). При таком значении по умолчанию одного правила для туннеля достаточно: порт 2049 отвечает на wg0 и нигде больше. Если значение по умолчанию равно allow, исправьте его, прежде чем продолжать.
NFSv4 без Kerberos сопоставляет владельцев по числовому user id. Если приложение работает с uid 1000 на одном компьютере, а файлы принадлежат uid 1000 на другом, всё работает. Если значения различаются, файлы отображаются принадлежащими nobody или запись завершается ошибкой Permission denied. Никакое количество chmod не поможет. Синхронизируйте ids на обоих компьютерах или добавьте all_squash,anonuid=1000,anongid=1000 в export, чтобы доступ каждого клиента выполнялся от имени одного владельца.
На application box смонтируйте файловую систему:
sudo apt install -y nfs-common
sudo install -d -m 755 /mnt/media
sudo mount -t nfs4 -o vers=4.2,nconnect=4 10.9.0.2:/srv/media /mnt/media
findmnt /mnt/mediaКоманда findmnt выводит источник, тип и действующие параметры. Затем добавьте запись в /etc/fstab и включите automount, чтобы загрузка при недоступном туннеле не зависала:
10.9.0.2:/srv/media /mnt/media nfs4 _netdev,x-systemd.automount,x-systemd.mount-timeout=30,x-systemd.idle-timeout=600,vers=4.2,nconnect=4,hard,timeo=100,retrans=3,acdirmin=60,acdirmax=600 0 0sudo systemctl daemon-reload
sudo systemctl start mnt-media.automount
systemctl status mnt-media.automountИмя unit определяется из пути: /mnt/media становится mnt-media.automount. При использовании x-systemd.automount systemd устанавливает при загрузке только trigger. Фактическое монтирование выполняется при первом обращении, поэтому медленная загрузка storage box не задержит загрузку application box.
Нужно понимать назначение трёх параметров, а не просто копировать их. nconnect=4 открывает несколько TCP connections к одному server, но не более документированного значения 16. Это помогает, когда окно одной connection ограничивает throughput на канале с заметной задержкой. Параметры acdirmin и acdirmax задают, как долго client доверяет кэшированным атрибутам каталогов, прежде чем запросить их снова. Увеличение этих значений по сравнению с короткими значениями по умолчанию — наиболее эффективное изменение для сканирования большого числа metadata. Компромисс заключается в том, что файл, добавленный на storage box другим процессом, станет виден с задержкой. Если файлы всегда поступают только через application box, этот компромисс ничего вам не стоит: при записи client обновляет собственный кэш. А noatime здесь ничего не делает, несмотря на утверждение в blog post, из которого вы скопировали строку: man page nfs прямо указывает, что параметры atime и noatime не влияют на NFS mounts.
Вариант 2: SSHFS
Для работы SSHFS на сервере хранения не требуется ничего, кроме SSH-аккаунта. Она работает в пространстве пользователя через FUSE, и каждая файловая операция, которую не удается выполнить из кэша, превращается в SFTP-запрос внутри SSH-соединения.
Понимайте, что вы используете. По состоянию на октябрь 2026 года в README проекта указано, что у него «нет активных постоянных участников, и существует ряд известных проблем», хотя мейнтейнер по-прежнему принимает pull-запросы и выпускает релизы. Пакет доступен во всех основных дистрибутивах и работает. Относитесь к нему как к стабильному, но не развивающемуся продукту.
sudo apt install -y sshfs
sudo install -d -m 755 /mnt/media
sshfs -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,uid=1000,gid=1000,allow_other,default_permissions storage@10.9.0.2:/srv/media /mnt/media
ls /mnt/mediaРазмонтируйте файловую систему с помощью fusermount -u /mnt/media, а не umount. Опции uid и gid преобразуют удаленные идентификаторы владельцев в локальные, что позволяет избежать проблемы несовпадения ID, характерной для NFS. reconnect вместе с двумя настройками keepalive позволяет точке монтирования пережить кратковременный разрыв туннеля. Без них любое прерывание соединения навсегда «убивает» точку монтирования, и при каждом обращении будет возвращаться Transport endpoint is not connected, пока вы не размонтируете и не смонтируете ресурс заново.
Для записи в fstab требуется ключ без парольной фразы, доступный для чтения пользователю root:
storage@10.9.0.2:/srv/media /mnt/media fuse.sshfs _netdev,x-systemd.automount,IdentityFile=/root/.ssh/id_ed25519,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,uid=1000,gid=1000,allow_other,default_permissions 0 0В чем заключаются недостатки: кэш метаданных живет недолго. SSHFS поддерживает собственный кэш имен, атрибутов и целей символических ссылок, который по умолчанию имеет время жизни 20 секунд (dcache_timeout), а FUSE добавляет поверх него кэш атрибутов ядра длительностью в одну секунду. Внутри этого окна повторные запросы остаются локальными. По истечении этого времени каждый запрос снова требует сетевого взаимодействия, которое по умолчанию выполняется через одно SSH-соединение (max_conns позволяет открыть больше). Длительное сканирование, на которое NFS-клиент отвечает из кэша столько времени, сколько позволяет ваш acdirmax, в SSHFS будет повторяться по сети каждые 20 секунд, если только вы не увеличите dcache_timeout и не согласитесь на задержку в отображении изменений на удаленной стороне. Кроме того, вы выполняете двойное шифрование — один раз для SSH и один раз для WireGuard, что является бесполезной тратой ресурсов процессора на маломощном устройстве. Это указывает на оправданную альтернативу: откажитесь от туннеля и используйте SSHFS через публичный адрес сервера хранения, так как SSH безопасно выставлять наружу, в отличие от NFS. Выберите один вариант архитектуры. Не используйте оба одновременно.
Вариант 3: монтирование через rclone
rclone также монтирует хранилища через FUSE. В качестве удалённого хранилища он может использовать SFTP на сервере хранения или bucket объектного хранилища в другом месте. Это правильный выбор, если удалённая сторона — объектное хранилище. Но если удалённая сторона — файловая система, которую можно нормально смонтировать, rclone использовать не следует.
sudo apt install -y rclone
rclone config
rclone lsd storage:rclone lsd позволяет проверить настройку удалённого хранилища до попытки монтирования. Точка монтирования в Linux должна быть существующим пустым каталогом, что прямо указано в документации rclone.
rclone mount storage:media /mnt/media --vfs-cache-mode full --vfs-cache-max-size 20G --dir-cache-time 72h --allow-other --daemon--vfs-cache-mode full обеспечивает работу обычных приложений с монтированным хранилищем и записывает локальный кэш. Этот кэш размещается на небольшом быстром диске, который вы старались не заполнять. Поэтому --vfs-cache-max-size не является необязательным параметром. --dir-cache-time 72h хранит списки каталогов в памяти в течение трёх дней. Благодаря этому можно просматривать большую библиотеку, но файл, добавленный другим процессом на удалённой стороне, может не отображаться в течение трёх дней. Для юнита systemd в документации указано, что Type=notify переводит сервис в состояние started только после подготовки точки монтирования. Именно на это состояние должны ориентироваться другие юниты при настройке порядка запуска.
Проблемы начинаются из-за отсутствия семантики POSIX. В объектном хранилище переименование выполняется как копирование с последующим удалением. Небольшая запись в середину большого существующего файла приводит к перезаписи всего объекта. Приложения, которые предполагают наличие настоящей файловой системы, включая любой media server, могут работать непредсказуемо. Документация не сможет заранее описать такое поведение для каждого приложения. rclone подходит для чтения больших файлов и для использования удалённого сервера как цели резервного копирования вне основной площадки. Но использовать его как диск общего назначения не следует.
Поведение приложения при разрыве соединения
Определите это до начала настройки, так как поведение по умолчанию часто не соответствует ожиданиям.
При использовании NFS hard, что является настройкой по умолчанию, разрыв соединения переводит все процессы, обращающиеся к точке монтирования, в состояние сна до тех пор, пока сервер не ответит. ps показывает состояние D, а df зависает. Если ваша система мониторинга запускает df, мониторинг также зависнет. Однако этот D не является «неубиваемым»: начиная с ядра 2.6.25, NFS-клиент ожидает в состоянии killable sleep, поэтому kill -9 завершает зависший процесс, и это способ очистить процесс, который вы не можете ждать. Все данные, которые процесс не успел записать, будут потеряны. Ядро записывает nfs: server 10.9.0.2 not responding, still trying в логи. Когда соединение восстанавливается, вы получаете nfs: server 10.9.0.2 OK, и все процессы, которые вы не трогали, продолжают работу с того же места без потери данных. Это правильный компромисс для любых операций записи.
При использовании NFS soft запросы завершаются ошибкой ввода-вывода (I/O error) после того, как исчерпаны timeo и retrans, и приложение получает EIO вместо ожидания. Предупреждение из man-страницы стоит процитировать дословно, так как оно серьезнее, чем многие предполагают: soft timeout «может привести к скрытому повреждению данных в определенных случаях», поэтому используйте его «только тогда, когда отзывчивость клиента важнее целостности данных». Монтирование носителя только для чтения — это такой случай. Путь для записи — нет. Не пытайтесь использовать intr, чтобы смягчить hard mount. В man-странице указано, что этот параметр игнорируется начиная с ядра 2.6.25 — того самого релиза, где killable sleep заменил его, поэтому в вашем fstab это лишь комментарий, не более того.
softreval занимает промежуточное положение. Он позволяет клиенту продолжать предоставлять пути и атрибуты из кэша после того, как попытки ревалидации завершились по таймауту, поэтому дерево каталогов остается доступным для просмотра, даже если данные за ними недоступны.
SSHFS с reconnect самостоятельно выполняет повторные попытки подключения по SSH. Монтирование rclone с --vfs-cache-mode full продолжает отдавать файлы, уже находящиеся в кэше, и ставит операции записи в очередь для последующей выгрузки, в то время как операции чтения, отсутствующие в кэше, завершаются ошибкой.
Что бы вы ни выбрали, письменно ответьте на один вопрос: когда хранилище перезагружается для обновления ядра, восстанавливается ли приложение самостоятельно или требуется вмешательство человека? Протестируйте это, намеренно перезагрузив хранилище, прежде чем доверять этой паре какие-либо важные данные.
Первая проблема, с которой вы столкнётесь: запись в пустую точку монтирования
Из-за этого уже теряли реальные данные. Причём неважно, какой протокол вы выбрали.
/mnt/media — это каталог. Когда файловая система смонтирована, в нём видны файлы хранилища. Когда монтирование неактивно, это пустой каталог на небольшом локальном диске. Приложение не может отличить эти состояния. Оно записывает данные в /mnt/media, запись завершается успешно, и данные попадают в корневую файловую систему сервера приложения. Вы обнаружите проблему, когда корневой диск заполнится и сервисы начнут завершаться с ошибками. При восстановлении ситуация ухудшается: повторное монтирование скрывает неправильно размещённые файлы под точкой монтирования. Поэтому диск остаётся заполненным, а через du /mnt/media нечего удалить.
Запретите запись в пустой каталог. Отмонтируйте файловую систему и установите атрибут immutable:
sudo umount /mnt/media
sudo chattr +i /mnt/media
lsattr -d /mnt/medialsattr -d выводит флаги, а i появляется, пока по этому пути ничего не смонтировано. Теперь запись в отмонтированный путь завершается ошибкой Operation not permitted. Приложение записывает ошибку в журнал, а не заполняет диск незаметно. Монтирование по-прежнему работает, поскольку оно не изменяет базовый каталог. Позже при необходимости удалите атрибут с помощью chattr -i. Это работает в ext4 и XFS, то есть подходит для корневой файловой системы обычного образа VPS.
Затем добавьте зависимость сервисов от монтирования. Создайте drop-in-конфигурацию systemd:
sudo systemctl edit jellyfin[Unit]
RequiresMountsFor=/mnt/mediaВажно понимать, что именно это даёт. RequiresMountsFor= запускает сервис после unit монтирования для указанного пути и подтягивает этот unit в зависимости. При использовании x-systemd.automount systemd устанавливает только триггер, поэтому сервис всё ещё может запуститься, если хранилище недоступно. Эта зависимость делает сбой видимым в systemctl status. От повреждения данных защищает именно каталог с атрибутом immutable.
Docker создаёт отдельную проблему: daemon самостоятельно создаёт отсутствующий источник bind mount. В Compose отключите это поведение:
services:
jellyfin:
volumes:
- type: bind
source: /mnt/media
target: /media
read_only: true
bind:
create_host_path: falseВ документации Compose указано, что create_host_path создаёт каталог по исходному пути на хосте, если по этому пути ничего нет. Значение по умолчанию — true. Значение false запрещает Compose создавать путь автоматически. Это не помогает, если путь уже существует, но является пустым. В данном случае это наиболее распространённая ситуация, поэтому сохраняйте каталог с атрибутом immutable.
Наконец, добавьте проверку во все скрипты, которые записывают данные в точку монтирования:
mountpoint -q /mnt/media || { echo "/mnt/media is not mounted" >&2; exit 1; }Какая система является источником истины?
Для каждого набора данных письменно определите это до запуска первого задания синхронизации.
Обычно каждая система выполняет свою роль. На системе приложений хранится небольшой изменяемый набор данных: база данных, конфигурация и ключи. На системе хранения находятся основные объёмы данных: медиафайлы, оригиналы и архивы. Ни одна система не является источником истины для всех данных. Поэтому резервное копирование выполняется в обоих направлениях, и легко случайно запустить одно из заданий в обратном направлении.
Опасное направление — это rsync --delete push с системы приложений на систему хранения. Если точка монтирования недоступна, исходный каталог отображается как пустой, а --delete добросовестно удаляет содержимое назначения. В результате терабайты данных исчезают за время, необходимое rsync для обхода списка файлов.
От этого защищают две привычки. Защищайте каждую синхронизацию проверкой mountpoint -q, описанной выше. Кроме того, предпочитайте pull вместо push: запускайте задание на системе, где хранится копия, чтобы она сама обращалась к источнику и забирала данные. Задание pull также сохраняет резервные копии, если система приложений была взломана, поскольку учётные данные с правом удаления резервных копий не находятся на машине, к которой злоумышленник получил доступ первой.
Используйте инструмент, который хранит снимки, а не зеркальную копию. restic и borg сохраняют историю. Поэтому файл, удалённый или зашифрованный в источнике, можно восстановить из более раннего снимка. Зеркальная копия rsync, напротив, точно копирует удаление. Если система хранения содержит копию, а не оригинал, настройте её как полноценное внешнее хранилище резервных копий с доступом только для добавления. Тогда источник сможет добавлять снимки, но не сможет удалять их.
Есть ещё одно направление, о котором часто забывают. Если на системе хранения находится единственная копия вашей библиотеки фотографий, этой системе требуется собственная резервная копия. Восстановление должно охватывать обе части пары. Их нужно создавать достаточно близко по времени, чтобы они были согласованы. Снимок базы данных за вторник и библиотека фотографий за четверг восстановят неработоспособную установку: строки базы данных будут ссылаться на файлы, которых ещё нет.
Что измерить на своей паре
Не доверяйте опубликованным значениям пропускной способности или задержки для этой схемы, в том числе тем, которые вы нашли в других источниках. Результат зависит от вашего провайдера, региона и выбранных тарифов. Измерьте свою пару. Каждую приведённую ниже команду вы запускаете самостоятельно с application box, если не указано иное.
Время кругового обмена через туннель. Оно влияет на каждую операцию с метаданными:
ping -c 100 10.9.0.2Пропускная способность канала без участия файловой системы. Запустите серверную часть на storage box:
iperf3 -siperf3 -c 10.9.0.2 -t 30Если результат намного ниже заявленного в вашем тарифе значения, проблема связана с сетью или туннелем. Параметры монтирования её не устранят. Сначала проверьте MTU, а не NFS.
Большое последовательное чтение через mount для сравнения с этим значением:
fio --name=seqread --rw=read --bs=1M --size=2G --numjobs=1 --direct=1 --filename=/mnt/media/fio-test --end_fsync=1
rm -f /mnt/media/fio-testВ FUSE mount удалите --direct=1, если fio сообщает, что этот флаг не поддерживается, поскольку обработка O_DIRECT зависит от драйвера. Большая разница между этим значением и значением iperf3 указывает на проблему в mount, а не в канале.
Стоимость операций с метаданными. Именно это значение фактически определяет выбор схемы:
time find /mnt/media -type f | wc -lЗапустите тест дважды. Первый проход выполняется без прогретого кэша. Второй показывает эффект кэша атрибутов. Затем запустите тот же find локально на storage box и сравните результаты. Отношение этих двух временных показателей — накладные расходы данной архитектуры для вашего приложения. Они измерены на вашем оборудовании, а не приняты по умолчанию.
Если вы выбрали NFS, измерьте задержку отдельных операций напрямую:
nfsiostat 5 3
nfsstat -cnfsiostat входит в состав nfs-common и сообщает количество операций в секунду, а также среднее время кругового обмена для каждой операции с каждым mount. nfsstat -c разбирает вызовы клиента по типам операций. Если вызовы getattr составляют основную часть общего количества, рабочая нагрузка ограничена операциями с метаданными: увеличьте acdirmin и acdirmax, повторно выполните mount и снова измерьте тот же find.
Затем выполните тест, который часто пропускают. Перезагрузите storage box, пока application box обслуживает трафик, и наблюдайте за результатом. Только так можно выяснить, соответствует ли поведение при сбое настроенной конфигурации или вашим предположениям.
FAQ
Можно ли разместить базу данных на смонтированном разделе VPS хранения?
Нет. Движки баз данных зависят от того, что fsync() обращается к реальному хранилищу, а блокировка файлов работает между процессами; ни то, ни другое не является надежным через NFS, SSHFS или монтирование rclone. При разрыве соединения база данных не приостанавливается корректно: процесс зависает или файл повреждается. Храните директорию с данными SQLite, PostgreSQL или MariaDB на локальном диске сервера приложений. Монтируемый раздел предназначен для больших файлов, которые записываются один раз и считываются целиком.
NFS, SSHFS или rclone: что выбрать?
Используйте NFS поверх туннеля WireGuard, если удаленная сторона является файловой системой в том же регионе, так как клиент ядра кэширует атрибуты, а hard, soft и softreval позволяют выбрать поведение при сбоях. Используйте SSHFS, если вы не хотите настраивать серверную часть и готовы смириться с коротким кэшем метаданных (по умолчанию 20 секунд) с задержкой на каждый запрос сверх этого времени. Используйте rclone, если удаленная сторона является объектным хранилищем, а не файловой системой, где его VFS-кэш и длительный --dir-cache-time выполняют основную работу.
Что произойдет с моим приложением, если VPS хранения отключится?
При стандартном монтировании NFS с hard процессы, обращающиеся к разделу, переходят в состояние D, а ядро записывает nfs: server 10.9.0.2 not responding, still trying в логи. Их можно завершить: начиная с ядра 2.6.25, kill -9 завершает зависший процесс, теряя все данные, которые еще не были записаны. Если их не трогать, они возобновят работу без потери данных после восстановления соединения. С параметром soft вызовы будут завершаться с ошибкой ввода-вывода, с задокументированным риском скрытого повреждения данных при записи. SSHFS с reconnect выполняет повторные попытки самостоятельно, а монтирование rclone продолжает отдавать данные, которые уже находятся в его VFS-кэше.
Почему мой корневой диск заполнился, когда монтирование было отключено?
Потому что отмонтированная точка монтирования остается доступной для записи пустой директорией на локальном диске, поэтому приложение записывало данные туда, и они попадали на корневую файловую систему. Повторное монтирование скрывает эти файлы под точкой монтирования, поэтому место остается занятым, хотя путь выглядит пустым. Размонтируйте раздел, удалите содержимое под точкой монтирования, затем выполните sudo chattr +i /mnt/media для пустой директории, чтобы будущие записи завершались ошибкой Operation not permitted вместо записи в неверное место.
Действительно ли оба сервера должны находиться в одном регионе?
Да, для данной архитектуры. Каждая операция с метаданными требует как минимум одного сетевого обмена, а сканирование библиотеки выполняет его для каждого файла, поэтому обмен между регионами замедляет самую медленную часть нагрузки в сто и более раз. Измерьте показатели с помощью ping -c 100 через туннель перед миграцией данных. Если среднее значение не составляет малую долю миллисекунды, размещайте приложение поблизости от его данных.