Раздел NFS, который ещё вчера отдавал файлы за миллисекунды, сегодня превращает любую команду в пытку: ls молчит минутами, df -h подвисает намертво, а в процессах плодятся задачи в состоянии D, которые не берёт даже kill -9. Знакомая картина для всякого, кто держал в продакшене сетевые файловые системы. Почти всегда корень проблемы один: сервер NFS недоступен, а клиент смонтировал шару с опциями по умолчанию, то есть hard. Разберём по косточкам, чем soft отличается от hard на уровне таймаутов и RPC, как выглядит реальная диагностика живой системы и в какой последовательности администратору действовать, чтобы вернуть машину к жизни без перезагрузки.
Механика повторных запросов и флаги к ним
NFS-клиент в ядре Linux общается с сервером через RPC. Каждая операция, будь то чтение блока или stat каталога, упаковывается в вызов, у которого есть два стража: timeo и retrans. Параметр timeo задаёт начальный таймаут ожидания ответа в десятых долях секунды, значение 10 означает одну секунду. Параметр retrans говорит, сколько раз клиент повторит запрос, прежде чем решить, что делать дальше. И вот здесь дорожки расходятся.
При hard-монтировании, а оно стоит по умолчанию, клиент после исчерпания retrans печатает в лог строку "server not responding" и начинает цикл заново, причём бесконечно. Запрос никогда не вернёт ошибку вызывающему процессу. Таймаут при этом растёт по экспоненте: 1, 2, 4, 8 секунд и так до потолка в 600 секунд, после чего последовательность обнуляется. Процесс сидит в неубиваемом сне D внутри ядерного вызова, и приложение, открывшее файловый дескриптор, повисает вместе с ним.
Soft-режим работает иначе: когда запросы с повторами исчерпаны, ядро возвращает операции ошибку ввода-вывода (EIO для большинства вызовов, ETIMEDOUT в отдельных случаях). Приложение получает сбой и решает само, что с ним делать. Расчёт на пальцах прост: timeo=10,retrans=3 при TCP даёт лавину примерно 1 + 2 + 4 = 7 секунд плюс накладные расходы, итого секунд десять до честной ошибки. Умолчания у клиента Linux суровые: для TCP это timeo=600 (минута) и retrans=2, то есть процесс без опции soft будет ждать, фактически, вечность.
Отдельного упоминания заслуживает пара intr и nointr. Флаг intr когда-то позволял прерывать зависший NFS-вызов сигналом, скажем по Ctrl-C. Начиная с ядер ветки 2.6.25 эта опция объявлена устаревшей: ядро её просто игнорирует, а семантика nointr стала поведением по умолчанию. Прерывание сигналами теперь разрешено всегда, но это не отменяет главного: сигнал убивает процесс, а не возвращает приложению осмысленную ошибку. В современных fstab писать intr бессмысленно, зато видеть его в старых конфигах полезно как маркер возраста системы.
Настройка отказоустойчивого монтирования через опции и конфиги
Теория полезна, но продакшен начинается с конкретных строк. Для шар с некритичными данными, скажем исходниками или кэшем сборки, разумный вариант выглядит так:
# Монтируем шару в soft-режиме: 3 десятых секунды стартовый таймаут, 2 повтора
mount -t nfs4 -o soft,timeo=30,retrans=2,noresvport srv01:/export/build /mnt/build
Строка для /etc/fstab, чтобы пережить перезагрузку и не уронить загрузку системы, если сервер лежит:
# nofail не даёт systemd ронять boot при недоступном сервере
# bg отправляет начальное монтирование в фон, x-systemd.idle-timeout для autofs-поведения
srv01:/export/build /mnt/build nfs4 soft,timeo=30,retrans=2,bg,nofail,_netdev 0 0
Проверить, какие опции реально применились ядром, помогает /proc/mounts или его расширенный родственник /proc/self/mountstats:
grep nfs /proc/mounts
# srv01:/export/build /mnt/build nfs4 rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,...
grep -A2 "mnt/build" /proc/self/mountstats
# device srv01:/export/build mounted on /mnt/build with fstype nfs4 statvers=1.1
# opts: rw,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,acregmin=3,...
# age: 14230
Вывод mountstats особенно ценен: там видно фактические значения timeo и retrans, объёмы прочитанных и записанных байт, счётчики ретрансмиссий по каждому типу RPC. Если в рапорте о простое звучит фраза "а мы же монтируем soft", а в /proc/mounts красуется hard, у расследования появляется главный подозреваемый ещё до анализа трафика.
Automounter меняет картину кардинально. Вместо постоянного монтирования шара появляется по требованию и исчезает по таймауту. Конфиг autofs:
# /etc/auto.master.d/nfs.autofs
/data /etc/auto.nfs --timeout=300
# /etc/auto.nfs
repo -fstype=nfs4,soft,timeo=30,retrans=2 srv01:/export/repo
Три сотни секунд без обращений, и autofs отмонтирует ресурс сам. Когда сервер падает, пострадают только те процессы, что реально трогали шару в момент отказа, а не вся машина с её df -h в мониторинге.
Симптомы на живой системе и чтение диагностических следов
Зависший NFS редко объявляет о себе прямо. Чаще приходит жалоба "сервер тупит", а дальше начинается классика. Команда df -h висит, потому что statfs лезет в каждый смонтированный раздел. ps aux показывает поражённые процессы:
ps -eo pid,stat,wchan:30,cmd | grep -E "^\s*[0-9]+\s+D"
# 4821 D rpc_wait_bit_killable /usr/bin/rsync -a /mnt/build/ /var/cache/
# 5199 D nfs4_wait_sequence vim /mnt/build/Makefile
Колонка STAT с буквой D означает сон в ядре без возможности прерывания обычными сигналами. Поле WCHAN подсказывает, где именно застрял поток: имена вроде rpc_wait_bit_killable или nfs4_wait_open выдают сетевое ожидание с головой.
В системном журнале сыплются характерные строки:
journalctl -k --since "10 min ago" | grep -i nfs
# kernel: nfs: server srv01 not responding, still trying
# kernel: nfs: server srv01 not responding, timed out
Первая строка - визитная карточка hard-монта: ядро предупреждает и продолжает молотить. Вторая появляется при soft и означает, что запрос ушёл в ошибку. Статистику ретрансмиссий показывает nfsstat:
nfsstat -rc
# Client rpc stats:
# calls retrans authrefrsh
# 182340 217 182340
Если счётчик retrans растёт на глазах при повторном запуске команды через десять секунд, сеть до сервера дырявая или сам сервер не отвечает. Проверка доступности сервисов на стороне сервера делается классикой:
rpcinfo -p srv01 | grep -E "nfs|mountd"
# 100003 4 tcp 2049 nfs
# 100005 3 tcp 20048 mountd
Пустой ответ или таймаут говорят, что проблема глубже NFS: либо сервер лежит, либо межсетевой экран съел порт 2049. Быстрый пробник showmount -e srv01 вернёт список экспортов либо сочную ошибку RPC: Port mapper failure, что тоже полноценный диагноз.
План восстановления доступа и чистого отмонтирования
Когда картина ясна и сервер признан недоступным, администратору нужен порядок действий, а не беспорядочный набор kill. Рабочая последовательность такова:
- Убедиться, что сервер действительно недоступен:
ping srv01,rpcinfo -p srv01, проверка с соседнего клиента. - Снять список зависших процессов через
psпо D-состоянию и WCHAN, попробовать мягкоеkill, потомkill -9(при intr-семантике современных ядер сигнал KILL процессу в rpc_wait всё же доставляется, хотя не сразу). - Попытаться отмонтировать шару принудительно:
umount -f /mnt/build; при soft-монтах это обычно проходит. - При неудаче применить ленивое отмонтирование:
umount -l /mnt/build; точка монтирования отцепляется от дерева каталогов немедленно, а внутренние ссылки дочищаются, когда освободятся дескрипторы. - Убедиться в
/proc/mounts, что записи больше нет, и пересоздать монтирование с разумными опциями либо убрать его из autofs-карты до восстановления сервера. - Разобрать инцидент: найти, кто стартовал зависший процесс (часто это cron-задача бэкапа), и перевести её на soft либо на локальное зеркало.
Отдельная засада поджидает после возвращения сервера: ошибка Stale NFS file handle. Она означает, что файловый дескриптор, который клиент кэшировал до сбоя, указывает на объект, которого на сервере больше нет: шару пересоздали, экспорт переименовали, inode перегенерирован при восстановлении из бэкапа. Лечится переброской монтирования, а в тяжёлых случаях перезапуском приложений, держащих старые дескрипторы. Ни один sigkill не оживит процесс, у которого cwd сидит в stale-каталоге, так что ленивый umount здесь спасение.
Сетевая проверка доступности сервера и анализ на уровне пакетов
Когда rpcinfo молчит, следующий шаг - выяснить, кто виноват: сеть, фильтр или сам сервер. Аргументированно отвечает связка простых проб. Сначала базовая связность и маршрут:
ping -c3 srv01
# 3 packets transmitted, 0 received, 100% packet loss
ip route get 10.20.30.40
# 10.20.30.40 via 10.20.1.1 dev ens192 src 10.20.1.15
Ноль ответов при живом маршруте уже сужает поиск. Дальше проверяем порт 2049 напрямую, ведь NFSv4 ходит по одному TCP-порту и не нуждается в portmapper:
timeout 5 bash -c "cat < /dev/null > /dev/tcp/srv01/2049" && echo OPEN || echo CLOSED
# CLOSED
ss -tn state established '( dport = :2049 or sport = :2049 )'
# пусто, значит TCP-сессии к серверу нет вовсе
Если порт закрыт, смотрят правила фильтрации: nft list ruleset | grep 2049 на клиенте и, при наличии доступа, на сервере. Когда порт открыт, а NFS молчит, на помощь приходит захват пакетов:
# Снимаем 20 секунд трафика по порту NFS на интерфейсе ens192
tcpdump -i ens192 -nn -c 50 'tcp port 2049'
# 14:02:11.412 IP 10.20.1.15.901 > 10.20.30.40.2049: Flags [S], seq 4122031
# 14:02:12.415 IP 10.20.1.15.901 > 10.20.30.40.2049: Flags [S], seq 4122031
Повторяющиеся SYN-пакеты без ответа - классическая картина фильтрации или мёртвого сервиса. А вот если SYN-ACK приходит, но RPC-ответов нет, сервер жив, а вот nfsd на нём перегружен или завис сам. Тогда на стороне сервера смотрят nfsstat -s, очередь /proc/net/rpc/nfsd и загрузку потоков ядра, их число регулируется через rpc.nfsd 16 или параметр threads в /etc/nfs.conf.
Полезно помнить и про тонкость с таймаутами: при UDP-клиенте timeo трактуется так же, но UDP в NFSv4 уже не поддерживается, и на современных системах разговор всегда идёт о TCP. Старые шары NFSv3 могут ещё тащить за собой rpcbind, mountd и динамические порты, поэтому для них rpcinfo -p не просто любопытство, а необходимость: без mountd на порту 20048 v3-шара не смонтируется в принципе.
Профилактика зависаний и выбор режима под тип нагрузки
Выбор между hard и soft не вопрос вкуса, а баланс между целостностью данных и отзывчивостью. Для записи данных, где потерянный блок недопустим, hard с bg и nofail остаётся единственным честным вариантом: NFS гарантирует, что запись либо завершится, либо клиент будет ждать, пока сервер воскреснет. Soft с коротким timeo годится для чтения, для идемпотентных операций и для автомонтирования служебных ресурсов. Золотая середина для смешанных шар - soft с умеренными значениями timeo=50,retrans=3 и мониторингом EIO в приложении.
Практики, которые экономят нервы: держать actimeo=30 или выше на редко меняющихся шарах, чтобы stat не бегал в сеть по каждому пустяку; включать lookupcache=positive; следить за счётчиками nfsstat -rc в системе метрик и алертить на рост retrans выше пары процентов от общего числа вызовов; никогда не монтировать NFS в корень / и не класть туда каталоги из PATH, иначе зависшая шара обездвижит даже логин по SSH. Cron-задачи, ходящие в NFS, обязаны иметь timeout 120 обёртку, чтобы не плодить процессы-призраки.
Отдельно стоит проверять версию протокола при миграциях: шару, переведённую с NFSv3 на NFSv4.1, клиенту лучше монтировать явно с vers=4.1,minorversion=1, иначе при обновлении сервера легко получить несовпадение filehandle и тот самый stale на ровном месте. Не менее полезно держать в fstab комментарий с датой и причиной выбора опций: через год история "кто поставил timeo=5 и зачем" разгадывается без археологии.
И напоследок о наблюдении из практики: большинство "загадочных зависаний сервера" раскрываются за десять минут, если первым делом посмотреть cat /proc/mounts | grep nfs и ps по D-процессам. NFS - честная старая технология: она висит не из вредности, а потому что кто-то, когда-то смонтировал её на всю жизнь словом hard и забыл сказать, когда останавливаться.