Linux Lesson 12 of 47

LVM in Depth: Physical Volumes, Volume Groups, Logical Volumes, Resizing & Snapshots

At 3 a.m. a disk-full alert fires. /var/lib/postgresql is at 100%, the database has stopped accepting writes, and the partition it lives on has no room to grow because the next partition on the disk starts immediately after it. On a raw-partition system your options at this hour are all bad: boot a rescue disk and shuffle partitions with parted (praying nothing is off by a sector), or provision a whole new disk, copy the data across, and swap it in during a maintenance window you do not have.

On a system built with LVM — the Logical Volume Manager — the same incident is a two-minute fix you run without unmounting anything: plug in a new disk, add it to the pool, and grow the volume and its filesystem with a single command while PostgreSQL keeps serving. That difference — repartition-at-3 a.m. versus one online command — is the entire reason LVM exists, and why it is the default layout on RHEL, Rocky, Fedora, and the “use LVM” checkbox in the Ubuntu installer.

This lesson builds LVM from first principles: the three layers and the extent that ties them together, how to inspect every layer, the full build from bare disk to mounted filesystem, the resize operations that make LVM worth the trouble, snapshots and thin provisioning, live data migration, and where LVM sits relative to encryption. Everything here runs on any Linux VM, WSL2 instance, or container with a couple of spare loopback files — the hands-on lab uses exactly that so you never need real spare disks.

LVM sits one layer above the disks and partitions themselves. If the words “partition table,” “/dev/sdb1,” or “fstab” are not yet second nature, read the disks, partitions, filesystems & fstab lesson first — LVM assumes you already know what a block device and a mount point are.

Why this matters

A raw partition is a fixed, contiguous range of sectors on one disk. That rigidity is the source of almost every painful storage moment on Linux:

LVM solves all four by inserting an abstraction layer between the physical disks and the filesystems. Instead of formatting a partition directly, you pool your disks into a volume group and carve flexible logical volumes out of the pool. Those logical volumes can grow, shrink, span disks, move between disks live, and be snapshotted — none of which a raw partition can do.

Here is the honest comparison, including the costs, because LVM is not free:

Capability Raw partition LVM
Resize after creation Painful/impossible (must repartition, often offline) Grow online in one command; shrink offline (ext4)
Span multiple disks No — one partition, one disk Yes — an LV can use extents from many disks in the VG
Add capacity to a full volume Only if free space happens to sit right after it Add any disk to the pool, then grow — space can be anywhere
Point-in-time snapshots No Yes — copy-on-write and thin snapshots
Move data off a dying disk live No (copy + swap, offline) Yes — pvmove migrates extents while mounted
Non-contiguous allocation No Yes — extents need not be adjacent
Boot complexity Simple — kernel reads the partition table Extra layer; /boot usually stays a plain partition
Failure blast radius One partition A VG spanning disks: lose one non-mirrored disk and the whole VG is degraded
Overhead / learning curve None Metadata + a mental model to learn (this lesson)

The trade-off worth internalising: LVM buys you enormous flexibility at the cost of a little complexity and one sharp edge — a plain (linear) VG that spans several disks has no redundancy, so if any one disk dies the whole group is compromised. LVM is a volume manager, not a RAID system. For redundancy you put LVM on top of mdadm RAID or hardware RAID (or use LVM’s own RAID/mirror LV types), which we note but do not build here.

The three layers: PV, VG, LV — and the extent that ties them together

LVM is three stacked abstractions. Learn them in order, bottom to top, and everything else follows.

  1. PV — Physical Volume. A whole disk (/dev/sdb) or a partition (/dev/sdb1) that you have initialised for LVM with pvcreate. This writes a small LVM label and metadata area onto the device. A PV is a “brick” LVM is allowed to build with. You do not put a filesystem on a PV.
  2. VG — Volume Group. A pool built from one or more PVs, created with vgcreate. The VG is where capacity lives. Once two 20 GiB PVs are in a VG you stop thinking “disk sdb / disk sdc” and start thinking “one ~40 GiB pool.” You grow the pool by adding PVs (vgextend).
  3. LV — Logical Volume. A slice carved out of the VG’s free space with lvcreate. This is the flexible equivalent of a partition — the thing you actually mkfs and mount. An LV can grow, shrink, span disks, and be snapshotted. It appears as a device node you can use like any block device.

Tying it together is the PE — Physical Extent (and its logical twin, the LE). When you create a VG, its space is divided into fixed-size chunks — 4 MiB by default. Every allocation LVM ever makes is a whole number of extents; there are no half-extents. A 20 GiB PV is 20480 ÷ 4 = 5120 extents. An LV of “20 GiB” is really “5120 logical extents mapped onto 5120 physical extents somewhere in the pool.” This is why you can size things two ways — an absolute size (-L 20G) or a count/percentage of extents (-l 5120, -l 100%FREE) — and why LVM can hand you a volume that is physically scattered across disks yet looks like one contiguous device to the filesystem.

Term Abbrev. What it is Created / grown by You put a filesystem on it?
Physical Volume PV A disk or partition initialised for LVM pvcreate No
Physical Extent PE Fixed-size allocation unit (default 4 MiB) Set at vgcreate (-s) N/A (internal unit)
Volume Group VG Pool of capacity built from PVs vgcreate / vgextend No
Logical Extent LE An extent as seen inside an LV Mapped 1:1 to a PE N/A
Logical Volume LV The flexible “partition” carved from a VG lvcreate / lvextend Yes — this is what you mkfs + mount

A few facts about extents that save you grief later:

