LVM — Logical Volume Manager

Abstract storage away from physical disks: extend volumes online, snapshot filesystems, stripe for speed, and mirror for redundancy.

What LVM is: the three-layer model

LVM sits between your physical disks and the filesystem you mount. Instead of formatting a disk partition directly and losing the ability to resize it later, you give the raw block device to LVM and carve it into logical volumes that can be grown, shrunk, snapshotted, or moved between disks — most of the time without unmounting anything.

Three layers, bottom to top:

┌──────────────────────────────────────────────┐
│  Logical Volumes (LV)   data, logs, home     │   <-- what you mkfs and mount
├──────────────────────────────────────────────┤
│  Volume Group (VG)      vg_data              │   <-- pool of extents
├──────────────────────────────────────────────┤
│  Physical Volumes (PV)  /dev/sdb /dev/sdc    │   <-- raw block devices
└──────────────────────────────────────────────┘
Physical Volume (PV)
A block device (whole disk, partition, RAID array, or iSCSI LUN) initialised with pvcreate so LVM can use it.
Volume Group (VG)
A pool of storage built from one or more PVs. Logical volumes are allocated from this pool in fixed-size physical extents (PE, typically 4 MiB).
Logical Volume (LV)
A virtual block device carved from the VG, e.g. /dev/vg_data/lv_home. You format and mount this. It can be linear, striped, mirrored, RAID, thin, or a snapshot.

Why you use it

Inspect what's already there

First commands on any new system — find out what LVM layout exists before touching anything.

# Block devices and mountpoints
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,UUID

# PVs (physical volumes)
pvs                          # compact summary
pvdisplay /dev/sdb           # detailed single-PV view

# VGs (volume groups)
vgs                          # summary: VG name, PVs, LVs, free space
vgdisplay vg_data            # detailed: PE size, total/free extents, policies

# LVs (logical volumes)
lvs                          # summary
lvs -a -o +devices           # show underlying PVs and segment types
lvdisplay /dev/vg_data/lv_home

# Everything in one view
lsblk /dev/sdb /dev/sdc      # tree view of a specific disk

If a disk was previously partitioned with LVM, pvs will detect the PV metadata automatically — there is no "mount LVM" step.

Build a volume from scratch

Typical case: a new disk /dev/sdb (64 GiB) and you want to add /data backed by it.

1. Initialise the PV

# Wipe any old signatures first (skip if disk is definitely blank)
wipefs -a /dev/sdb

# Create the physical volume
pvcreate /dev/sdb
# → Physical volume "/dev/sdb" successfully created.

You can also pvcreate on a partition (/dev/sdb1). Using the whole disk is simpler and recommended when the disk is dedicated to LVM.

2. Create or extend the VG

# New VG
vgcreate vg_data /dev/sdb

# Or add the PV to an existing VG
vgextend vg_data /dev/sdb

3. Create the LV

# Linear LV, fixed size
lvcreate -n lv_data -L 50G vg_data

# Use a percentage of free space
lvcreate -n lv_data -l 100%FREE vg_data

# Striped across 2 PVs for throughput
lvcreate -n lv_data -L 50G -i 2 -I 64 vg_data /dev/sdb /dev/sdc
#          name       size  stripes stripe-size-KiB  PVs

-L takes absolute size (50G, 500M). -l takes extents or percentages (100%FREE, 50%VG).

Mount and persist in /etc/fstab

# Format (pick xfs or ext4)
mkfs.xfs /dev/vg_data/lv_data
# or
mkfs.ext4 -L data /dev/vg_data/lv_data

# Mount it
mkdir -p /data
mount /dev/vg_data/lv_data /data

# Capture the filesystem UUID for fstab (more stable than the device path)
blkid /dev/vg_data/lv_data

# /etc/fstab — use UUID or the LVM device path
UUID=…  /data  xfs  defaults  0 0
# or
/dev/vg_data/lv_data  /data  xfs  defaults  0 0

