Recently I needed to customize the OS template used to deploy KVM guests and once it was ready I realized it could be a lot smaller – there was plenty of free space on the partition which made the image larger than necessary, therefore I decided to reduce the size to the minimum possible and while it could be done via tools such as GParted – I couldn’t miss the opportunity to explore how it’s done behind the scene.
The goal is to reduce the size of the image, removing all unused space but leaving 100MB of free space available (that is optional and if free space is not needed in your case, adjust the formulas described below), which allows for further small modifications in case it’s needed.
Here’s how I’ve done it – on an example of 3.5GB OS image with AlmaLinux – note: naturally it also applies to shrinking partitions located on physical disks.
Road map:
– Shrink filesystem
– Shrink partition
– Align SWAP partition
– Shrink the image
Examine the input image:
1. Show details of the partitions on the image:
1 2 3 4 5 6 7 8 9 10 11 |
~# fdisk -l AlmaLinux-8.4.qcow2 Disk AlmaLinux-8.4.qcow2: 3.5 GiB, 3758096384 bytes, 7340032 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x23e1395e Device Boot Start End Sectors Size Id Type AlmaLinux-8.4.qcow2p1 * 2048 6711295 6709248 3.2G 83 Linux AlmaLinux-8.4.qcow2p2 6711296 6973439 262144 128M 82 Linux swap / Solaris |
2. Examine the image itself:
1 2 3 4 5 |
~# du -hs AlmaLinux-8.4.qcow2 2.3G AlmaLinux-8.4.qcow2 ~# ls -lah AlmaLinux-8.4.qcow2 -rw-r--r-- 1 root root 3.5G Sep 13 16:29 AlmaLinux-8.4.img |
As can be seen above – there is a difference in size reported by ‘du’ and ‘ls’ tools which might be confusing – this is because ‘ls’ shows how big is the AlmaLinux-8.4.qcow2 as a file on disk which is just a container for underlying partitions, while ‘du’ tool by default prints data used by the image without holes, internal fragmentation, indirect blocks, etc., add ‘–apparent-size’ switch to change ‘du’ default behavior:
1 2 |
~# du -hs --apparent-size AlmaLinux-8.4.qcow2 3.5G AlmaLinux-8.4.qcow2 |
(Optional) Convert the image to raw format:
1 2 3 4 5 6 7 8 |
~# virt-sparsify --format raw --ignore /dev/sda1 AlmaLinux-8.4.qcow2 AlmaLinux-8.4.img [ 0.0] Create overlay file in /tmp to protect source disk [ 0.1] Examine source disk [ 4.6] Clearing Linux swap on /dev/sda2 [ 5.8] Copy to destination and make sparse [ 16.9] Sparsify operation completed with no errors. virt-sparsify: Before deleting the old disk, carefully check that the target disk boots and works correctly. |
Map partitions on the image:
1 2 3 |
~# kpartx -av AlmaLinux-8.4.img add map loop1p1 (253:0): 0 6709248 linear 7:1 2048 add map loop1p2 (253:1): 0 262144 linear 7:1 6711296 |
Now the image can be accessed via /dev/loop1 block device and the partitions are mapped via /dev/mapper:
1 2 3 4 5 6 |
~# lsblk -fs /dev/mapper/loop1p* -oNAME,FSTYPE,SIZE NAME FSTYPE SIZE loop1p1 ext4 3.2G └─loop1 3.5G loop1p2 swap 128M └─loop1 3.5G |
Shrink the OS partition:
It’s important to run a filesystem check before examining and resizing the filesystem:
1 2 3 4 5 6 7 8 |
~# e2fsck -f /dev/mapper/loop1p1 e2fsck 1.45.6 (20-Mar-2020) Pass 1: Checking inodes, blocks, and sizes Pass 2: Checking directory structure Pass 3: Checking directory connectivity Pass 4: Checking reference counts Pass 5: Checking group summary information root: 40508/209664 files (0.1% non-contiguous), 495036/838656 blocksGet the count of free and used blocks on the filesystem, which is needed to calculate reduced size of the partition: |
1. Get the count of free and used blocks on the filesystem, which is needed to calculate the reduced size of the partition:
1 2 3 4 |
~# tune2fs -l /dev/mapper/loop1p1|egrep '(Block count|Free blocks|Block size)' Block count: 838656 Free blocks: 343620 Block size: 4096 |
2. Calculate the minimum size for the /dev/mapper/loop1p1 filesystem using the following formula:
1 |
( [Block count] - [Free blocks] ) * [Block size] / 1024[bytes] |
therefore:
(838656 – 343620) * 4096 / 1024 = 1980144 KiB
3. Reduce filesystem size based on the calculated result:
1 2 3 4 5 |
~# resize2fs -f /dev/mapper/loop1p1 1980144K resize2fs 1.45.6 (20-Mar-2020) Resizing the filesystem on /dev/mapper/loop1p1 to 495036 (4k) blocks. The filesystem on /dev/mapper/loop1p1 is now 495036 (4k) blocks long. Then run filesystem scan again on /dev/mapper/loop1p1 to ensure the calculated result is correct - it can be smaller than the partition but not larger. |
4. Reduce partition size, leaving ~100MB which will be free space for the underlying filesystem:
a) Get the filesystem block count after resizing:
1 2 3 4 |
~# tune2fs -l /dev/mapper/loop1p1|egrep '(Block count|Free blocks|Block size)' Block count: 495036 Free blocks: 5458 Block size: 4096 |
b) Check current partitions layout:
1 2 3 4 5 |
~# fdisk -l /dev/loop1 |egrep '(loop1|Sector size)' Disk /dev/loop1: 3.5 GiB, 3758096384 bytes, 7340032 sectors Sector size (logical/physical): 512 bytes / 512 bytes /dev/loop1p1 * 2048 4167136 4165089 2G 83 Linux /dev/loop1p2 4167137 4429823 262687 128.3M 82 Linux swap / Solaris |
c) Calculate the new size of the partition, using the following formula:
1 |
( [Block count] + [Free Blocks] ) * [Block size] / [Sector size] |
therefore:
(495036 + 5458) * 4096 / 512 = 4003952Â sectors
5. With the calculated new size of the partition in sectors we can now start resizing via fdisk (note: this is a single fdisk session separated by comments).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
~# fdisk /dev/loop1 Command (m for help): p Disk /dev/loop1: 3.5 GiB, 3758096384 bytes, 7340032 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x23e1395e Device Boot Start End Sectors Size Id Type /dev/loop1p1 * 2048 6711295 6709248 3.2G 83 Linux /dev/loop1p2 6711296 6973439 262144 128M 82 Linux swap / Solaris Command (m for help): d Partition number (1,2, default 2): Partition 2 has been deleted. Command (m for help): d Selected partition 1 Partition 1 has been deleted. Command (m for help): n Partition type p primary (0 primary, 0 extended, 4 free) e extended (container for logical partitions) Select (default p): p Using default response p. Partition number (1-4, default 1): First sector (2048-7340031, default 2048): 2048 Last sector, +sectors or +size{K,M,G,T,P} (2048-7340031, default 7340031): 4121424 Created a new partition 1 of type 'Linux' and of size 2 GiB. Partition #1 contains a ext4 signature. |
It’s important not to delete the filesystem signature:
1 |
Do you want to remove the signature? [Y]es/[N]o: n |
The first partition is with a new size now, so we can align the SWAP to it:
1 2 3 4 5 6 7 8 |
Command (m for help): n Partition type p primary (1 primary, 0 extended, 3 free) e extended (container for logical partitions) Select (default p): p Using default response p. Partition number (2-4, default 2): 2 |
Use the first sector available – not the default one:
1 2 3 4 |
First sector (4167137-7340031, default 4167680): 4167137 Last sector, +sectors or +size{K,M,G,T,P} (4167137-7340031, default 7340031): +128M Created a new partition 2 of type 'Linux' and of size 127.6 MiB. |
Set the OS partition as bootable and mark the second partition as SWAP type:
1 2 3 4 5 6 7 8 9 10 |
Command (m for help): a Partition number (1,2, default 2): 1 The bootable flag on partition 1 is enabled now. Command (m for help): t Partition number (1,2, default 2): 2 Hex code (type L to list all codes): 82 Changed type of partition 'Linux' to 'Linux swap / Solaris'. |
That’s it, the changes need to be written:
1 2 3 4 5 6 |
Command (m for help): w The partition table has been altered. Calling ioctl() to re-read partition table. Re-reading the partition table failed.: Invalid argument The kernel still uses the old table. The new table will be used at the next reboot or after you run partprobe(8) or kpartx(8). |
6. Refresh tables and update mappings:
1 2 3 |
~# partprobe ; kpartx -uav AlmaLinux-8.4.img add map loop1p1 (253:0): 0 4165089 linear 7:1 2048 add map loop1p2 (253:1): 0 262144 linear 7:1 4167680 |
7. Format new swap partition:
1 2 3 |
~# mkswap /dev/mapper/loop1p2 Setting up swapspace version 1, size = 128 MiB (134213632 bytes) no label, UUID=a35f4ffc-581e-40ad-8326-bc8177c5a736 |
8. Expand OS partition to assign the ~100MB reserve from partition to the filesystem:
1 2 3 4 |
~# resize2fs /dev/mapper/loop1p1 resize2fs 1.45.6 (20-Mar-2020) Resizing the filesystem on /dev/mapper/loop1p1 to 514922 (4k) blocks. The filesystem on /dev/mapper/loop1p1 is now 514922 (4k) blocks long. |
9. Check the changes and note the Sectors count, this will be needed to shrink the image:
1 2 3 4 5 |
~# fdisk -l /dev/loop1 |egrep '(loop1|^Device)' Disk /dev/loop1: 3.5 GiB, 3758096384 bytes, 7340032 sectors Device Boot Start End Sectors Size Id Type /dev/loop1p1 * 2048 4121424 4119377 2G 83 Linux /dev/loop1p2 4121425 4382719 261295 127.6M 82 Linux swap / Solaris |
10. At this point the image mapping can be removed:
1 2 3 4 |
~# kpartx -dv AlmaLinux-8.4.img del devmap : loop1p2 del devmap : loop1p1 loop deleted : /dev/loop1 |
Shrink the image using ‘dd’ tool:
The new size is the sum of Sectors from both partitions, the start size, and the reserve, the result is provided to ‘dd’ via ‘count’ option and the ‘bs’ value must equal to the size of one sector – formula:
1 |
[loop1p1 start sector] + [loop1p1 sectors count] + [loop1p2 sectors count] + [100MB in sectors] |
therefore:
2048 + 4165089 + 262687 + 204800 = 4587520 sectors
1 2 3 4 5 |
# dd if=AlmaLinux-8.4.img of=shrinked-AlmaLinux-8.4.img bs=512 count=4587520 status=progress 2335447552 bytes (2.3 GB, 2.2 GiB) copied, 22 s, 106 MB/s 4587520+0 records in 4587520+0 records out 2348810240 bytes (2.3 GB, 2.2 GiB) copied, 22.1224 s, 106 MB/s |
Compare both images:
1 2 3 4 5 6 7 8 9 10 11 |
~# du -hs AlmaLinux-8.4.img shrinked-AlmaLinux-8.4.img 2.3G AlmaLinux-8.4.img 2.2G shrinked-AlmaLinux-8.4.img ~# du -hs --apparent-size AlmaLinux-8.4.img shrinked-AlmaLinux-8.4.img 3.5G AlmaLinux-8.4.img 2.2G shrinked-AlmaLinux-8.4.img ~# ls -lah AlmaLinux-8.4.img shrinked-AlmaLinux-8.4.img -rw-r--r-- 1 root root 3.5G Sep 15 12:59 AlmaLinux-8.4.img -rw-r--r-- 1 root root 2.2G Sep 15 13:04 shrinked-AlmaLinux-8.4.img |
That’s it.