---
title: 'Logical Volume Management'
source: 'https://academia.sh/en/courses/system-administration/logical-volume-management'
course: 'System Administration'
language: en
updated: '2026-08-17T18:10:06+00:00'
license: 'CC BY-SA 4.0'
---

# Logical Volume Management

Growing is two steps, and seven of twenty-one readings point to the wrong link in the chain; a snapshot with 512 blocks allocated fills at the 413th second, and monitoring that looks once every sixty seconds sees a number sitting at 256 blocks until then.

All of the previous lesson's measurements assumed a partition's boundaries
are fixed: a range drawn on the device and a file system built inside it.
Under this assumption, the only thing to be done for a partition that fills
up was deleting; the free space on a neighboring partition stayed
unreachable.

This lesson measures the layer that removes this fixedness. The layer
collects partitions into a pool and cuts pieces of a requested size from
the pool; the cut piece can be grown while it runs, and a copy of one of
its moments can be kept. The gain is real, and it brings two new sources of
wrong diagnosis: which link in the chain has grown, and how long a copy of
a moment will last.

## Three Layers and the Unit Between Them

The layer consists of three objects. A **physical volume** is a block
device or partition that has joined the pool. A **volume group** is one or
more physical volumes presented as a single area. A **logical volume** is a
piece cut from the pool that a file system can be built on.

The cutting is done not byte by byte but in fixed-size **extents**. An
extent's size is set when the pool is created; every logical volume holds a
whole number of extents, and growing happens in multiples of these units
too. This has two consequences: the requested size gets rounded up, and a
logical volume can spread across **more than one** of the pool's physical
volumes.

Spreading is both the layer's strength and its silent risk. If a logical
volume is spread across two physical volumes, either one failing makes the
whole volume unusable; adding a device to the pool does not raise
durability, it only raises capacity. The layer itself is not a redundancy
scheme, and the diagnosis errs most expensively when it is assumed to be.

The three layers are read with three tools: `pvs` lists physical volumes,
`vgs` lists pools, `lvs` lists logical volumes. The transcript below is a
**sample transcript** showing the shape of `lvs`'s output; it was not run,
and no numeric claim in this lesson comes from it.

```text
LV             VG      Attr       LSize   Pool Origin Data%  Meta%
data           pool-1  -wi-ao----  32.00g
data-snapshot  pool-1  swi-a-s---  2.00g       data   41.30
log            pool-1  -wi-ao----   8.00g
```

The `Origin` and `Data%` columns in the second row are the subject of this
lesson's second measurement: a snapshot's source and its fill level.

## Growing Is Two Steps

Growing a logical volume does not grow the file system. The layer adds new
extents to the volume; the file system keeps holding its own ledgers
according to the old boundary and does not see the new space. The two
steps are separate, their order is binding, and in the gap between them,
tools report different numbers.

- **ST26** — The chain passes through seven stages: start, adding a
  physical volume to the pool, growing the logical volume, growing the
  file system, writing continuing, a snapshot getting taken, and the
  snapshot dropping.
- **ST27** — All numbers are in blocks; the extent is a multiple of a
  block in this measurement, and rounding is not counted.
- **ST28** — The diagnosis asked is a single one: **how many more blocks
  can be written right now.** The oracle is the file system's capacity
  minus what is used.

