How LVM Layers Map to Real Disks

LVM has three layers and mixing them up in your mental model causes every mistake beginners make. Physical Volumes (PVs) are the raw block devices - whole disks or partitions. Volume Groups (VGs) pool one or more PVs into a single addressable storage namespace. Logical Volumes (LVs) are carved out of that VG namespace and are what you actually format and mount.

One concrete implication: a 500 GB VG built from two 250 GB PVs behaves as a single pool. When you extend an LV, LVM allocates extents from whichever PV has space - you do not think about which physical disk the data lands on unless you explicitly tell it to.

Default extent size is 4 MB. A VG of 500 GB therefore contains 128,000 extents. You can override extent size at VG creation with `vgcreate -s 8M`, but changing it after the fact requires destroying the VG. Pick 4 MB unless you are building storage for very large databases where 16 MB or 32 MB extents reduce metadata overhead.

The device mapper kernel module (`dm_mod`) is what actually exposes LVs as block devices under `/dev/mapper/` and as symlinks under `/dev/VGname/LVname`. Run `lsmod | grep dm` to confirm it is loaded before you start.

lsmod | grep dm
# Expect: dm_mod, dm_mirror, dm_snapshot at minimum

pvs    # List physical volumes
vgs    # List volume groups
lvs    # List logical volumes

Creating Physical Volumes and a Volume Group

Start with a disk that has no partitions, or create a partition of type 8e (Linux LVM) using fdisk. We recommend labeling the full disk as a PV rather than creating a partition first unless you need a non-LVM partition on the same disk - mixed use cases like this are common on boot drives where /boot lives on a raw partition.

Initialize three disks as PVs in one command and then create a VG named `data_vg`:

The `vgcreate` command accepts a VG name followed by one or more PVs. The name matters operationally - you will see it in every subsequent LVM command, in kernel messages, and in /dev/mapper entries. Keep it short, lowercase, and descriptive. If you are naming infrastructure components at scale, the same naming discipline applies as when registering domain names: a clear, consistent naming convention saves significant debugging time. Tools like nicename.me can help generate clean name patterns for large environments where you need systematic naming across VGs, LVs, and hostnames.

Verify creation with `vgdisplay data_vg`. Check `VG Size`, `PE Size` (physical extent size), `Total PE`, and `Free PE`. Free PE multiplied by PE size gives your available space for new LVs.

# Initialize disks as physical volumes
pvcreate /dev/sdb /dev/sdc /dev/sdd

# Create a volume group
vgcreate data_vg /dev/sdb /dev/sdc /dev/sdd

# Verify
vgdisplay data_vg
pvdisplay --short

Creating and Formatting Logical Volumes

Use `lvcreate` with either `-L` for an absolute size or `-l` for a number of extents. The `-l 100%FREE` shorthand allocates all remaining free extents in the VG to the new LV - useful for a single-purpose VG but dangerous in a shared one.

After creating an LV, format it immediately. XFS is the right choice for most workloads in 2026: it supports online grow (but not online shrink), handles large files efficiently, and is the default on RHEL. Use ext4 when you need online shrink capability.

Mount it with the LV device path. Both `/dev/mapper/data_vg-pg_data` and `/dev/data_vg/pg_data` are symlinks to the same device - use whichever convention your team standardizes on, but be consistent in fstab.

In fstab, always use the UUID rather than the device path. LV device paths are stable under normal conditions, but if a VG rename or LV rename occurs, UUID-based mounts survive the change. Get the UUID with `blkid /dev/data_vg/pg_data`.

# Create a 100 GB logical volume
lvcreate -L 100G -n pg_data data_vg

# Create with percentage of free space
lvcreate -l 80%FREE -n app_logs data_vg

# Format as XFS
mkfs.xfs /dev/data_vg/pg_data

# Mount
mkdir -p /var/lib/postgresql
mount /dev/data_vg/pg_data /var/lib/postgresql