Fact about the PE Why it matters
Default size is 4 MiB Fine for almost everyone; you rarely need to change it
Set once, at vgcreate with -s (e.g. -s 16M) Cannot be changed cleanly later without recreating the VG
All sizes round to a whole number of extents -L 10.5G gets rounded; lvs shows the real rounded size
Very large VGs may need a larger PE Historically the PE limited extent count; on modern LVM this is rarely a concern, but a bigger PE reduces metadata for huge pools
An LV’s extents can be non-contiguous and cross PVs This is exactly what lets you grow into free space anywhere in the VG

Every LVM command name follows the same three-letter-prefix pattern, so once you know the pattern you can guess the command. pv* operates on physical volumes, vg* on volume groups, lv* on logical volumes; the suffix is the verb.

Layer Create Scan/list (short) Detail Grow Shrink Remove
PV pvcreate pvs / pvscan pvdisplay (n/a — grow the VG) pvresize (after disk grows) pvremove
VG vgcreate vgs / vgscan vgdisplay vgextend vgreduce vgremove
LV lvcreate lvs / lvscan lvdisplay lvextend (-r) lvreduce (-r) lvremove

That single table is the map for the whole lesson — every command below is one cell in it.

The following diagram is the mental model to hold the entire time. Read it left → right: two raw disks become two PVs, both PVs feed one VG pool, the pool is carved into logical volumes (one of which has a snapshot hanging off it), and only at the far right does a filesystem get created and mounted. Notice the filesystem sits four layers above the physical disk — that indirection is precisely what lets the disk underneath change (grow, move, be replaced) without the filesystem ever knowing.

LVM abstraction stack from raw disks through physical volumes, a volume group pool, logical volumes with a copy-on-write snapshot, up to mounted ext4 and XFS filesystems

Inspecting the stack: pvs, vgs, lvs and lsblk

Before you build anything, learn to read an LVM stack, because 90% of LVM work is inspection. There are two families of display command per layer: the terse, scriptable pvs/vgs/lvs (one line per object, great for a quick overview) and the verbose pvdisplay/vgdisplay/lvdisplay (a full paragraph per object).

# The three-second overview — run all three
sudo pvs      # one line per physical volume
sudo vgs      # one line per volume group
sudo lvs      # one line per logical volume

Typical output on a two-disk pool with two LVs:

$ sudo pvs
  PV         VG      Fmt  Attr PSize   PFree
  /dev/sdb   vg_data lvm2 a--  <20.00g      0
  /dev/sdc   vg_data lvm2 a--  <20.00g <20.00g

$ sudo vgs
  VG      #PV #LV #SN Attr   VSize   VFree
  vg_data   2   2   0 wz--n- <39.99g <20.00g

$ sudo lvs
  LV      VG      Attr       LSize   Pool Origin Data%  Meta%
  lv_data vg_data -wi-ao---- <20.00g
  lv_logs vg_data -wi-ao---- <10.00g

The < you see (<20.00g) means “a little less than” — LVM reserves a sliver of each PV for its own metadata, so a 20 GiB disk yields slightly under 20 GiB of usable extents. That is normal, not a bug.

Here is what the columns actually mean — memorise the load-bearing ones:

Command · column Meaning
pvs · PSize / PFree Total size of the PV, and how much of it is not yet allocated to any LV
pvs · Attr a-- a = allocatable (LVM may use it); a PV can be marked non-allocatable
vgs · #PV / #LV / #SN Count of PVs in the group, LVs carved from it, and snapshots
vgs · VSize / VFree Total pool size and free space left to hand out — VFree is the number you check before lvcreate
lvs · LSize The logical volume’s size
lvs · Pool / Origin For thin volumes, the pool they draw from; for snapshots, the origin LV they shadow
lvs · Data% / Meta% For snapshots and thin pools, how full the copy-on-write / pool data (and metadata) is — watch this so a snapshot does not overflow

The Attr column is a compact status string; the two positions you actually read on an LV are the type (first character) and the state (fifth and sixth):

lvs Attr position Common value Meaning
1 — volume type - linear · s snapshot · o origin · t thin pool · V thin volume · m/r mirror/RAID What kind of LV this is
5 — state a active · - inactive/not activated Whether the LV is currently usable
6 — device open o open (mounted/in use) · - closed Whether something has it open right now

So -wi-ao---- reads as: linear volume, writable, inheritable allocation, active, open (mounted). A snapshot shows swi-a-s--- (snapshot, active) and its origin flips to owi-aos--- (origin).

Two more inspection tools you will lean on constantly:

# lsblk shows the whole tree — disks, partitions, and the LVM devices on top
lsblk

# The device-mapper view underneath LVM (LVM is built on device-mapper)
sudo dmsetup ls
$ lsblk
NAME              MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda                 8:0    0   50G  0 disk
├─sda1              8:1    0    1G  0 part /boot
└─sda2              8:2    0   49G  0 part /
sdb                 8:16   0   20G  0 disk
└─vg_data-lv_data 253:0    0   20G  0 lvm  /data
sdc                 8:32   0   20G  0 disk
└─vg_data-lv_logs 253:1    0   10G  0 lvm  /var/log/app

lsblk is the single best “what is going on with storage here?” command — it shows the physical disk, the lvm type, the device-mapper name (vg_data-lv_data), and the mount point in one tree. Note /boot is a plain partition on sda1, outside LVM — that is the standard layout, because the bootloader needs a simple partition it can read before LVM is available.

Building an LVM stack end to end

Now build the whole thing, bottom to top: disk → PV → VG → LV → filesystem → mount → fstab. This is the sequence you will run on every new server. We use two fresh 20 GiB disks, /dev/sdb and /dev/sdc.

⚠️ pvcreate writes a new label and will destroy any existing filesystem or partition signature on the target device. Triple-check the device name with lsblk first — pvcreate /dev/sda on your system disk is unrecoverable. Everything below assumes /dev/sdb and /dev/sdc are empty, unmounted, spare disks.