```python
"""Growing is two steps; every tool shows a different link in the chain."""
STAGE = (
    ("0 start",                  0, 4096, 4096, 3800),
    ("1 physical volume added",  8192, 4096, 4096, 3800),
    ("2 logical volume grown",   4096, 8192, 4096, 3800),
    ("3 file system grown",      4096, 8192, 8192, 3800),
    ("4 writing continued",      4096, 8192, 8192, 6000),
    ("5 snapshot taken",         3584, 8192, 8192, 6000),
    ("6 snapshot dropped",       4096, 8192, 8192, 6000),
)


def readings(a):
    """Question: how many more blocks can be written right now."""
    _, vg_free, lv, fs, used = a
    return {"oracle": fs - used,
            "vgs": vg_free + lv - used,     # thinks the pool's free space is writable
            "lvs": lv - used,               # thinks the volume's size is writable
            "df": fs - used}


print("stage                      vg free    lv    fs        used  oracle"
      "    vgs    lvs     df")
for a in STAGE:
    o = readings(a)
    print(f"{a[0]:24s} {a[1]:7d} {a[2]:5d} {a[3]:5d} {a[4]:11d}"
          f" {o['oracle']:6d} {o['vgs']:6d} {o['lvs']:6d} {o['df']:6d}")
print()
print("tool  correct reads  wrong reads")
total = 0
for tool in ("vgs", "lvs", "df"):
    y = sum(readings(a)[tool] != readings(a)["oracle"] for a in STAGE)
    total += y
    print(f"  {tool:3s} {len(STAGE) - y:11d}  {y:12d}")
print("wrong diagnosis:", total, "/", len(STAGE) * 3)
```

```
stage                      vg free    lv    fs        used  oracle    vgs    lvs     df
0 start                        0  4096  4096        3800    296    296    296    296
1 physical volume added     8192  4096  4096        3800    296   8488    296    296
2 logical volume grown      4096  8192  4096        3800    296   8488   4392    296
3 file system grown         4096  8192  8192        3800   4392   8488   4392   4392
4 writing continued         4096  8192  8192        6000   2192   6288   2192   2192
5 snapshot taken            3584  8192  8192        6000   2192   5776   2192   2192
6 snapshot dropped          4096  8192  8192        6000   2192   6288   2192   2192

tool  correct reads  wrong reads
  vgs           1             6
  lvs           6             1
  df            7             0
wrong diagnosis: 7 / 21
```

Three numbers side by side. **The oracle:** at the second stage, the
writable space is **296 blocks** — even though the logical volume has
doubled. **The tool's output:** at the same stage, `lvs` shows 4392, `vgs`
shows 8488 blocks. **Wrong diagnosis:** **seven** of twenty-one readings
are wrong.

The table shows the real difficulty the layer brings: the same question is
answered by three separate layers, and only the topmost one is correct.
The pool's free space is never directly writable space; a logical volume's
size is not writable space unless the file system is grown too. A process
stopped at the second stage — the growth command issued, the file system
not grown — leaves the system in a state thought to have "opened up space"
but that has not, and the full-disk failure surfaces **after the growth
was done**.

The order itself is binding too. In growing, the logical volume is grown
first, then the file system; in shrinking, the order is reversed, and if
it is not reversed, the file system keeps pointing at blocks left outside
its own boundary. The difference between these two sentences is an
unrecoverable data loss.

## How a Snapshot Fills

A **snapshot** is a second volume that keeps a logical volume's state at a
given moment readable. The copying is not done at the start; the first
time a block in the source is written over, its **old state** is moved
into the snapshot's space. This is why the space a snapshot consumes is
proportional not to the source's size, but to the **number of blocks that
change** in the source.

When the allocated space runs out, the snapshot becomes invalid and drops.
The source volume is unaffected and keeps running; what is lost is the
validity of the backup being taken from that snapshot.

- **ST29** — The source logical volume is **4096 blocks**, and the space
  allocated for the snapshot is **512 blocks**. The usual load renews two
  of the **256 blocks in the hot set** every second.
- **ST30** — Between the 400th and 430th seconds, a backup job runs a
  broad scan and touches **20 blocks** every second; the touched blocks
  are drawn from the whole volume.
- **ST31** — The monitoring tool reads fill level once every `interval`
  seconds; the first reading that crosses the threshold is a warning. If
  the warning arrives **after** the fill moment, or never arrives, the
  diagnosis counts as wrong.