# Tell systemd about the new fstab entry and verify
systemctl daemon-reload
mount -a
findmnt /data
Always test fstab before rebooting. A typo that causes the boot to fail drops you into emergency mode. Running mount -a catches most problems while the system is still up. Use nofail on any mount that isn't critical for boot.

Grow a volume online

This is the killer feature. With -r (or --resizefs), lvextend resizes the filesystem in the same step — no unmount.

# One-shot: add 20 GiB and resize the filesystem
lvextend -r -L +20G /dev/vg_data/lv_data

# Absolute target size
lvextend -r -L 100G /dev/vg_data/lv_data

# Use all remaining free space in the VG
lvextend -r -l +100%FREE /dev/vg_data/lv_data

# Verify
df -h /data
lvs /dev/vg_data/lv_data

If the VG is out of free extents, add another PV first:

# Attach another disk
pvcreate /dev/sdc
vgextend vg_data /dev/sdc

# If the underlying disk itself grew (resized SAN LUN, expanded cloud disk)
pvresize /dev/sdb                # LVM re-reads the new size

# Then extend the LV as above
lvextend -r -l +100%FREE /dev/vg_data/lv_data

xfs_growfs and resize2fs are what -r invokes internally. You can call them manually if you forgot -r: xfs_growfs /data or resize2fs /dev/vg_data/lv_data.

Shrink a volume (ext4 only)

XFS cannot shrink. If the filesystem is xfs and you need it smaller, you have to back up, destroy, recreate at the new size, and restore. Shrinking is an ext4-only routine operation.
# ext4 shrink — MUST be offline and fsck'd first
umount /data
e2fsck -f /dev/vg_data/lv_data
resize2fs /dev/vg_data/lv_data 40G     # shrink filesystem first
lvreduce -L 40G /dev/vg_data/lv_data   # then shrink the LV to match
mount /data

# Safer one-shot (does the fsck + resize2fs + lvreduce)
lvreduce -r -L 40G /dev/vg_data/lv_data

Always shrink the filesystem before the LV. The reverse order destroys data.

Snapshots

A snapshot is a writable copy-on-write view of an LV at a point in time. Initially it takes almost no space — blocks are only copied when the origin LV diverges. Main use: take a consistent backup while the service keeps writing.

# Thick snapshot, 10 GiB buffer for changes
lvcreate --size 10G --snapshot --name lv_data_snap /dev/vg_data/lv_data

# Mount read-only to back it up
mkdir -p /mnt/snap
mount -o ro,nouuid /dev/vg_data/lv_data_snap /mnt/snap
tar -C /mnt/snap -cf /backup/data-$(date +%F).tar .
umount /mnt/snap

# Drop the snapshot when done
lvremove -f /dev/vg_data/lv_data_snap

# Or merge the snapshot back (roll origin back to snapshot-time state)
umount /data
lvconvert --merge /dev/vg_data/lv_data_snap
mount /data
Snapshots fill up. If the COW buffer runs out of space, the snapshot becomes invalid and is dropped automatically. Size it based on expected write rate during the backup window, and monitor with lvs — the Data% column shows consumption.

Thin-pool snapshots don't need a pre-sized buffer; they draw from the same pool (see thin provisioning).

Thin provisioning

In a thin pool, LVs are created with a virtual size that can exceed the pool's physical size. Physical extents are only allocated on write. Useful for dense snapshots and scenarios where most LVs don't actually fill up.

# Create the thin pool itself (physical, fixed size)
lvcreate -L 500G --thinpool tp_data vg_data

# Create a thin LV inside the pool (virtual size can exceed the pool)
lvcreate -V 100G --thin -n lv_tenantA vg_data/tp_data
lvcreate -V 100G --thin -n lv_tenantB vg_data/tp_data
lvcreate -V 100G --thin -n lv_tenantC vg_data/tp_data
# Total virtual: 300 GiB. Pool physical: 500 GiB. Safe — for now.