Step 1 — initialise the PVs. You can use whole disks or partitions. Whole disks are simplest; partitions (type 8e/8309) are traditional and let you leave room for a non-LVM partition on the same disk.

# Initialise two whole disks as physical volumes
sudo pvcreate /dev/sdb /dev/sdc
  Physical volume "/dev/sdb" successfully created.
  Physical volume "/dev/sdc" successfully created.

Step 2 — create the volume group from both PVs. Name it something meaningful (vg_data, vg_system, rl on RHEL). This is where you can override the extent size with -s, though the 4 MiB default is fine.

sudo vgcreate vg_data /dev/sdb /dev/sdc
  Volume group "vg_data" successfully created

At this point vgs shows one VG of ~40 GiB, all free. You now carve volumes out of it.

Step 3 — create logical volumes. Two sizing styles, and you will use both:

# Absolute size with -L
sudo lvcreate -L 20G -n lv_data vg_data

# Extents / percentage with -l — here, everything left in the VG
sudo lvcreate -l 100%FREE -n lv_logs vg_data
  Logical volume "lv_data" created.
  Logical volume "lv_logs" created.

The -L vs -l distinction trips up beginners, so here it is explicitly:

Flag Takes Examples Use when
-L (capital) An absolute size with a unit -L 20G, -L 500M, -L 1T You know the exact size you want
-l (lowercase) A count of extents or a percentage -l 5120, -l 50%VG, -l 100%FREE, -l 80%PVS You want “half the pool” or “all remaining space” without doing arithmetic
-n The LV name -n lv_data Always — otherwise you get an auto name like lvol0

Percentage suffixes worth knowing: %VG = of the whole volume group, %FREE = of the currently free space, %PVS = of the specified PVs. -l 100%FREE is the single most-used form — “use everything that is left.”

Step 4 — put a filesystem on each LV. The LV is now a block device at two equivalent paths (more on that below). You mkfs it exactly like a partition. The choice of filesystem has one consequence that will matter later — whether you can ever shrink it:

Filesystem Default on Grow Shrink Pick it when
ext4 Debian/Ubuntu Online Offline (yes) General-purpose, universal, well-understood; you might need to shrink later
XFS RHEL/Rocky/Fedora Online Never Large, high-throughput, highly parallel workloads; you are certain you will never shrink

⚠️ mkfs erases everything on the target. Confirm you are pointing at the LV (/dev/vg_data/lv_data), never a whole disk with data.

sudo mkfs.ext4 /dev/vg_data/lv_data     # ext4 for /data
sudo mkfs.xfs  /dev/vg_data/lv_logs     # xfs for logs
mke2fs 1.46.5 (30-Dec-2021)
Creating filesystem with 5242624 4k blocks and 1310720 inodes
Filesystem UUID: 2f4a...c1
Superblock backups stored on blocks: ...
Writing superblocks and filesystem accounting information: done

Step 5 — mount, and make it permanent in /etc/fstab. An LV is reachable at two paths that point at the same device:

Path form Example Notes
/dev/<vg>/<lv> /dev/vg_data/lv_data A friendly symlink LVM creates; easiest to read
/dev/mapper/<vg>-<lv> /dev/mapper/vg_data-lv_data The real device-mapper node. Hyphens in a VG/LV name are doubled here (vg-datavg--data)
UUID=... UUID=2f4a...c1 The filesystem UUID from blkid; survives renames
sudo mkdir -p /data /var/log/app
sudo mount /dev/vg_data/lv_data /data
sudo mount /dev/vg_data/lv_logs /var/log/app

Unlike a raw /dev/sdb1 (whose kernel name can change if you re-cable disks), LVM device-mapper names are stablevg_data-lv_data is always that LV regardless of which physical disk it lives on. That makes both the /dev/mapper/... path and the filesystem UUID safe for /etc/fstab. Get the UUID and append an entry:

# Find the filesystem UUID
sudo blkid /dev/vg_data/lv_data
# /dev/vg_data/lv_data: UUID="2f4a...c1" TYPE="ext4"

Then add to /etc/fstab (device-mapper path shown; UUID is equally valid and preferred for portability):

/dev/mapper/vg_data-lv_data  /data         ext4  defaults  0 2
/dev/mapper/vg_data-lv_logs  /var/log/app  xfs   defaults  0 2
# Validate fstab WITHOUT rebooting into a broken system
sudo findmnt --verify        # sanity-check the fstab syntax/targets
sudo umount /data && sudo mount -a && findmnt /data

⚠️ Always test a new /etc/fstab with mount -a (and findmnt --verify) before rebooting. A typo in fstab can drop the machine to an emergency shell on next boot. mount -a surfaces the error while you still have a shell.

That is the complete build. The reason all this effort pays off is the next section.

Resizing: growing online (the killer feature) and shrinking

Resizing is why LVM earns its place. A raw partition is stuck at its birth size; an LV is elastic.

Growing — usually online, usually one command

The common case: a volume is filling and you want it bigger. Two sub-cases.

Case A — the VG still has free space (vgs shows non-zero VFree). Just extend the LV, and let LVM grow the filesystem in the same breath with -r (--resizefs):

# Grow the LV by 10 GiB AND grow the filesystem on it — one command, stays mounted
sudo lvextend -r -L +10G /dev/vg_data/lv_data
  Size of logical volume vg_data/lv_data changed from 20.00 GiB (5120 extents) to 30.00 GiB (7680 extents).
  Logical volume vg_data/lv_data successfully resized.