```python
"""A snapshot copies on write; the space allocated for it invalidates once full."""
SEED = 20260218
PERIOD = 600
VOLUME_BLOCKS = 4096     # source logical volume's block count
ALLOCATED = 512          # blocks allocated for the snapshot
HOT_SET = 256            # block set the usual load touches
BURST = (400, 430)       # interval where the backup job runs a broad scan


def generator(seed):
    d = seed

    def next_value(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return next_value


def copied(seed=SEED):
    """Oracle: blocks the snapshot holds at the end of every second."""
    r = generator(seed)
    seen, curve = set(), []
    for t in range(PERIOD):
        if BURST[0] <= t < BURST[1]:
            for _ in range(20):
                seen.add(r(VOLUME_BLOCKS))
        else:
            for _ in range(2):
                seen.add(r(HOT_SET))
        curve.append(min(len(seen), ALLOCATED))
    return curve


def fill_time(curve):
    for t, v in enumerate(curve):
        if v >= ALLOCATED:
            return t
    return None


def diagnose(curve, interval, threshold):
    """The tool looks every `interval` seconds; a reading past the threshold is a warning."""
    fills = fill_time(curve)
    looks = [t for t in range(0, PERIOD, interval) if t < fills]
    warning = next((t for t in range(0, PERIOD, interval)
                     if curve[t] / ALLOCATED >= threshold), None)
    on_time = warning is not None and warning < fills
    return {"last_reading": curve[looks[-1]] if looks else 0,
            "warning": warning, "wrong": not on_time}


E = copied()
D = fill_time(E)
print("oracle: snapshot fills at", D, "seconds |", ALLOCATED, "blocks allocated")
print("curve:", [(t, E[t]) for t in (0, 60, 180, 300, 360, 400, 410, 420)])
print()
print("interval  threshold  last reading before fill  first warning  wrong diagnosis")
wrong = 0
for interval in (10, 30, 60, 120, 300):
    for threshold in (0.70, 0.90):
        s = diagnose(E, interval, threshold)
        wrong += s["wrong"]
        print(f"{interval:8d}  {threshold:.2f}  {s['last_reading']:24d}"
              f"  {str(s['warning']):>9s}  {str(s['wrong']):>11s}")
print("wrong diagnosis:", wrong, "/ 10")
print()
for seed in (SEED, 20260219):
    e2 = copied(seed)
    y = sum(diagnose(e2, i, t)["wrong"] for i in (10, 30, 60, 120, 300)
            for t in (0.70, 0.90))
    print(f"seed {seed}: fill time {fill_time(e2)}  t=360 reading {e2[360]}"
          f"  wrong diagnosis {y} / 10")
```

```
oracle: snapshot fills at 413 seconds | 512 blocks allocated
curve: [(0, 2), (60, 122), (180, 256), (300, 256), (360, 256), (400, 276), (410, 466), (420, 512)]

interval  threshold  last reading before fill  first warning  wrong diagnosis
      10  0.70                       466        410        False
      10  0.90                       466        410        False
      30  0.70                       256        420         True
      30  0.90                       256        420         True
      60  0.70                       256        420         True
      60  0.90                       256        420         True
     120  0.70                       256        480         True
     120  0.90                       256        480         True
     300  0.70                       256       None         True
     300  0.90                       256       None         True
wrong diagnosis: 8 / 10

seed 20260218: fill time 413  t=360 reading 256  wrong diagnosis 8 / 10
seed 20260219: fill time 413  t=360 reading 256  wrong diagnosis 8 / 10
```

Three numbers side by side. **The oracle:** the snapshot fills at the
**413th second**. **The tool's output:** monitoring that looks once every
sixty seconds sees **256 blocks** at its last reading before filling —
exactly half of the allocated space. **Wrong diagnosis:** **eight** of the
ten sampling-and-threshold combinations fail to produce the warning on
time.