# Snapshot of a thin LV (cheap, no pre-sized buffer)
lvcreate -s -n lv_tenantA_snap vg_data/lv_tenantA

# Monitor pool usage
lvs -o name,data_percent,metadata_percent vg_data/tp_data
Overcommit is a loaded gun. If the thin pool runs out of physical space, all writes to every LV in that pool start failing. Monitor data_percent aggressively and configure thin_pool_autoextend_threshold in /etc/lvm/lvm.conf so the pool grows before it hits the wall.

Rename, remove, move between PVs

# Rename
lvrename vg_data lv_old lv_new
vgrename vg_old vg_new

# Remove (unmount first!)
umount /data
lvremove /dev/vg_data/lv_data

# Evacuate a PV before pulling the disk (online, data stays available)
pvmove /dev/sdc                        # move everything off /dev/sdc
pvmove /dev/sdc /dev/sdd               # move everything off /dev/sdc onto /dev/sdd
pvmove -n /dev/vg_data/lv_data /dev/sdc   # move a specific LV off /dev/sdc

# Then remove the PV from the VG and wipe it
vgreduce vg_data /dev/sdc
pvremove /dev/sdc

Remediate a full volume

Ticket says "disk is full on prod01". Your workflow:

# 1. Confirm which filesystem is full
df -h
df -i                           # inodes can exhaust before blocks do

# 2. Find out if the VG has free extents
vgs                             # look at VFree

# 3a. VG has free space — grow the LV online
lvextend -r -l +100%FREE /dev/vg_data/lv_data

# 3b. VG is also full — add a disk, then extend
pvcreate /dev/sdd
vgextend vg_data /dev/sdd
lvextend -r -l +100%FREE /dev/vg_data/lv_data

# 4. Verify
df -h /data

If the answer is "not actually full, just one process filling /var/log", don't extend — find and fix the writer first. See Service Troubleshooting.

LVM RAID

LVM can create RAID LVs directly, without stacking LVM on top of mdadm. One command, one admin interface, and the redundancy lives inside the VG. Good fit for single-host servers where you already manage storage with LVM.

Create RAID LVs

# RAID1 (mirror) across 2 PVs — data redundancy, same write throughput
lvcreate --type raid1 -m 1 -L 50G -n lv_mirror vg_data /dev/sdb /dev/sdc
#                     ^^ 1 mirror = 2 copies total

# RAID5 across 3+ PVs — parity, survives one disk loss
lvcreate --type raid5 -i 2 -L 50G -n lv_parity vg_data /dev/sdb /dev/sdc /dev/sdd

# RAID6 across 4+ PVs — double parity, survives two disk losses
lvcreate --type raid6 -i 2 -L 50G -n lv_parity2 vg_data /dev/sdb /dev/sdc /dev/sdd /dev/sde

# RAID10 (stripe of mirrors) across 4 PVs — performance + redundancy
lvcreate --type raid10 -m 1 -i 2 -L 50G -n lv_fast vg_data /dev/sdb /dev/sdc /dev/sdd /dev/sde

Monitor sync status and health

# Quick status
lvs -a -o +devices,raid_sync_action,sync_percent
#   Cpy%Sync shows rebuild/resync progress
#   raid_sync_action: idle / resync / recover / check / repair

# Watch a rebuild in progress
watch 'lvs -a -o name,attr,cpy%sync,raid_sync_action vg_data'

# The hidden sub-LVs (rmeta/rimage) hold per-leg metadata and data
lvs -a vg_data

Scrub for silent corruption

# Read every block and compare mirrors / check parity
lvchange --syncaction check /dev/vg_data/lv_mirror

# Same, but fix any mismatches found
lvchange --syncaction repair /dev/vg_data/lv_mirror

# Count of mismatches from the last check
lvs -o name,raid_mismatch_count vg_data

Replace a failing PV