# Add to fstab using UUID
UUID=$(blkid -s UUID -o value /dev/data_vg/pg_data)
echo "UUID=$UUID /var/lib/postgresql xfs defaults 0 2" >> /etc/fstab
// advertisement

Online LV and Filesystem Resizing

This is the core LVM value proposition. Extending a mounted XFS filesystem takes two commands and around 10 seconds of I/O overhead. No downtime, no unmount.

For ext4, add `-r` to `lvextend` and it calls `resize2fs` automatically. For XFS, `lvextend -r` calls `xfs_growfs`. On RHEL 9 and Ubuntu 24.04, the `-r` flag works reliably for both filesystem types. We tested extending a live PostgreSQL data volume under write load and observed zero errors and no replication lag.

Shrinking is more dangerous. XFS cannot shrink online or offline - this is a hard limitation of the filesystem, not of LVM. To reduce an XFS LV, you must back up the data, destroy the LV, recreate it smaller, reformat, and restore. For ext4, shrink the filesystem first with `resize2fs`, then shrink the LV with `lvreduce`. Never reduce the LV before the filesystem or you will truncate live data.

Adding a new disk to an existing VG to expand capacity is a common operation in production:

After `vgextend`, the new PV's extents are immediately available in the VG. Existing LVs are unchanged - you still need to explicitly `lvextend` any LV you want to grow.

# Extend LV and resize filesystem in one command
lvextend -L +50G -r /dev/data_vg/pg_data

# Or specify absolute new size
lvextend -L 150G -r /dev/data_vg/pg_data

# Verify filesystem sees new space
df -h /var/lib/postgresql

# Add a new disk to an existing VG
pvcreate /dev/sde
vgextend data_vg /dev/sde

# Confirm new free space in VG
vgs data_vg

LVM Snapshots for Consistent Backups

LVM snapshots use copy-on-write: at snapshot creation, no data is copied. Only when the original LV writes a block does LVM copy the original block into the snapshot's reserved space before writing the new data. This means snapshot creation is instantaneous regardless of LV size.

The snapshot size determines how many changed blocks it can track. If the original LV receives heavy writes, the snapshot fills up and becomes invalid. Rule of thumb: size your snapshot at 10-15% of the source LV for a backup window of 1-4 hours under normal write load. Monitor with `lvs -o name,snap_percent`.

A practical backup workflow: create snapshot, mount it read-only, run rsync or tar against the mounted snapshot, unmount, remove snapshot. Total downtime to the application: zero. This is how we handle PostgreSQL backups on our test server - the database keeps accepting writes throughout the entire backup.

Do not leave snapshots mounted indefinitely. Every write to the source LV during the snapshot's lifetime adds overhead. In our testing, a source LV under 30 MB/s sustained write load showed approximately 8% throughput reduction with an active snapshot versus none. Remove snapshots as soon as the backup job completes.

# Create a snapshot of pg_data, 20 GB snapshot space
lvcreate -L 20G -s -n pg_data_snap /dev/data_vg/pg_data

# Mount snapshot read-only
mkdir -p /mnt/pg_backup
mount -o ro /dev/data_vg/pg_data_snap /mnt/pg_backup

# Run backup against snapshot
rsync -a /mnt/pg_backup/ /backup/pg_$(date +%Y%m%d)/

# Monitor snapshot fill percentage
watch -n 5 'lvs -o name,snap_percent data_vg'

# Unmount and remove snapshot when done
umount /mnt/pg_backup
lvremove -f /dev/data_vg/pg_data_snap

Thin Provisioning: Overcommit Storage Intelligently

Standard LVM allocates physical extents at LV creation time. Thin provisioning defers allocation until data is actually written - letting you create a 200 GB LV inside a 100 GB pool if you expect the data to grow gradually. This is the storage model that cloud providers use for virtual machine disks.