resize2fs 1.46.5 (30-Dec-2021)
The filesystem on /dev/mapper/vg_data-lv_data is now 7864320 (4k) blocks long.

The -r flag is the one to remember: it runs resize2fs (ext4) or xfs_growfs (XFS) for you, while the filesystem stays mounted and in use. Both ext4 and XFS support online grow, so there is no downtime. Note the + in -L +10G means “add 10 GiB”; -L 30G (no plus) means “make it exactly 30 GiB.” To consume all remaining VG space at once: sudo lvextend -r -l +100%FREE /dev/vg_data/lv_data.

Case B — the VG is full too (VFree is 0). This is the 3 a.m. scenario. Add a disk to the pool first, then grow:

sudo pvcreate /dev/sdd                 # initialise the new disk as a PV
sudo vgextend vg_data /dev/sdd         # add it to the pool — VG is now bigger
sudo lvextend -r -l +100%FREE /dev/vg_data/lv_data   # grow the LV into the new space
  Physical volume "/dev/sdd" successfully created.
  Volume group "vg_data" successfully extended
  Size of logical volume vg_data/lv_data changed from 30.00 GiB to 50.00 GiB.

No unmount, no reboot, no repartition. That sequence — pvcreatevgextendlvextend -r — is the muscle memory this whole lesson is building toward.

Doing it in two steps (when you skip -r). Sometimes you extend the LV without -r and grow the filesystem separately — useful when scripting, or when you want to grow the LV now and the filesystem in a later window:

sudo lvextend -L +10G /dev/vg_data/lv_data   # grow the LV only
# then, for ext4 — resize2fs takes the DEVICE, works mounted or unmounted:
sudo resize2fs /dev/vg_data/lv_data
# or, for XFS — xfs_growfs takes the MOUNT POINT, filesystem MUST be mounted:
sudo xfs_growfs /var/log/app

Two portable gotchas hide in those two lines: resize2fs is given the device, while xfs_growfs is given the mount point (and XFS must be mounted to grow — you cannot grow an unmounted XFS). Mixing these up is the most common resize error.

The flags that do the heavy lifting on both lvextend and lvreduce:

Flag / form Applies to Meaning
-L +10G / -L -5G extend / reduce A leading +/- means “relative delta”; no sign means “absolute target size”
-l +100%FREE extend Grow by all remaining free extents in the VG (percentage/extent form)
-r / --resizefs both Resize the filesystem too (via fsadmresize2fs/xfs_growfs) in the correct order
device argument both /dev/vg/lv and the short vg/lv are both accepted

Shrinking — offline for ext4, impossible for XFS

Shrinking is where filesystems diverge hard, and where the biggest LVM footgun lives.

⚠️ lvreduce shrinks the LV without touching the filesystem. If you shrink the LV below the filesystem’s size, you slice off live data and corrupt it. The filesystem must always be shrunk first, then the LV. This is the exact reverse of growing.

The rule of ordering, which you must never violate:

Operation Correct order Mnemonic
Grow LV first, then filesystem The container must be big enough before the filesystem expands into it
Shrink Filesystem first, then LV The filesystem must shrink out of the way before you cut the container

For ext4, shrinking works but must be done offline (unmounted). The safe manual sequence:

sudo umount /data
sudo e2fsck -f /dev/vg_data/lv_data          # force a full check first (required by resize2fs)
sudo resize2fs /dev/vg_data/lv_data 15G      # shrink the FILESYSTEM to 15 GiB
sudo lvreduce -L 15G /dev/vg_data/lv_data    # then shrink the LV to match
sudo mount /data

Or let lvreduce -r orchestrate both (it shrinks the filesystem first via fsadm), which is safer because it cannot get the order wrong:

sudo umount /data
sudo lvreduce -r -L 15G /dev/vg_data/lv_data   # -r shrinks FS then LV, in the right order
sudo mount /data

For XFS there is no shrink at all. There is no xfs_shrink, there never has been, and it is not on the roadmap — XFS’s allocation-group design makes it impractical. If you must make an XFS filesystem smaller, the only path is: create a new, smaller LV+XFS, copy the data across (rsync / xfs_copy / restore from backup), swap the mount, and delete the old one. Choose XFS only when you are confident you will never need to shrink it — which, for a root or data volume you might over-allocate, is a real consideration.

The full decision matrix — bookmark this:

Filesystem Grow Shrink
ext4 Online (mounted): lvextend -r -L +N (or lvextend + resize2fs <device>) Offline (unmount): e2fsck -fresize2fs <device> <size>lvreduce -L <size> (or lvreduce -r)
XFS Online (must be mounted): lvextend -r -L +N (or lvextend + xfs_growfs <mountpoint>) Not possible — no shrink tool exists; back up, recreate smaller, restore
Btrfs (aside) Online: btrfs filesystem resize +Ng <mnt> Online: btrfs filesystem resize -Ng <mnt> (Btrfs can shrink online)

The one-line summary you will repeat to colleagues for the rest of your career: everything grows online; ext4 shrinks offline; XFS never shrinks.

Snapshots and thin provisioning

Snapshots: a frozen view for consistent backups

A snapshot is a point-in-time view of an LV. The moment you create it, the snapshot looks exactly like the origin. As the origin changes afterward, LVM uses copy-on-write (COW): before overwriting any block on the origin, it first copies the old block into the snapshot’s reserved space. The snapshot therefore always shows the data as it was at creation time, while the origin keeps serving live traffic.

# Snapshot lv_data. -s = snapshot, -L sizes the COW space, -n names it
sudo lvcreate -s -n lv_data_snap -L 2G /dev/vg_data/lv_data
  Logical volume "lv_data_snap" created.

