MAATRIX / Блог / Почему копирование файла на тот же диск иногда быстрее, чем его чтение

Почему копирование файла на тот же диск иногда быстрее, чем его чтение

MAATRIX

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

Как обычно копирует cp: три разных пути внутри одной команды

Когда вы пишете cp source.img dest.img, происходит одна из трёх разных операций — в зависимости от файловой системы и версии coreutils.

Самый простой и медленный вариант — классический цикл read()/write(). Ядро читает блок данных с диска в буфер ядра, копирует его в буфер процесса cp, процесс отдаёт этот буфер обратно через write(), ядро копирует его снова — и данные попадают на диск (или сперва оседают в page cache и улетают асинхронно). Итого минимум два прохода данных через память и полноценное физическое чтение с диска, если файла ещё нет в кеше.

Второй вариант — системный вызов copy_file_range(), который современный cp использует по умолчанию (начиная примерно с coreutils 8.28). Копирование идёт целиком внутри ядра, без обязательной прогулки данных в userspace и обратно — меньше переключений контекста и копирований в памяти. Но физически данные всё равно читаются с диска и записываются на диск, просто без лишнего пассажира.

Третий вариант — тот самый, из-за которого копия может оказаться почти мгновенной: reflink, он же copy-on-write clone. Здесь copy_file_range() (или ioctl FICLONE) не копирует данные вообще — файловая система создаёт новый файл, у которого экстенты указывают на те же физические блоки, что и у оригинала. Данные не читаются и не пишутся — переписываются только метаданные: extent-дерево (XFS) или B-дерево (Btrfs) получает новую запись на существующие блоки, и растёт счётчик ссылок на них.

Именно третий случай и даёт «парадокс из заголовка»: cp отработала за миллисекунды, потому что физического копирования не произошло — это операция с метаданными, сравнимая по стоимости с созданием жёсткой ссылки, а не с переносом гигабайт данных.

Reflink и copy-on-write: копия, которая не копирует

Идея copy-on-write в файловых системах та же, что в fork() процессов или в снапшотах виртуальных дисков: вместо немедленного дублирования данных система дублирует ссылку на них, а реальное дублирование откладывает до первого изменения одной из копий.

# Реально клонирует данные без физического копирования (XFS с reflink, Btrfs)
cp --reflink=always original.img clone.img

# Пробует reflink, если ФС не поддерживает — тихо копирует как обычно
cp --reflink=auto original.img clone.img

Начиная с версии coreutils, где copy_file_range() вызывается по умолчанию, обычный cp без флагов тоже может «случайно» сделать reflink-копию на подходящей файловой системе — ядро само пробует clone-путь внутри copy_file_range(), прежде чем откатиться на физическое копирование. Если десятигигабайтная копия неожиданно заняла доли секунды — скорее всего, дело именно в этом, а не в аномально быстром диске.

Что происходит на уровне конкретных файловых систем:

  • Btrfs. Сама файловая система построена на copy-on-write — это не опция, а фундамент. Reflink работает «из коробки» для любого файла, если только на нём явно не выставлен chattr +C (nodatacow), отключающий CoW — обычно так делают для файлов баз данных (подробнее — в разборе, где Btrfs хорош, а где рискован).
  • XFS. Поддержка reflink — надстройка, а не изначальная особенность. Том должен быть создан с опцией reflink=1 (в современных версиях mkfs.xfs она по умолчанию, но на старых томах, созданных до этого, придётся пересоздавать ФС). Проверка: xfs_info /точка/монтирования | grep reflink.
  • ext4. Reflink не поддерживает вообще — extent-дерево ext4 не умеет делить блоки между несколькими inode. cp --reflink=always на ext4 просто вернёт ошибку. Сравнение подходов к выбору файловой системы — в статье ext4 vs XFS.
  • ZFS. В относительно недавних версиях OpenZFS появилось block cloning той же природы, но доступность зависит от версии пула и включённых feature flags — на старом пуле его может не быть.

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

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

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

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

Почему чтение может быть медленнее, чем такая «копия»

Здесь и разворачивается парадокс. Чтение файла — это буквально противоположность reflink-копии по объёму работы.