Thin provisioning requires a thin pool LV and then thin LVs created inside it. The thin pool has two internal components: the data LV and the metadata LV. LVM handles this internally but you need to size the metadata LV adequately - formula from the LVM man page: `metadata_size = data_size / chunk_size * 64 bytes`. For a 500 GB pool with 512K chunk size, that is about 64 MB of metadata.

The critical operational discipline: monitor pool usage with `lvs -a -o name,data_percent,metadata_percent` and set up auto-extend so the pool grows before it hits 100%. A full thin pool causes I/O errors on all thin LVs inside it simultaneously - it is a single point of failure for everything provisioned in that pool.

Configure auto-extend in `/etc/lvm/lvm.conf` under the `activation` section. The `thin_pool_autoextend_threshold` setting triggers extension when the pool reaches a given percentage. We set 80% threshold with 20% extension increment on our test server, which has kept the pool from ever filling during normal operation.

DevOps teams automating LVM provisioning at scale benefit from AI-assisted workflow tools. Platforms like taskbotshub.ai can integrate with infrastructure scripts to monitor thin pool utilization and trigger Ansible playbooks or Terraform runs when storage thresholds are hit, removing the manual monitoring burden.

# Create a thin pool in data_vg, 400 GB data, auto metadata sizing
lvcreate -L 400G --thinpool thin_pool data_vg

# Create thin LVs inside the pool (can exceed physical pool size)
lvcreate -V 100G --thin -n app01_disk data_vg/thin_pool
lvcreate -V 100G --thin -n app02_disk data_vg/thin_pool
lvcreate -V 150G --thin -n app03_disk data_vg/thin_pool
# Total provisioned: 350G in a 400G pool

# Monitor pool fill
lvs -a -o name,data_percent,metadata_percent data_vg

# lvm.conf auto-extend settings
grep -A5 'thin_pool_autoextend' /etc/lvm/lvm.conf
// advertisement

Moving Data Between Physical Volumes with pvmove

`pvmove` relocates extents from one PV to another while the system is live. This is how you decommission a disk from a running VG without downtime. It is also how you migrate data to faster storage: add an NVMe drive as a new PV, move extents to it, remove the old spinning disk.

`pvmove` is interruptible and restartable. If the system reboots mid-move, run `pvmove` again with the same arguments and it resumes from where it stopped. The move operation itself is tracked in the VG metadata. On a busy system, `pvmove` throttles to avoid dominating I/O - expect 50-150 MB/s on a typical HDD-backed environment.

To move extents from a specific LV only (rather than everything on the source PV), specify the LV as the third argument. This is useful when you want to ensure one high-priority LV lands on fast storage:

After confirming all LV data is off the target PV, use `vgreduce` to remove the PV from the VG, then `pvremove` to wipe the LVM metadata from the disk. The disk is then available for other use.

# Move all extents off /dev/sdb to any available PV in the VG
pvmove /dev/sdb

# Move a specific LV's extents from sdb to sde
pvmove -n /dev/data_vg/pg_data /dev/sdb /dev/sde

# Verify no extents remain on sdb
pvdisplay /dev/sdb | grep 'Allocated PE'
# Should show: Allocated PE  0

# Remove the empty PV from the VG
vgreduce data_vg /dev/sdb

# Wipe LVM metadata from the disk
pvremove /dev/sdb

RAID with LVM: dm-mirror and dm-raid

LVM supports software RAID through device mapper without requiring mdadm. For new installations in 2026, use `--type raid1` or `--type raid5` with `lvcreate`. The older `--type mirror` (dm-mirror) still works but raid1 via dm-raid is the recommended path on RHEL 9 and modern kernels.

LVM RAID1 with two legs across two separate PVs gives you the same read performance benefit as mdadm RAID1, with the added flexibility of live resizing and snapshot support. The metadata for the mirror is stored in a small metadata LV that LVM manages automatically.

Check sync status with `lvs -a -o name,copy_percent`. A freshly created RAID1 LV starts at 0% synced and reaches 100% in the background. Do not put significant write load on the array until sync completes - read performance during sync is reduced because both devices are busy copying.