The -L 2G is not the size of the data — the snapshot appears to be the full size of the origin. It is the size of the COW store: how much changed data it can hold before it overflows. Check its fill level with lvs and watch the Data% column:

$ sudo lvs
  LV           VG      Attr       LSize   Pool Origin  Data%
  lv_data      vg_data owi-aos--- 30.00g
  lv_data_snap vg_data swi-a-s---  2.00g       lv_data 0.42

⚠️ If a classic (thick) snapshot’s Data% reaches 100%, the snapshot is invalidated and dropped by the kernel — you lose the frozen view (the origin is unaffected). Size the COW store for the write churn during the snapshot’s lifetime, not the size of the data. A 500 GiB database that receives 5 GiB of writes during a 20-minute backup needs roughly a 6–8 GiB snapshot, not 500 GiB.

The killer use case is consistent backups of a live system. Snapshot, back up the frozen copy at leisure, then delete the snapshot — the application never stopped:

sudo lvcreate -s -n db_snap -L 8G /dev/vg_data/lv_db    # freeze the DB volume
sudo mkdir -p /mnt/snap
sudo mount -o ro /dev/vg_data/db_snap /mnt/snap         # mount the frozen copy read-only
# ... back up /mnt/snap with tar / rsync / restic / dump ...
sudo umount /mnt/snap
sudo lvremove -y /dev/vg_data/db_snap                   # discard the snapshot when done

For XFS snapshots you may need mount -o ro,nouuid because two filesystems (origin and snapshot) share a UUID. For a truly transaction-consistent database backup, FLUSH/fsfreeze or a DB-level quiesce for the split-second of snapshot creation is still best practice — the snapshot removes the duration problem, not the instant one.

The other superpower is rollback: merge the snapshot back into the origin to undo everything since the snapshot (a risky upgrade, a bad migration). lvconvert --merge does it:

sudo lvconvert --merge /dev/vg_data/lv_data_snap
  Merging of volume vg_data/lv_data_snap started.
  vg_data/lv_data: Merged: 100.00%

If the origin is open (mounted), the merge is deferred until the next activation, and you will see: Can't merge over open origin volume. Merging will occur on next activation. Unmount the origin (or reboot) to complete it. After a merge, the snapshot is automatically removed.

Everything about classic snapshots in one table:

Aspect Detail
Create lvcreate -s -n <snap> -L <cow-size> /dev/<vg>/<origin>
Mechanism Copy-on-write — old blocks copied to the snapshot before origin is overwritten
-L sizes The COW store (space for changed blocks), not the data
Monitor lvsData%; alert well before 100%
Overflow Thick snapshot at 100% → invalidated and dropped (origin safe)
Consistent backup Mount snapshot read-only, back it up, then lvremove it
Rollback lvconvert --merge (deferred if origin is open)
Performance cost Every origin write triggers a copy → write amplification while the snapshot exists
Remove lvremove /dev/<vg>/<snap>

Thin provisioning: overcommit on purpose

Classic snapshots and volumes are thick — every extent is allocated up front, and every snapshot needs a fixed COW store you size by hand. Thin provisioning flips this: you create a thin pool of real space, then create thin volumes whose virtual size can exceed the pool. Blocks are allocated from the pool only when actually written.

# Create a 10 GiB thin pool, then a 50 GiB thin volume backed by it
sudo lvcreate -L 10G -T vg_data/pool0                 # -T (--thin) makes a thin POOL
sudo lvcreate -V 50G -T vg_data/pool0 -n lv_thin      # -V = virtual size, from the pool
  Thin pool volume with chunk size 64.00 KiB can address at most 15.81 TiB of data.
  Logical volume "pool0" created.
  Logical volume "lv_thin" created.
$ sudo lvs
  LV      VG      Attr       LSize  Pool  Origin Data%  Meta%
  pool0   vg_data twi-aotz-- 10.00g              0.00   10.55
  lv_thin vg_data Vwi-a-tz-- 50.00g pool0        0.00

The 50 GiB thin volume is backed by only 10 GiB of real space — you have overprovisioned by 5×. Thin snapshots are also far more efficient: they need no fixed COW size, do not slow the origin down the way thick snapshots do, and you can snapshot a snapshot.

⚠️ Overprovisioning means the pool can run out of real space while volumes still think they have room. When a thin pool fills, writes fail and filesystems can corrupt. You must monitor Data% on the pool and enable auto-extend (thin_pool_autoextend_threshold / thin_pool_autoextend_percent in /etc/lvm/lvm.conf, driven by dmeventd). Thin provisioning is powerful but it converts a “disk full” annoyance into a potential data-loss event if you ignore the pool’s fill level.

Thick (classic) Thin
Space allocation Up front, fully reserved On write, from a shared pool
Overcommit No Yes — virtual size can exceed real space
Snapshot cost Fixed COW store you size by hand Cheap; no fixed size; snapshot-of-snapshot
Snapshot overflow Snapshot dropped at 100% Pool exhaustion → writes fail / corruption risk
Main risk Wasted space; snapshot fills Pool fills silently → monitor Data% + auto-extend
Create lvcreate -L … / lvcreate -s … lvcreate -T (pool) + lvcreate -V -T (volume)

Migrating and removing: pvmove and tearing the stack down

pvmove — evacuate a dying disk while it stays online

A disk starts throwing SMART errors. You need its data off it without downtime. pvmove relocates all the extents on one PV to other PVs in the same VG, live, while filesystems stay mounted:

# Move every extent off /dev/sdb to anywhere else in the VG that has room
sudo pvmove /dev/sdb
  /dev/sdb: Moved: 5.1%
  /dev/sdb: Moved: 37.4%
  /dev/sdb: Moved: 100.0%