Чтобы прочитать файл, ядру нужно пройти все его экстенты и для каждого выполнить физический ввод-вывод: отправить команду диску, дождаться ответа, получить данные, отдать их наверх. Если файла нет в page cache (после перезагрузки, после echo 3 > /proc/sys/vm/drop_caches, или он просто ни разу не читался), это реальные операции с NAND-флешем на SSD или магнитными пластинами на HDD, реальная задержка на каждую операцию, реальный предел пропускной способности.

Чтобы сделать reflink-копию, ядру достаточно один раз прочитать (обычно уже из закешированных метаданных) extent-карту файла и записать новую extent-карту для клона плюс обновить счётчики ссылок. Это операция с метаданными — по объёму данных, реально проходящих через диск, она несопоставима с чтением содержимого файла на несколько гигабайт. Метаданные малы и почти всегда уже в кеше (dentry cache, inode cache), поэтому такая операция часто укладывается в единицы миллисекунд независимо от размера самого файла.

Отсюда наблюдение: cp --reflink=always bigfile.img copy.img может отработать быстрее, чем dd if=bigfile.img of=/dev/null bs=1M, если файла нет в page cache. Копия не читает данные — только карту блоков, чтение читает все данные целиком.

Важная оговорка: если файл уже лежит в page cache (недавно читался, писался или просто маленький и давно закеширован), обычное чтение тоже будет быстрым — вы читаете из RAM, а не с диска. Разница между reflink-копией и чтением максимально заметна на «холодном» файле: копия остаётся дешёвой метаданной-операцией в любом случае, а чтение резко замедляется, как только приходится идти на реальный диск. Насколько именно быстрее — зависит от размера файла, фрагментации и модели диска; конкретные разы стоит один раз замерить на своём железе, а не полагаться на чужие цифры.

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

Как проверить, что произошёл именно reflink

Слепо доверять скорости не стоит — она может обмануть за счёт page cache. Есть более надёжные способы убедиться, что вы получили CoW-клон, а не полную копию.

Сравнить фактическое использование места. du показывает дисковое пространство, реально занятое файлом с учётом общих блоков, а stat/ls -l — логический размер. У двух reflink-копий логический размер будет одинаковым, а суммарное потребление места после копии почти не вырастет:

stat -c '%s %n' original.img clone.img   # логический размер — одинаковый
du -sh original.img clone.img            # суммарный физический размер — почти не изменился
df -h /точка/монтирования                # свободное место практически не уменьшилось

Если после копии du показывает удвоение объёма — reflink не сработал: либо ФС не поддерживает CoW, либо на файле стоит запрет, либо копия шла между разными файловыми системами.

Посмотреть карту экстентов. filefrag -v покажет физические диапазоны блоков файла — у двух reflink-связанных файлов они совпадут:

filefrag -v original.img
filefrag -v clone.img

На XFS для той же цели удобнее xfs_io -c "fiemap -v" original.img.

Явно потребовать reflink. Надёжнее всего использовать cp --reflink=always вместо =auto — если файловая система не умеет reflink или копирование идёт между разными томами, команда явно завершится ошибкой вместо тихого отката на полное копирование:

cp --reflink=always source.img dest.img || echo "reflink не сработал"

Где reflink не сработает и что важно не упустить

Работает только в пределах одной файловой системы. Скопировать файл с reflink между /mnt/disk1 и /mnt/disk2, если это разные точки монтирования разных блочных устройств, невозможно в принципе — делить блоки не с чем. cp --reflink=always вернёт ошибку, =auto тихо откатится к полной физической копии. Это же касается копирования между «изолированными» Btrfs-подтомами без общей истории.

Экономия места не постоянна. Пока копии не менялись, они честно делят одни и те же блоки. Но как только вы меняете хотя бы байт в любой из них, срабатывает сам механизм CoW: ФС не может править общий блок на месте (это испортило бы вторую копию), поэтому выделяет новый блок под изменённые данные, старый остаётся у нетронутой копии. Чем активнее файлы расходятся, тем больше реального места они занимают — в пределе, если переписать всё содержимое обеих, они займут ровно вдвое больше, как если бы reflink и не было.

