Signal9Signal9 — home
online
all lessons

LVM: Flexible Storage

Intermediate40 min7 steps
What you'll learn
  • Explain the PV → VG → LV model and the indirection that makes LVM beat raw partitions
  • Read any machine's storage stack with lsblk, pvs, vgs, and lvs before touching it
  • Build LVM storage from disks: pvcreate → vgcreate → lvcreate → mkfs → mount → fstab
  • Grow a logical volume and its filesystem online, non-destructively — the headline skill
  • Use snapshots for consistent backups, sized by change rate
  • Evacuate a failing disk online with pvmove, then retire it with vgreduce and pvremove
  1. Why LVM: the problem with promises carved in stone

    Understand the PV → VG → LV model and what the indirection buys

    A raw partition is a promise carved in stone: when you make /dev/sdb1 50 GB, you're predicting — on day one, knowing the least you'll ever know — how big that data will grow. Guess low and you're migrating data off a full partition at the worst possible time; guess high and the surplus is stranded where nothing else can use it. Repartitioning to fix either mistake is slow, risky, and usually needs downtime.

    LVM (Logical Volume Manager) breaks the stone by inserting a layer of indirection between disks and filesystems. Think of it as a pantry, in three layers:

    • Physical Volume (PV) — a disk or partition handed over to LVM (/dev/sdb). The raw ingredients.
    • Volume Group (VG) — a pool combining one or more PVs into one undifferentiated space. The shared pantry — capacity lives here, unassigned.
    • Logical Volume (LV) — a slice carved from the VG, which you format and mount exactly like a partition. The portion you're cooking with — and you can go back to the pantry for more whenever it has room.

    The indirection buys three things stone can't do:

    1. Resize on demand — an LV grows whenever the VG has free space, while mounted and in use.

    2. Span disks — a VG made of three 1 TB disks can carve one 2.5 TB LV; the filesystem never knows it's straddling hardware.

    3. Snapshots — freeze a point-in-time view of an LV for consistent backups, instantly.

    One naming convention to internalize now: an LV's device path is /dev/<vg>/<lv> — e.g. /dev/vg_data/lv_home. When you see that shape in an fstab or a mount table, you know you're looking at LVM.

    Check yourself

    In the pantry model, where does UNASSIGNED free capacity live — and why does that placement matter when an LV fills up?

  2. Read the stack before you build

    Survey any machine's storage in four commands

    Storage work punishes assumptions harder than any other domain — the command that formats the wrong disk doesn't offer an undo. So the LVM discipline starts with reading, and there's one summary command per layer, plus the block-device tree that shows everything at once:

    lsblk
    NAME               MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
    sda                  8:0    0   64G  0 disk
    ├─sda1               8:1    0    1G  0 part /boot
    └─sda2               8:2    0   63G  0 part
      ├─rhel-root      253:0    0   40G  0 lvm  /
      └─rhel-swap      253:1    0    4G  0 lvm  [SWAP]
    sdb                  8:16   0  100G  0 disk
    sdc                  8:17   0  100G  0 disk
    The tree view of every block device. Read the TYPE column: disk → part → lvm shows the layering — sda2 is a partition serving as a PV, with the root and swap LVs carved from it (that's the installer's default LVM layout). And the two prizes: sdb and sdc are bare disks, no partitions, no mountpoints — the raw material for the stack we're about to build. Confirming a disk is genuinely unused in lsblk BEFORE pvcreate is the habit that prevents storage tragedies.
    sudo pvs; sudo vgs; sudo lvs
      PV         VG   Fmt  Attr PSize   PFree
      /dev/sda2  rhel lvm2 a--  <63.00g    0
      VG   #PV #LV #SN Attr   VSize   VFree
      rhel   1   2   0 wz--n- <63.00g    0
      LV   VG   Attr       LSize  Pool Origin Data%
      root rhel -wi-ao---- 40.00g
      swap rhel -wi-ao----  4.00g
    pvs / vgs / lvs
    compact per-layer summaries; the -display variants (vgdisplay) give full detail
    VFree
    (vgs) unallocated space in the pool — the number that decides your grow strategy
    One summary per layer: pvs (which disks are in), vgs (the pools and — the column you'll check constantly — VFree, the unassigned headroom), lvs (the carved volumes). This VG shows VFree 0: every byte is allocated, so growing any LV here would first require feeding the pool. That one glance at VFree tells you whether a grow is a one-command job or a two-command job — which is exactly the decision the next steps walk through.

    Check yourself

    vgs shows VFree 0 for vg_data, and /dev/vg_data/lv_app is full. What does that VFree tell you about the fix?

  3. Build the stack

    Go from bare disks to a mounted, persistent filesystem

    The build always flows up the layers — ingredients into the pantry, pantry onto the plate: initialize PVs, pool them into a VG, carve an LV, then format and mount it like any partition. Five commands, and the first two do the LVM-specific half:

    sudo pvcreate /dev/sdb /dev/sdc
    sudo vgcreate vg_data /dev/sdb /dev/sdc
      Physical volume "/dev/sdb" successfully created.
      Physical volume "/dev/sdc" successfully created.
      Volume group "vg_data" successfully created
    pvcreate
    initialize disks/partitions as LVM physical volumes — destroys existing data
    vgcreate NAME PVs...
    pool the listed PVs into a named volume group
    pvcreate stamps LVM metadata onto each disk — the moment of no return for any data on them, which is why the lsblk check came first. vgcreate pools both into one 200 GB space named vg_data. Naming conventions (vg_ and lv_ prefixes) are optional but make every later command self-documenting.

    Example — not runnable on your lab box

    sudo lvcreate -L 50G -n lv_home vg_data
    sudo mkfs.xfs /dev/vg_data/lv_home
    sudo mount /dev/vg_data/lv_home /home
      Logical volume "lv_home" created.
    meta-data=/dev/vg_data/lv_home   isize=512    agcount=4, agsize=3276800 blks
    ...
    -L 50G
    absolute size; -l 100%FREE takes all remaining pool space instead
    -n lv_home
    the LV's name — becomes part of its /dev/<vg>/<lv> path
    Carve 50 GB from the pool (leaving 150 GB of VFree for the future — remember, starting modest and growing later is the LVM way), then format and mount the LV's device path exactly as you would a partition. From the filesystem's point of view, /dev/vg_data/lv_home IS a partition; the LVM magic is invisible above this line.

    Check yourself

    Which LVM layer sits between the physical volumes and the logical volumes, and what single job does it do?

  4. Grow it without downtime — the headline skill

    Extend an LV and its filesystem online, choosing the right path by VFree

    The payoff scene: /home is at 95% and users are complaining. Your first move is the one you drilled — vgs, read VFree. That number picks between the one-command path (pool has room) and the two-command path (pool is empty, feed it first). Here's the full version, worst case first:

    sudo pvcreate /dev/sdd
    sudo vgextend vg_data /dev/sdd
      Physical volume "/dev/sdd" successfully created.
      Volume group "vg_data" successfully extended
    Only needed when VFree is 0: initialize a fresh disk and pour it into the pool. Note what did NOT happen — no LV changed, no filesystem grew; the pantry just got restocked. In a virtualized environment this pairs beautifully with hot-added virtual disks: attach in the hypervisor, pvcreate + vgextend in the guest, no reboot anywhere.
    sudo lvextend -L +50G -r /dev/vg_data/lv_home
      Size of logical volume vg_data/lv_home changed from 50.00 GiB to 100.00 GiB.
      Logical volume vg_data/lv_home successfully resized.
    meta-data=/dev/mapper/vg_data-lv_home isize=512 ...
    data blocks changed from 13107200 to 26214400
    -L +50G
    grow BY 50G (the + matters; without it you're setting absolute size)
    -l +100%FREE
    consume all remaining pool space instead of a fixed amount
    -r
    also resize the filesystem — the flag whose absence causes the df mystery
    The two-layer truth in one command: -L +50G grows the VOLUME, and -r grows the FILESYSTEM inside it (calling xfs_growfs or resize2fs for you). Skip the -r and you hit the classic confusion — lvs shows 100G, df still shows 50G, because the container grew and the contents didn't. And the quiet miracle: /home stayed mounted, users kept writing, nothing paused. That last output line — data blocks doubled — is your proof for df.

    Check yourself

    After lvextend -L +20G (without -r), lvs shows the new size but df doesn't. What happened, and what fixes it?

  5. Snapshots for consistent backups

    Freeze a point-in-time view, back up from it, size it by change rate

    One more trick the indirection enables. Backing up a live filesystem has a subtle flaw: the backup takes an hour, files change during that hour, and the archive ends up capturing file A from 2:00 and file B from 2:40 — a combination that never existed. For a database, that inconsistency can mean an unrestorable backup.

    An LVM snapshot solves it: a frozen, point-in-time view of an LV, created in under a second, that stays consistent while the original keeps changing. The mechanism is copy-on-write: the snapshot stores only the original versions of blocks that change after its creation — so it starts near-empty and grows with the origin's change rate, not its size.

    Example — not runnable on your lab box

    sudo lvcreate -L 10G -s -n lv_snap /dev/vg_data/lv_home
    sudo mount -o ro /dev/vg_data/lv_snap /mnt/snap
    sudo tar czf /backup/home-$(date +%F).tgz -C /mnt/snap .
      Logical volume "lv_snap" created.
    -s
    snapshot (copy-on-write) of the given origin LV, not a normal volume
    -L 10G
    reserved space for original copies of blocks that change — size by change rate
    -o ro
    (mount) read-only — a backup source shouldn't be writable
    Three beats: freeze (instant, -s makes it a snapshot; the 10G is space reserved for copied-on-write originals), mount read-only, and back up from the frozen view while real /home stays live and changing. Then tear down promptly: umount /mnt/snap && lvremove /dev/vg_data/lv_snap. Snapshots are a backup TOOL, not a backup — they live on the same disks as the origin and die with them.

    Check yourself

    Why is a snapshot created in under a second even for a 2 TB volume?

  6. Evacuate a failing disk: pvmove, then vgreduce

    Move every extent off one PV while the volumes stay mounted, then retire the disk

    The same indirection that lets a volume grow lets it move. Say /dev/sdb starts logging SMART warnings. Its extents belong to vg_data, not to any filesystem directly, so LVM can copy them to another PV in the pool underneath the mounted LVs — users keep working while the data migrates.

    The tool is `pvmove SOURCE [DESTINATION]`. The first device you name is the source: the disk being emptied. The optional second one is where its extents go; leave it out and LVM picks free extents anywhere else in the VG. Before you start, read pvs: the other PVs' combined PFree must be at least what the source has allocated, or there is nowhere to put the data.

    Example — not runnable on your lab box

    sudo pvmove /dev/sdb /dev/sdd
      /dev/sdb: Moved: 12.50%
      /dev/sdb: Moved: 57.81%
      /dev/sdb: Moved: 100.00%
    /dev/sdb
    SOURCE — the PV whose extents are moved off (the disk you're evacuating)
    /dev/sdd
    DESTINATION (optional) — where the extents land; omit it to let LVM use any free space in the VG
    -i 10
    print a progress line every 10 seconds (the default interval is 15)
    Each progress line names the source device and how much of it has been moved so far — so on a long migration, the device in those Moved: lines is the disk being evacuated, never the destination. The copy runs online: LVs on /dev/sdb stay mounted and writable throughout. If the session drops, the move survives; running pvmove with no arguments resumes any interrupted move.

    When the move reaches 100%, /dev/sdb still belongs to the VG — it just holds no data. Two more steps retire it: take it out of the pool, then wipe its LVM label so nobody mistakes it for a PV later.

    Example — not runnable on your lab box

    sudo vgreduce vg_data /dev/sdb
    sudo pvremove /dev/sdb
      Removed "/dev/sdb" from volume group "vg_data"
      Labels on physical volume "/dev/sdb" successfully wiped.
    vgreduce VG PV
    remove an EMPTY physical volume from a volume group
    pvremove PV
    erase the LVM label so the disk is no longer a physical volume
    vgreduce shrinks the pool by one disk; it refuses (still in use) if any extent remains, which is your proof that pvmove finished. pvremove then clears the label, and the disk can be pulled. The order is the point: pvmove → vgreduce → pvremove.

    Check yourself

    An operator ran pvmove /dev/sdc /dev/sde and the output shows /dev/sdc: Moved: 40.0%. Which disk is being emptied?

  7. Now do it — read the stack and rehearse the grow

    Walk the storage layers on the sandbox and stage the exam's most common fix

    Storage practice has a special rule: the read is always safe, the write needs a spare disk. Walk the reads for real — they're the first two minutes of every storage task you'll ever do — and rehearse the grow command's exact shape so it's finger-memory when a graded lab (or a 2 a.m. page) hands you a full volume.

    lsblk
    The tree first, always: find the disk → part → lvm layering, spot which mountpoints ride on LVM, and check for bare disks. Say out loud what each TYPE row is — if you can narrate lsblk, you understand the machine's storage.
    sudo pvs; sudo vgs; sudo lvs
    The three-layer summary. Find VFree in vgs and state the verdict it implies: is a grow on this box a one-command or a two-command job? That single glance-then-verdict is the professional reflex this lesson exists to build.
    sudo vgdisplay 2>/dev/null | grep -E 'VG Name|Alloc|Free'
    The long-form pool report: allocated vs free extents, spelled out. vgdisplay/lvdisplay are the detail views behind the vgs/lvs summaries — you'll want them when the summary raises a question.
    echo 'the fix: sudo lvextend -l +100%FREE -r /dev/<vg>/<lv>'
    Type the real command shape out (with your VG/LV names from lvs) even though you're not running it here: the +100%FREE form plus -r is the single most common real-world resolution to 'the disk is full' — pool to plate, filesystem grown, users unblocked, zero downtime.
Recap
  • Raw partitions are promises carved in stone; LVM's indirection — PV (disk) → VG (pool) → LV (resizable slice at /dev/<vg>/<lv>) — lets storage follow demand instead of day-one guesses.
  • Read before building: lsblk for the tree, pvs/vgs/lvs per layer. VFree in vgs decides everything — nonzero means grow is one command, zero means feed the pool first (pvcreate + vgextend).
  • Build upward: pvcreate → vgcreate → lvcreate (-L size or -l 100%FREE) → mkfs → mount, then fstab by UUID and prove it with mount -a before any reboot.
  • The headline skill: lvextend -L +N -r — grows volume AND filesystem, online, mounted, zero downtime. Without -r, lvs grows but df doesn't (the filesystem never claimed the space).
  • XFS never shrinks (lvreduce on XFS = corruption) — size modestly, grow into demand. Snapshots (lvcreate -s) freeze a consistent backup view via copy-on-write: size by change rate, watch Data% in lvs, keep them short-lived.
  • pvmove SOURCE [DEST] migrates extents OFF the first PV named, online, while LVs stay mounted; then vgreduce the empty disk out of the VG and pvremove its label.
Task → command
TaskCommand
Survey the stacklsblk · pvs · vgs (VFree!) · lvs
Init disk as PVpvcreate /dev/sdb
Create / feed the poolvgcreate vg /dev/sdb · vgextend vg /dev/sdd
Carve a volumelvcreate -L 50G -n lv vg (or -l 100%FREE)
Grow volume + filesystemlvextend -L +50G -r /dev/vg/lv
Snapshot for backuplvcreate -L 10G -s -n snap /dev/vg/lv → mount ro → tar → lvremove
Evacuate a failing diskpvmove /dev/old [/dev/new] → vgreduce vg /dev/old → pvremove /dev/old
Persist a mountblkid → fstab by UUID → mount -a to prove it