You can target a destination (sudo pvmove /dev/sdb /dev/sdc) or move a single LV’s extents (sudo pvmove -n lv_data /dev/sdb). pvmove is restartable — if interrupted, rerunning pvmove (no args) resumes it. Once the PV is empty, remove it from the VG and de-initialise it, then physically pull the disk:

sudo vgreduce vg_data /dev/sdb     # remove the now-empty PV from the VG
sudo pvremove /dev/sdb             # wipe the LVM label off the disk
  Removed "/dev/sdb" from volume group "vg_data"
  Labels on physical volume "/dev/sdb" successfully wiped.

This is the capability that has no equivalent with raw partitions — swapping a failing disk with zero service interruption.

Tearing the whole stack down (in reverse)

You build bottom-up; you tear down top-down, unmounting first. Skip a step or reverse the order and you get “device is busy” or “in use” errors.

⚠️ lvremove destroys the logical volume and all its data instantly. There is no recycle bin. Double-check the LV name and that you have a backup.

sudo umount /data /var/log/app                 # 1. unmount everything on the VG
# 2. remove the fstab lines so it doesn't try to mount next boot
sudo lvremove -y /dev/vg_data/lv_data /dev/vg_data/lv_logs   # 3. delete the LVs
sudo vgremove vg_data                          # 4. delete the (now empty) VG
sudo pvremove /dev/sdb /dev/sdc                # 5. de-initialise the PVs
Order Command Undoes Note
1 umount mount Fails if anything holds the mount open (lsof/fuser -m)
2 edit /etc/fstab the fstab entry Otherwise next boot may hang on the missing device
3 lvremove lvcreate Destroys data. Remove snapshots first if any
4 vgremove vgcreate VG must have no LVs left
5 pvremove pvcreate VG must no longer claim the PV (vgreduce first if still a member)

One layer down: where LVM meets encryption (LUKS)

Full-disk encryption on Linux uses LUKS (via cryptsetup), and it stacks with LVM in one of two orders, which you choose deliberately. LVM-on-LUKS (“encrypt, then pool”) puts LUKS at the bottom: you encrypt the raw disk/partition, open it to a /dev/mapper/cryptdev, run pvcreate on that, and build the VG and all LVs inside the encrypted container — so one passphrase unlocks everything and every volume (including swap) is encrypted. This is the standard whole-disk-encryption layout the Ubuntu and Fedora installers produce. LUKS-on-LVM (“pool, then encrypt”) is the reverse: build LVM first, then run cryptsetup luksFormat on individual LVs — giving you per-volume keys (different passphrase per LV, encrypt only the volumes that need it) at the cost of unlocking each separately. Rule of thumb: LVM-on-LUKS for simple one-unlock full-disk encryption; LUKS-on-LVM for granular per-volume control. /boot stays unencrypted (or uses a detached header) either way, because the bootloader must read it before any key exists.

Layout Stack (bottom → top) Unlock Best for
LVM-on-LUKS disk → LUKS → PV → VG → LVs One passphrase unlocks the whole VG Simple full-disk encryption (Ubuntu/Fedora installer default)
LUKS-on-LVM disk → PV → VG → LV → LUKS Per-LV passphrase, unlocked separately Granular per-volume keys; encrypt only some LVs

Hands-on lab

This lab builds, inspects, grows, snapshots, and tears down a complete LVM stack without any real spare disks — it uses loopback files as stand-in block devices, so it runs on any Linux VM, WSL2, or container where you have sudo and the lvm2 package. Everything is confined to files under /root, so there is nothing to accidentally destroy on your real disks.

First, make sure LVM is installed:

# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y lvm2
# RHEL/Rocky/Fedora
sudo dnf install -y lvm2

Step 1 — create three fake disks as loopback devices.

sudo mkdir -p /root/lvmlab
# Three 1 GiB sparse files
for d in disk1 disk2 disk3; do
  sudo truncate -s 1G /root/lvmlab/$d.img
done
# Attach each file to a loop device; --show prints the /dev/loopN name
sudo losetup --show -f /root/lvmlab/disk1.img
sudo losetup --show -f /root/lvmlab/disk2.img
sudo losetup --show -f /root/lvmlab/disk3.img
lsblk | grep loop

You should see three loop devices (e.g. /dev/loop0, /dev/loop1, /dev/loop2). What just happened: losetup made three regular files behave like block devices — perfect PVs for practice. Substitute your actual loop names below.

Step 2 — build the stack: PV → VG → LV.

sudo pvcreate /dev/loop0 /dev/loop1          # two PVs (leave loop2 spare for later)
sudo vgcreate vg_lab /dev/loop0 /dev/loop1   # pool them (~2 GiB)
sudo lvcreate -L 500M -n lv_app vg_lab       # a 500 MiB LV
sudo vgs vg_lab && sudo lvs vg_lab

What just happened: you have a ~2 GiB pool with one 500 MiB volume; vgs shows ~1.5 GiB VFree.

Step 3 — filesystem, mount, write a file.

sudo mkfs.ext4 /dev/vg_lab/lv_app
sudo mkdir -p /mnt/lvapp
sudo mount /dev/vg_lab/lv_app /mnt/lvapp
echo "hello from LVM $(date)" | sudo tee /mnt/lvapp/marker.txt
df -h /mnt/lvapp

What just happened: a real ext4 filesystem on your LV, mounted at /mnt/lvapp, with a file in it. df -h shows ~500 MiB.

Step 4 — grow it online (the killer feature). Notice the filesystem stays mounted throughout.

sudo lvextend -r -L +300M /dev/vg_lab/lv_app   # grow LV + FS in one shot
df -h /mnt/lvapp                                # now ~800 MiB, still mounted
cat /mnt/lvapp/marker.txt                        # data intact