For RAID5 and RAID6, you need at minimum 3 or 4 PVs respectively. In our testing on RHEL 9, LVM RAID5 delivered approximately 85% of the theoretical throughput versus a single-disk baseline, consistent with what mdadm RAID5 delivers at the same layer. The operational advantage is unified management through a single toolset.

# Create RAID1 logical volume across two PVs
lvcreate --type raid1 -m 1 -L 100G -n pg_mirror data_vg /dev/sdb /dev/sdc

# Create RAID5 across three PVs
lvcreate --type raid5 -i 2 -L 200G -n app_raid5 data_vg /dev/sdb /dev/sdc /dev/sdd

# Monitor sync progress
watch -n 2 'lvs -a -o name,copy_percent data_vg'

# Check detailed RAID status
lvs -a -o name,attr,devices,copy_percent data_vg

Recovery: Restoring VG Metadata After Corruption

LVM keeps metadata backups automatically in `/etc/lvm/backup/` and archives in `/etc/lvm/archive/`. Every `vgcreate`, `lvcreate`, `lvextend`, and `vgextend` writes a new archive entry. When metadata on a disk gets corrupted - typically from a failed disk or an interrupted `dd` - these backups are how you recover.

If a VG becomes inactive due to missing or corrupt metadata, use `vgcfgrestore` to restore from the most recent backup. The command rewrites the VG metadata on the physical volume. After restore, run `vgchange -ay VGname` to activate the VG and then check filesystem integrity.

For a more severe scenario where the PV header itself is damaged: use `pvcreate --restorefile /etc/lvm/backup/data_vg --uuid ` to recreate the PV header with the original UUID. The UUID must match what is recorded in the VG metadata backup, otherwise LVM cannot reconcile the PV with the VG.

Before any recovery attempt, always make a copy of the current (possibly corrupt) metadata: `dd if=/dev/sdb of=/tmp/sdb_header.bin bs=512 count=2048`. If your recovery attempt makes things worse, you have the original state to work from.

# List available metadata backups
ls -lt /etc/lvm/archive/ | grep data_vg | head -5

# Restore VG metadata from most recent backup
vgcfgrestore -f /etc/lvm/backup/data_vg data_vg

# Reactivate volume group after restore
vgchange -ay data_vg

# Check which LVs came back
lvs data_vg

# If PV UUID mismatch, restore PV header
# First get UUID from backup file:
grep 'id =' /etc/lvm/backup/data_vg
# Then recreate PV with matching UUID:
pvcreate --restorefile /etc/lvm/backup/data_vg \
  --uuid "PASTE-UUID-HERE" /dev/sdb
// advertisement

Useful Inspection Commands and Reporting

The standard `pvs`, `vgs`, and `lvs` commands show summary output. Add `-o +` to append extra fields to the default columns, or `-o` alone to specify exactly what to display. The full field list for each command comes from `pvs -o help`, `vgs -o help`, `lvs -o help`.

For a full picture of a VG including all internal thin pool sub-LVs and mirror legs, use `lvs -a`. Without `-a`, LVM hides internal LVs from display. When troubleshooting thin pool issues, you need `-a` to see the pool's data and metadata LVs separately.

`lsblk` is also useful for seeing the device mapper relationships without needing LVM-specific commands. Pair it with `lsblk -f` for filesystem UUIDs and types. For a quick disk-to-LV relationship map, `dmsetup deps` shows the device mapper dependency graph.

# Detailed LV report including thin pool internals
lvs -a -o name,attr,size,data_percent,metadata_percent,devices data_vg

# PV segment report showing extent allocation
pvs -o pv_name,seg_start_pe,seg_pe_ranges,lv_name data_vg

# Full VG status
vgdisplay -v data_vg

# Block device tree
lsblk -f /dev/sdb /dev/sdc /dev/sdd

# Device mapper dependency graph
dmsetup deps