CoW плохо уживается со случайной перезаписью «на месте». Файлы БД (страничные форматы вроде InnoDB, PostgreSQL heap) активно переписывают одни и те же блоки на месте, а под CoW каждая такая перезапись физически уезжает в новый блок — оригинальный блок превращается в мусор для сборщика свободного места. Итог — заметная фрагментация именно на файлах с частой случайной записью. Поэтому на Btrfs для файлов БД обычно отключают CoW через chattr +C (действует только на новые файлы в каталоге, где атрибут выставлен заранее на пустой каталог), а на XFS с reflink просто не клонируют файлы, которые собираются активно и хаотично переписывать.

df после массового reflink может вводить в заблуждение. Раздел с кучей клонов покажет куда больше свободного места, чем «интуитивно кажется правильным» по сумме логических размеров файлов — это ожидаемо, но если клоны рано или поздно массово разойдутся (например, при обновлении множества похожих VM-образов), реальный расход места вырастет резко и может застать врасплох, если полагаться только на df.

Права и метаданные не остаются общими. Reflink-клон — независимый файл со своими правами, владельцем и временными метками с момента создания. Изменение прав на одной копии не затронет вторую — общими остаются только сами блоки данных.

Практическое применение

Клонирование образов виртуальных машин. Если вы держите «золотой» образ диска и разворачиваете из него новые VM на одном XFS- или Btrfs-томе, reflink-клон вместо полной копии — это разница между секундами и минутами на каждый образ:

cp --reflink=always golden-image.qcow2 vm-042.qcow2

Тестовые и staging-окружения. Копия продакшен-датасета для тестов на том же томе через reflink не тратит лишнее место и не грузит диск — пока тестовый набор не начнёт активно расходиться с оригиналом. Это удобно, когда нужно многократно откатывать тестовые данные к исходному состоянию.

Резервные копии перед рискованной операцией. Перед миграцией схемы БД, обновлением приложения или экспериментом с конфигом reflink-копия критичного файла — способ подстраховаться почти без цены по времени и месту, при условии что ФС это поддерживает.

Осторожно с сетевой репликацией. Как только файл покидает исходную файловую систему — по сети, на другой диск, в архив — reflink теряет смысл: rsync, scp, загрузка в объектное хранилище всегда читают и передают реальные данные, потому что на другом конце нет тех же физических блоков, на которые можно сослаться. «Раз локальная копия была мгновенной, то и бэкап будет таким же» — ложное ожидание.

При выборе конфигурации сервера под такие сценарии стоит сразу закладывать файловую систему с поддержкой reflink — XFS с reflink=1 или Btrfs — а не выяснять постфактум, что на ext4 такой трюк недоступен в принципе. Заодно полезно понимать, как устроены inode и extent-карта на низком уровне — это помогает быстрее диагностировать, почему место вроде бы есть, а файл не создаётся, и как page cache влияет на скорость чтения тех же файлов — это разобрано в статье про файловый кеш и грязные страницы.

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

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

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

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

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

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

Работает ли reflink на ext4?

Нет. Формат ext4 не поддерживает разделяемые экстенты между разными inode — reflink доступен только на файловых системах, спроектированных под copy-on-write или добавивших эту возможность позже: Btrfs, XFS (с reflink=1 при создании тома), некоторые версии OpenZFS через block cloning.

Если reflink-копия «бесплатна», значит ли это, что диск вообще не работает при копировании?

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

Можно ли посчитать, сколько места реально экономит reflink?

Общий объём, занятый общими блоками, можно оценить через du в сравнении с суммой логических размеров файлов, а на Btrfs — через btrfs filesystem du, который явно показывает эксклюзивный и разделяемый объём. Заранее назвать процент экономии нельзя — он зависит от того, насколько копии успели разойтись с оригиналом.

Что будет, если я сделаю reflink-копию, а потом перенесу файл на другой сервер через rsync?

Reflink — свойство конкретной файловой системы, оно не «путешествует» вместе с файлом. На принимающей стороне rsync создаст обычный физический файл — данные будут прочитаны на одном конце и записаны на другом, никакой магии CoW через сеть не бывает.

Стоит ли отключать CoW (chattr +C) на файлах баз данных на Btrfs?

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

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

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

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