What just happened: the volume and its filesystem grew by 300 MiB with no unmount and no data loss — marker.txt is still there.

Step 5 — add the spare disk to the full pool, then grow into it.

sudo pvcreate /dev/loop2
sudo vgextend vg_lab /dev/loop2                 # pool is now ~3 GiB
sudo lvextend -r -l +100%FREE /dev/vg_lab/lv_app  # grab all remaining space
df -h /mnt/lvapp

What just happened: you rehearsed the 3 a.m. rescue — added capacity and grew a live volume onto a brand-new disk.

Step 6 — snapshot, change data, roll back with a merge.

sudo lvcreate -s -n lv_app_snap -L 200M /dev/vg_lab/lv_app   # freeze the volume
echo "OOPS destructive change" | sudo tee /mnt/lvapp/marker.txt   # simulate a bad change
sudo lvs vg_lab                                              # see the snapshot + Data%
sudo umount /mnt/lvapp                                       # merge needs origin closed
sudo lvconvert --merge /dev/vg_lab/lv_app_snap              # roll back to the snapshot
sudo mount /dev/vg_lab/lv_app /mnt/lvapp
cat /mnt/lvapp/marker.txt                                    # the ORIGINAL line is back

What just happened: the snapshot let you undo a destructive change — after the merge, marker.txt shows the original “hello from LVM” line, and the snapshot is gone.

Step 7 — tear it all down and detach the loop devices.

sudo umount /mnt/lvapp
sudo lvremove -y vg_lab
sudo vgremove -y vg_lab
sudo pvremove /dev/loop0 /dev/loop1 /dev/loop2
sudo losetup -D                                 # detach ALL loop devices
sudo rm -rf /root/lvmlab

What just happened: a clean top-down teardown, and the loopback files are gone. You built and destroyed a full LVM stack without ever touching a real disk. Run the whole lab twice — the second time it will feel like muscle memory.

Common mistakes and troubleshooting

Symptom Likely cause Fix
Grew the LV but df still shows old size You ran lvextend without -r and forgot to grow the filesystem resize2fs /dev/vg/lv (ext4) or xfs_growfs <mountpoint> (xfs). Next time use lvextend -r
lvreduce “succeeded” but the filesystem is now corrupt Shrunk the LV below the filesystem size — cut off live data Restore from backup. Always shrink the filesystem first, or use lvreduce -r. Never lvreduce a raw -L without shrinking the FS
“Can’t shrink XFS” / no xfs_shrink exists XFS cannot be shrunk, by design Create a smaller new LV+XFS, copy data over, swap the mount
Snapshot disappeared during a long backup Thick snapshot’s COW store hit 100% (Data%) → invalidated Recreate with a larger -L; monitor lvs Data%; consider thin snapshots
vgextend/lvcreate says “no space” but the disk is huge You added the disk but never pvcreate’d it, or never vgextend’d it into the VG pvcreate /dev/sdX then vgextend <vg> /dev/sdX; confirm with vgs VFree
Device /dev/sdb excluded by a filter on pvcreate Existing partition table / filesystem signature, or an LVM filter in lvm.conf Confirm it’s the right disk, then wipefs -a /dev/sdb (destroys data); or pvcreate -ff to force
Boot drops to emergency shell after adding an LV Bad /etc/fstab entry (wrong device path or nofail missing for optional mounts) Fix at the emergency shell; always mount -a + findmnt --verify before rebooting
umount/lvremove says “device is busy” A process or mount still holds the LV open sudo fuser -m /mnt/x or lsof /mnt/x to find it; unmount, then remove
LVs missing after a reboot / disk move VG not activated, or a PV is missing sudo vgscan && sudo vgchange -ay; check all PVs present with pvs
Thin pool full, writes failing everywhere Overprovisioned pool hit 100% real space Extend the pool (lvextend -L +Ng vg/pool0); enable auto-extend in lvm.conf to prevent recurrence

The three that bite hardest, in prose:

Forgetting -r (or growing the wrong layer). New LVM users almost always run lvextend -L +20G alone, then wonder why df shows no change. LVM grew the container; the filesystem inside it still thinks it is the old size. The block device and the filesystem are separate things. Either always use -r (grow both atomically), or remember the two-step and — critically — remember that resize2fs wants the device while xfs_growfs wants the mount point.

Shrinking in the wrong order. This is the one that loses data. Growing is forgiving because you enlarge the container before the filesystem fills it. Shrinking is unforgiving because lvreduce will happily cut the LV to a size smaller than the filesystem living on it, truncating live data with no warning that survives. The safe reflex: for any shrink, either use lvreduce -r (which shrinks the filesystem first) or manually resize2fs <device> <smaller-size> before lvreduce — and never attempt it on XFS at all.

Snapshot overflow. A classic snapshot with too small a COW store silently dies mid-backup the instant total changed data exceeds its size, and you discover it only when the backup you thought you had is gone. Size snapshots for the write volume during their lifetime, watch Data%, and for anything long-lived prefer thin snapshots, which draw from a shared pool instead of a hand-sized COW store.

Cheat-sheet