The curve shows why. In the first three hundred seconds, fill level sits
at 256, because the usual load keeps touching the same hot set, and once
that set has been copied once, it needs no new copy. The number does not
budge for three hundred seconds and looks **stable** to whoever is
watching. Then the backup job runs its broad scan, and fill level climbs
from 256 to 512 in thirteen seconds. Lowering the threshold does not fix
this: in the table, the 0.70 and 0.90 thresholds give the same result,
because the problem is not how high the threshold is, but **the distance
between two looks**. The same structure as the sampling measurement in the
Process Management topic holds here too.

In the second seed, neither the fill moment nor the wrong-diagnosis count
changes. The reason is structural: the hot set saturates under every seed,
and the burst's width is independent of the seed. The result depends not
on the fiction but on copy-on-write's own behavior.

The design rule that follows is this: the space allocated to a snapshot is
chosen not by the source's size, but by **the number of blocks that will
change over the time the snapshot will live**. A snapshot kept for a long
time can demand as much space as the whole source.

In practice, three precautions follow directly from this measurement. The
first is **shortening the snapshot's lifetime**: a snapshot removed right
after the backup is taken ends without any threshold being crossed. The
second is reading fill level **together with the event** — two readings,
taken as the backup job starts and finishes, say more than a regular
sixty-second sample. The third is that the snapshot **can be grown while
it runs**: new extents can be added from the pool before the allocated
space runs out, but not after it has. What the three share is not leaving
the decision to the sampling interval.

There is also a point commonly misunderstood. A snapshot is not a backup;
it sits on the same physical volumes as its source, and the two are lost
together if the device underlying the source fails. What it provides is a
**consistent read moment** — it makes it possible for the process taking
the backup to read from a fixed image while the data keeps changing.
Backup itself is outside this course and has been measured elsewhere.

## Shrinking and Deleting

Among this layer's commands, the irreversible ones are not open-ended;
their names are known. The command that deletes a logical volume returns
that volume's extents to the pool, and the file system inside it can never
be built again. The command that shrinks a logical volume returns the file
system's last blocks to the pool if the file system has not been shrunk
beforehand; if the file system is using those blocks, the data loss is
silent and certain. This lesson does not give these two commands in a
runnable, complete form.

The safe way to test is three steps. The first is a **dry run**:
volume-management tools carry an option that reports the result without
applying the change, and the real command is not issued before that
option's output is read. The second is a **snapshot**: a snapshot taken
before the change makes recovery possible if the source is corrupted — as
long as it is used before the allocated space fills. The third is
**testing in a separate pool**: a small pool built from loop devices makes
it possible to run the whole chain away from real data.

## Summary

- The layer consists of three objects: physical volumes that have joined
  the pool, the volume group presenting them as a single area, and logical
  volumes cut from the pool in fixed-size extents. Adding a device to the
  pool raises capacity, not durability.
- Growing is two steps, and the gap between tools is measurable: seven of
  twenty-one readings across seven stages are wrong; `vgs` errs six times,
  `lvs` once, `df` never.
- When the logical volume is grown but the file system is not, the
  writable space stays at 296 blocks, but `lvs` shows 4392, `vgs` shows
  8488.
- A snapshot fills not by the source's size but by the number of blocks
  that change: a 512-block snapshot fills at the 413th second, and
  monitoring that looks once every sixty seconds sees a number sitting at
  256 blocks until then. Eight of ten sampling-and-threshold combinations
  fail to produce the warning on time.
- Lowering the threshold does not fix this miss; what distinguishes is the
  distance between two looks. The commands for deleting and shrinking a
  volume are irreversible, and this lesson does not give them in complete
  form.

## Next Step

This lesson treated storage only as a layer that holds data. Yet a portion
of the same block device holds no files at all: it is the space pages get
written to under memory pressure. The next lesson measures that space and
answers a question — in which indicator does swap usage show up, and how
late. The measurement will come out two-directional: some indicators
report pressure late, one keeps reporting it even after the pressure has
passed.
