Зачем sparse
Когда нужно создать файл "размером" 100 GB, который будет постепенно заполняться - не обязательно сразу аллоцировать 100 GB на диске. Можно создать пустой файл с логическим размером 100 GB, но физически 0 байт. По мере записи ФС аллоцирует блоки.
Применения:
- qcow2/vmdk для VM - "тонкий" виртуальный диск
- loop-image для FS - 10 GB файла с ext4 внутри, реально занят на 1 GB
- БД с pre-allocated tablespace (Oracle, MS SQL)
- Бэкапы дисков с пустыми областями - ddrescue
- Sparse logfile - перематываемый кольцевой буфер
Как создаются дыры
Три способа:
1. Seek + write через границу
dd if=/dev/zero of=big.img bs=1 count=0 seek=10G
Создаёт файл с logical size 10 GB, занимает 0 блоков. ФС не пишет нули - просто запоминает "тут дыра до позиции X".
2. truncate / ftruncate
truncate -s 10G big.img
То же самое одной командой - расширяет inode-len без аллокации.
3. Удаление блоков из существующего файла (FALLOC_FL_PUNCH_HOLE)
fallocate -p -o 1G -l 1G existing.dat
Удалить байты 1-2 GB из середины файла, создав дыру. Логический размер не меняется, физическое использование падает.
ls / stat / du - кто что показывает
$ truncate -s 10G big.img
$ ls -lh big.img
-rw-r--r-- 1 user user 10G May 2 15:00 big.img ← apparent (logical)
$ du -h big.img
0 big.img ← actual (allocated)
$ stat big.img
Size: 10737418240 Blocks: 0 IO Block: 4096 regular empty file
ls -lпоказывает apparent size - то что вернёт seek SEEK_ENDduпоказывает disk usage в килобайтных юнитахdu --apparent-sizeилиdu -k --apparent-size- apparentstatпоказывает оба:Size:(apparent) иBlocks:(×512 = bytes)
Если на диске только 5 GB свободно, а ФС "видит" файлы суммарно на 20 GB - это нормально для sparse, но опасно: при заполнении дыр можно упереться в ENOSPC внутри write().
fallocate vs sparse
Sparse - не аллоцированные блоки.
fallocate (без -p) - наоборот, резервирует блоки без записи нулей:
fallocate -l 10G allocated.dat
Файл "занимает" 10 GB на диске, но содержимое - undefined garbage (ядро не зануляет). Ускорение для случаев "будем писать 10 GB последовательно":
- Защита от фрагментации - блоки выделены подряд
- Гарантия что write() не упрётся в ENOSPC
Если ФС поддерживает - аллокация мгновенная (без записи нулей). На ext4/xfs - да. На fat - нет (всегда пишутся нули).
Опции fallocate:
| Опция | Что |
|---|---|
-l SIZE | размер |
-o OFFSET | смещение |
-p | FALLOC_FL_PUNCH_HOLE - сделать дыру |
-z | FALLOC_FL_ZERO_RANGE - зануление с возможным sparse |
-d | FALLOC_FL_DIG_HOLES - найти zero-блоки и сделать их дырами |
-c | FALLOC_FL_COLLAPSE_RANGE - удалить и сдвинуть |
-i | FALLOC_FL_INSERT_RANGE - вставить и сдвинуть |
fallocate -d - уплотнить существующий файл, превратив
zero-области в дыры:
fallocate -d disk.img
SEEK_HOLE / SEEK_DATA
Современные ФС (ext4, xfs, btrfs, tmpfs) поддерживают seek в lseek():
SEEK_HOLE- найти следующую дыруSEEK_DATA- найти следующий аллоцированный блок
Через cp --sparse=auto (default) копирование сохраняет дыры:
cp --sparse=auto big.img copy.img # переносит sparse, если ФС умеет
cp --sparse=always big.img copy.img # ищет zero-области и делает дыры
cp --sparse=never big.img copy.img # копирует "плотно", дыры заполнит нулями
Аналогично для tar, rsync, dd:
rsync --sparse # дыры сохраняются
tar --sparse -cf backup.tar big.img
dd conv=sparse if=src of=dst # пропускать zero-блоки
Без правильных флагов sparse 100GB-файл при копировании развернётся в честные 100 GB.
Реальные применения в проде
qcow2 для KVM
qemu-img create -f qcow2 disk.qcow2 100G
qcow2 - формат с встроенным sparse + COW + chain-of-snapshots. На ext4-хосте qcow2-файл ещё и сам sparse - двойная экономия.
Loop-устройство с ФС внутри
truncate -s 10G ext4.img
mkfs.ext4 ext4.img
sudo mount -o loop ext4.img /mnt/loop
Файл начинает с 0 байт, mkfs выделит метаданные ( 1% от размера),
дальше расходуется по мере записи.
Backup с дырками
# Прямое копирование диска с пропуском zero-блоков
dd if=/dev/sda of=backup.img conv=sparse status=progress
# Или через ddrescue
ddrescue /dev/sda backup.img backup.log
Восстановление - dd if=backup.img of=/dev/sda без conv=sparse:
тогда дыры дойдут до диска как реальные нули.
Когда что-то пошло не так
- ENOSPC при записи в "пустую" дырку - физического места не хватило на материализацию блока. Sparse экономит только пока дыры пусты.
- du показывает огромные числа после восстановления бэкапа - копировали
без
--sparse=auto. Дыры заполнились нулями = реальные блоки. tarраспаковал sparse-файл "толстым" - нужен--sparseпри создании архива и при распаковке. На GNU tar 1.30+ распаковка sparse работает автоматически если archive создан с--sparse.- VM-диск растёт сам по себе - guest переписывает ненулевые блоки
на нули, но qcow2/ФС не знает что это zero. Решение: внутри VM
периодически
fstrim(для SSD-aware) илиzerofree+fallocate -d. fallocateпадает с ENOTSUP на NFS - не все версии NFS поддерживают punch_hole. На NFSv4.2 - работает.- rsync разворачивает дыры в нули -
--sparseвместе с-S.
Проверка что файл sparse
# Соотношение allocated / apparent
python3 -c "
import os
s = os.stat('big.img')print(f'apparent: {s.st_size}, allocated: {s.st_blocks * 512}, ratio: {s.st_blocks * 512 / s.st_size if s.st_size else 0:.2%}')"
# Карта аллоцированных областей
filefrag -v big.img
xfs_io -c 'fiemap -v' big.img # на любой ФС в ядре >= 2.6.36