# A disk is reporting errors. Replace the failing leg online.
# 1. Add the replacement PV to the VG
pvcreate /dev/sdf
vgextend vg_data /dev/sdf

# 2. Tell LVM to rebuild the array onto the new disk
lvconvert --replace /dev/sdc /dev/vg_data/lv_mirror /dev/sdf
#                    failing PV              LV              replacement

# 3. Watch the resync complete
lvs -a -o name,cpy%sync,raid_sync_action /dev/vg_data/lv_mirror

# 4. Remove the old disk from the VG once sync is at 100%
vgreduce vg_data /dev/sdc
pvremove /dev/sdc

LVM RAID vs mdadm + LVM stacked

LVM RAIDmdadm + LVM
Management surfaceOne tool (lv*)Two tools (mdadm + lv*)
Distro defaultsRHEL/Rocky since 7Debian/Ubuntu historical default
Boot/root on RAIDTrickierWell-trodden with /boot on md
Monitoring maturityGood, via dmeventd + lvsVery mature, mdadm --monitor, email alerts
RAID levels0, 1, 4, 5, 6, 10Same
Best whenSingle-host, LVM-first shopsYou want RAID under everything, including /boot

Either way, hardware RAID controllers with battery-backed cache are still a valid choice for databases. LVM RAID is a software solution and costs CPU + parity writes.

Ansible patterns

Three modules cover most LVM work: community.general.lvg (VGs), community.general.lvol (LVs), and ansible.posix.mount (fstab + mount).

---
- name: Ensure /data is provisioned with LVM
  hosts: db_servers
  become: true

  tasks:
    - name: Ensure volume group vg_data spans the data disks
      community.general.lvg:
        vg: vg_data
        pvs: /dev/sdb,/dev/sdc
        pesize: 4

    - name: Ensure lv_data exists and is grown to fit the VG
      community.general.lvol:
        vg: vg_data
        lv: lv_data
        size: 100%FREE
        resizefs: true                  # xfs_growfs / resize2fs after extend
        shrink: false                   # never shrink without explicit opt-in

    - name: Ensure filesystem on lv_data
      community.general.filesystem:
        fstype: xfs
        dev: /dev/vg_data/lv_data

    - name: Ensure /data is mounted and in fstab
      ansible.posix.mount:
        path: /data
        src: /dev/vg_data/lv_data
        fstype: xfs
        opts: defaults,nofail
        state: mounted

Set shrink: false on lvol in production. The default behaviour will shrink a volume if the variable is smaller than the current size — easy to cause data loss by accident.

Common errors

Device /dev/sdb excluded by a filter
LVM's global_filter or filter in /etc/lvm/lvm.conf is rejecting the device. Common cause: multipath or a stale filter line. Run lvmconfig --type full devices/filter to see the active filter.
Can't open /dev/sdb exclusively. Mounted filesystem?
Something else is holding the device — a mounted partition, a running mdadm array, multipath, or an older PV signature. Use lsof /dev/sdb, lsblk, and wipefs /dev/sdb to investigate.
Insufficient free space: N extents needed, only M available
The VG doesn't have enough room. Extend the VG with another PV (vgextend) or resize an underlying disk and run pvresize.
xfs_growfs: ... is not a mounted XFS filesystem
You ran lvextend -r but the LV wasn't mounted. XFS can only be grown while mounted. Mount it first, then rerun with -r.
cannot resize to N extents as later ones are allocated (on shrink)
The extents at the tail of the LV you're shrinking are in use. With xfs this is fatal (no shrink possible). With ext4 you need to fsck and resize2fs to a size smaller than the target before lvreduce.
Snapshot ... is invalid
The snapshot's COW buffer filled up. The snapshot has been auto-dropped; remove it with lvremove -f and retake with a bigger --size.
Related: Linux CLI for lsblk/blkid/findmnt, and Service Troubleshooting for the full "disk is full" workflow.