Lesson 20 / 22
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.
Contents
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.
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.
"""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
intervalseconds; 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.
"""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;
vgserrs six times,lvsonce,dfnever. - When the logical volume is grown but the file system is not, the
writable space stays at 296 blocks, but
lvsshows 4392,vgsshows 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.