# Sparse-файлы - дыры и apparent size _Файловая система · LinuxLab Knowledge Base_ **TL;DR:** Sparse-файл имеет "дыры" - блоки которые ФС не аллоцировала. При чтении возвращают нули, но не занимают места. ls показывает apparent size, du - реальное. Используются в qcow2, бэкапах, sparse loop. ## Зачем 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 через границу** ```bash dd if=/dev/zero of=big.img bs=1 count=0 seek=10G ``` Создаёт файл с logical size 10 GB, занимает 0 блоков. ФС не пишет нули - просто запоминает "тут дыра до позиции X". **2. truncate / ftruncate** ```bash truncate -s 10G big.img ``` То же самое одной командой - расширяет inode-len без аллокации. **3. Удаление блоков из существующего файла (FALLOC_FL_PUNCH_HOLE)** ```bash fallocate -p -o 1G -l 1G existing.dat ``` Удалить байты 1-2 GB из середины файла, создав дыру. Логический размер не меняется, физическое использование падает. ## ls / stat / du - кто что показывает ```bash $ 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_END - **`du`** показывает **disk usage** в килобайтных юнитах - **`du --apparent-size`** или **`du -k --apparent-size`** - apparent - **`stat`** показывает оба: `Size:` (apparent) и `Blocks:` (×512 = bytes) Если на диске только 5 GB свободно, а ФС "видит" файлы суммарно на 20 GB - это нормально для sparse, но опасно: при заполнении дыр можно упереться в ENOSPC внутри write(). ## fallocate vs sparse Sparse - **не аллоцированные** блоки. `fallocate` (без `-p`) - наоборот, **резервирует** блоки без записи нулей: ```bash 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-области в дыры: ```bash fallocate -d disk.img ``` ## SEEK_HOLE / SEEK_DATA Современные ФС (ext4, xfs, btrfs, tmpfs) поддерживают seek в lseek(): - **`SEEK_HOLE`** - найти следующую дыру - **`SEEK_DATA`** - найти следующий аллоцированный блок Через `cp --sparse=auto` (default) копирование сохраняет дыры: ```bash 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`: ```bash rsync --sparse # дыры сохраняются tar --sparse -cf backup.tar big.img dd conv=sparse if=src of=dst # пропускать zero-блоки ``` Без правильных флагов sparse 100GB-файл при копировании развернётся в честные 100 GB. ## Реальные применения в проде ### qcow2 для KVM ```bash qemu-img create -f qcow2 disk.qcow2 100G ``` qcow2 - формат с **встроенным** sparse + COW + chain-of-snapshots. На ext4-хосте qcow2-файл ещё и сам sparse - двойная экономия. ### Loop-устройство с ФС внутри ```bash truncate -s 10G ext4.img mkfs.ext4 ext4.img sudo mount -o loop ext4.img /mnt/loop ``` Файл начинает с 0 байт, mkfs выделит ~метаданные (~ 1% от размера), дальше расходуется по мере записи. ### Backup с дырками ```bash # Прямое копирование диска с пропуском 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 ```bash # Соотношение 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 ``` ## Команды ```bash truncate -s 10G big.img ``` Создать sparse-файл логического размера 10 GB, физически 0 ```bash du -h --apparent-size big.img ``` Apparent size (как видит приложение) vs обычный du = allocated ```bash fallocate -d big.img ``` Найти zero-области и превратить их в дыры - уплотнение ```bash cp --sparse=auto big.img copy.img ``` Скопировать sparse-файл, сохранив дыры ```bash rsync --sparse src.img dst.img ``` rsync с распознаванием дыр - не разворачивать в нули ```bash qemu-img info disk.qcow2 ``` Покажет virtual size и actual size - двойная sparse-индикация ```bash filefrag -v file.img ``` Карта extents, видны 'holes' - проверить sparse-структуру файла ## См. также - [Block devices - диски в Linux](/kb/block-devices.md) - [Inode](/kb/inode.md) - [mount и /etc/fstab - подключение ФС](/kb/mount-and-fstab.md) - [Файловые системы: ext4, xfs, btrfs, zfs](/kb/filesystems.md) - [LVM - Logical Volume Manager](/kb/lvm.md)