Task Command
Init a disk/partition as a PV pvcreate /dev/sdb
List / detail PVs pvs · pvdisplay · pvscan
Create a VG from PVs vgcreate vg_data /dev/sdb /dev/sdc
Add a disk to a VG pvcreate /dev/sdd && vgextend vg_data /dev/sdd
List / detail VGs vgs · vgdisplay · vgscan
Create an LV (size) lvcreate -L 20G -n lv_data vg_data
Create an LV (all free space) lvcreate -l 100%FREE -n lv_data vg_data
List / detail LVs lvs · lvdisplay · lvscan
Whole storage tree lsblk · lsblk -f (with filesystems)
Filesystem on an LV mkfs.ext4 /dev/vg_data/lv_data · mkfs.xfs …
Grow LV + FS online lvextend -r -L +10G /dev/vg_data/lv_data
Grow into all free space lvextend -r -l +100%FREE /dev/vg_data/lv_data
Grow FS separately (ext4) resize2fs /dev/vg_data/lv_data
Grow FS separately (xfs) xfs_growfs /data (mount point)
Shrink ext4 (offline, safe) umountlvreduce -r -L 15G /dev/vg_data/lv_datamount
Shrink XFS ❌ impossible — recreate smaller + copy
Create a snapshot lvcreate -s -n snap -L 2G /dev/vg_data/lv_data
Roll back a snapshot lvconvert --merge /dev/vg_data/snap
Thin pool + thin volume lvcreate -L 10G -T vg/pool0lvcreate -V 50G -T vg/pool0 -n lv_thin
Migrate data off a disk (live) pvmove /dev/sdb
Remove a PV from a VG vgreduce vg_data /dev/sdb
Delete LV / VG / PV lvremove … · vgremove … · pvremove …
Activate all VGs (after boot/move) vgscan && vgchange -ay
Rescan device sizes into a PV pvresize /dev/sdb

Interview and exam questions

Q: What are the three layers of LVM, bottom to top, and which one do you put a filesystem on? A: Physical Volume (PV — an initialised disk/partition), Volume Group (VG — a pool of PVs), Logical Volume (LV — a slice of the VG). You mkfs and mount the LV; PVs and VGs never hold a filesystem directly.

Q: What is a physical extent (PE) and why does it matter? A: The fixed-size allocation unit of a VG, 4 MiB by default, set once at vgcreate with -s. Every LVM allocation is a whole number of extents, which is what lets an LV be non-contiguous and span multiple PVs while presenting as one continuous device.

Q: A production volume is 90% full and the VG has no free space. Walk me through growing it with zero downtime. A: pvcreate /dev/sdNEW to init the new disk, vgextend vg_name /dev/sdNEW to add it to the pool, then lvextend -r -l +100%FREE /dev/vg_name/lv_name to grow the LV and its filesystem online. No unmount, no reboot.

Q: What does the -r flag in lvextend -r do, and what happens if you forget it? A: -r (--resizefs) grows the filesystem to match the enlarged LV in the same command (calling resize2fs or xfs_growfs). Forget it and only the LV grows — df shows no extra space until you run the filesystem resize yourself.

Q: Can you shrink an XFS filesystem? An ext4 one? A: XFS: no — there is no shrink tool; you must recreate a smaller filesystem and copy the data. ext4: yes, but offline — unmount, e2fsck -f, resize2fs the filesystem down, then lvreduce the LV (or lvreduce -r, which orders it for you). Always shrink the filesystem before the LV.

Q: Why is the order of operations reversed between growing and shrinking? A: Growing enlarges the LV first so the filesystem has room to expand into; shrinking must shrink the filesystem first so it moves its data out of the region you are about to cut from the LV. Shrink the LV first and you truncate live filesystem data.

Q: How does an LVM snapshot work, and how do you size it? A: Copy-on-write: the snapshot starts identical to the origin, and before any origin block is overwritten LVM copies the old block into the snapshot’s COW store. Size the COW store (-L) for the amount of data that will change during the snapshot’s lifetime, not the size of the whole volume. If it fills to 100%, a thick snapshot is dropped.

Q: How do you take a consistent backup of a live database volume using LVM? A: Create a snapshot of the DB’s LV (ideally quiescing/flushing the DB for the instant of creation), mount the snapshot read-only, back it up at leisure with tar/rsync/restic, then lvremove the snapshot. The database never stops serving.

Q: What is thin provisioning and what is its main danger? A: A thin pool of real space backing thin volumes whose virtual size can exceed it (overprovisioning); blocks are allocated on write. The danger is pool exhaustion — if the pool fills, writes fail and filesystems can corrupt — so you must monitor the pool’s Data% and enable auto-extend.

Q: A disk is failing and you must evacuate its data without downtime. Which command? A: pvmove /dev/sdBAD relocates all its extents to other PVs in the VG while everything stays mounted; then vgreduce vg /dev/sdBAD and pvremove /dev/sdBAD, and pull the disk.

Q (RHCSA-style task): Given a new 10 GiB disk /dev/vdb, create a volume group datavg with 8 MiB extents, a 5 GiB logical volume datalv, format it XFS, and mount it persistently at /data. A:

sudo pvcreate /dev/vdb
sudo vgcreate -s 8M datavg /dev/vdb
sudo lvcreate -L 5G -n datalv datavg
sudo mkfs.xfs /dev/datavg/datalv
sudo mkdir -p /data
echo '/dev/mapper/datavg-datalv /data xfs defaults 0 2' | sudo tee -a /etc/fstab
sudo mount -a && findmnt /data

Q (LFCS-style task): Extend datalv by 2 GiB and confirm the filesystem grew, without unmounting. A: sudo lvextend -r -L +2G /dev/datavg/datalv then df -h /data — the -r grows the XFS online with xfs_growfs, so no unmount is needed.

Key takeaways

Once LVM is second nature, the natural next steps are protecting what lives on it — consistent backup and bare-metal recovery workflows that build directly on the snapshot technique above — and understanding where these volumes mount in the wider tree, covered in the filesystem hierarchy & navigation lesson. LVM is the foundation every serious storage capability on Linux is built on; you now own it.

linuxlvmpvcreatevgcreatelvcreatelvextendresize2fsxfssnapshotsthin-provisioningdevice-mapperstorageluksrhcsa
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments