@@ -340,49 +340,57 @@ Write a `blockio.yaml` file that defines the named classes you want to
340340 node without exceeding the budget enforced by the extended resource
341341 in [ step 5] ( #5-advertise-iops-capacity-as-an-extended-resource ) .
342342
343- Example ladder (adjust devices and values for your environment):
343+ ** Target the underlying physical disks, not the dm devices.** When the
344+ PV stack uses LVM-based CSI (TopoLVM, LVMS, OpenEBS LVM-LocalPV), every
345+ PVC gets its own ` /dev/dm-N ` mapping that the Postgres process writes
346+ to. It is tempting to glob ` /dev/dm-* ` in the class so each LV is
347+ throttled at its own dm. ** Don't.** That path is fragile (see
348+ [ Limitations] ( #limitations-and-caveats ) for why). Instead, glob the
349+ underlying physical block devices (` /dev/sd* ` , ` /dev/nvme*n* ` ). The
350+ kernel propagates the originating cgroup tag through the
351+ device-mapper layer, so blk-throttle on the physical disk caps I/O
352+ submitted by the workload via its LV. This was verified end-to-end:
353+ with a class globbing ` /dev/sd[a-z]* ` and a 5000 IOPS cap, fio
354+ inside a Postgres pod whose data PV is an LVM-LocalPV LV measured
355+ 5006 IOPS read / 5007 IOPS write (within 0.1% of the cap). The
356+ physical-disk approach is stable across PVC churn, restarts, and
357+ node lifecycle events; the per-dm approach is not.
358+
359+ Example ladder (adjust device paths and values for your environment):
344360
345361``` yaml
346362# blockio.yaml
347363Classes :
348364
349365 stackgres-io-1k :
350- - Devices : ["/dev/nvme[0-9]n[0-9] ", "/dev/dm- [0-9]*"]
366+ - Devices : ["/dev/sd[a-z]* ", "/dev/nvme[0-9]n [0-9]*"]
351367 ThrottleReadIOPS : 1k
352368 ThrottleWriteIOPS : 1k
353369 ThrottleReadBps : 50M
354370 ThrottleWriteBps : 50M
355371
356372 stackgres-io-5k :
357- - Devices : ["/dev/nvme[0-9]n[0-9] ", "/dev/dm- [0-9]*"]
373+ - Devices : ["/dev/sd[a-z]* ", "/dev/nvme[0-9]n [0-9]*"]
358374 ThrottleReadIOPS : 5k
359375 ThrottleWriteIOPS : 5k
360376 ThrottleReadBps : 200M
361377 ThrottleWriteBps : 200M
362378
363379 stackgres-io-20k :
364- - Devices : ["/dev/nvme[0-9]n[0-9] ", "/dev/dm- [0-9]*"]
380+ - Devices : ["/dev/sd[a-z]* ", "/dev/nvme[0-9]n [0-9]*"]
365381 ThrottleReadIOPS : 20k
366382 ThrottleWriteIOPS : 20k
367383 ThrottleReadBps : 800M
368384 ThrottleWriteBps : 800M
369385` ` `
370386
371- Device globs are expanded by the runtime against the real devices present
372- on each node. Classes that reference devices absent on a given node
373- simply produce no cgroup entries for that pod on that node --they do not
374- fail.
375-
376- > **Important: glob expansion is one-shot, at CRI-O / containerd startup.**
377- > Each ` Devices` glob is evaluated once, when the runtime reads
378- > `blockio.yaml`. The class is then stored as a fixed list of
379- > `(major:minor, parameter, value)` tuples. Block devices that don't
380- > exist at that moment are not in the class; devices that appear later
381- > (notably the per-PVC `/dev/dm-N` devices that LVM-based CSI drivers
382- > provision on demand) are **not** retroactively added. The annotated
383- > pods that need those devices get no throttle --silently, with no
384- > event. See [Limitations and caveats](#limitations-and-caveats) below
385- > for the operational implications and the recommended workflow.
387+ The globs above match SATA/SCSI and NVMe physical devices respectively.
388+ Adjust to whatever names the database nodes' kernels actually assign
389+ to the physical disks hosting the LVM VG. The glob is expanded by the
390+ runtime at startup against the real devices present on each node;
391+ entries that don't match anything are silently dropped --but for
392+ physical disks this isn't a problem because their device names are
393+ stable from boot.
386394
387395Save this file locally; the next step places it on the database nodes via
388396 the path appropriate to your runtime/platform.
@@ -783,11 +791,15 @@ Create a test SGCluster that exercises the full path: the BlockIO class
783791> not the name: the BlockIO cap has to apply to the device the workload
784792> actually writes to.
785793>
786- > - **Per-PVC device-mapper devices.** LVM-based local CSI drivers expose
787- > each PVC as its own `/dev/dm-N` device. That's why the `blockio.yaml`
788- > in [step 3](#3-author-the-blockio-class-ladder) includes
789- > `/dev/dm-[0-9]*` in the device globs --the cap is enforced on the
790- > dm device that the Postgres process writes to.
794+ > - **Per-PVC LVM devices on top of a stable physical disk.** LVM-based
795+ > local CSI drivers expose each PVC as a `/dev/dm-N` mapping over a
796+ > physical disk (the one that hosts the LVM VG). The cap from
797+ > [step 3](#3-author-the-blockio-class-ladder)'s class targets the
798+ > *physical disk*; the kernel propagates the workload's cgroup tag
799+ > through the dm layer, so the throttle binds correctly regardless of
800+ > which dm minor the PVC happens to land on. See
801+ > [Why target physical disks and not dm devices](#why-target-physical-disks-and-not-dm-devices)
802+ > for the underlying explanation.
791803> - **Local storage.** I/O stays on the node, so a kernel-block-layer cap
792804> is meaningful. With network-attached storage (Ceph RBD, EBS, GCE PD,
793805> etc.) the bottleneck is the network and a per-device cgroup throttle
@@ -969,25 +981,29 @@ BASE=/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-p
969981cat ${BASE}/container/io.max 2>/dev/null || cat ${BASE}/io.max
970982` ` `
971983
972- Expected output (exact device numbers depend on your storage stack) :
984+ Expected output (exact device numbers depend on your storage stack;
985+ one line per device matched by the class's `Devices:` glob) :
973986
974987```
975988259:0 rbps=209715200 wbps=209715200 riops=5000 wiops=5000
976989```
977990
978- Device major numbers you might see in the output, depending on the
979- backing storage :
991+ With the recommended physical-disk glob from
992+ [step 3](#3-author-the-blockio-class-ladder), `major:minor` corresponds
993+ to a physical disk on the node, **not** to the LV's `/dev/dm-N`. That
994+ is deliberate: the kernel propagates the cgroup tag through the dm
995+ layer, so the throttle on the physical disk caps I/O the workload
996+ submits via its LV. Common majors:
980997
981998| Major | Device type |
982999|---|---|
9831000| `7` | loop devices (lab setups, e.g. file-backed LVM) |
984- | `8` | SCSI / SATA |
985- | `252`, `253` | device-mapper (LVM logical volumes, dm-thin ) |
986- | `259` | NVMe |
1001+ | `8` | SCSI / SATA (`/dev/sd*`) |
1002+ | `252`, `253` | device-mapper (only if the class globs `/dev/ dm-*` directly ) |
1003+ | `259` | NVMe (`/dev/nvme*`) |
9871004
988- If the `major:minor` in the output matches one of the entries the
989- ` Devices:` glob in your `blockio.yaml` would expand to, the throttle is
990- attached to the right device.
1005+ If the `major:minor` matches one of the physical disks underlying your
1006+ LVM VG, the throttle is attached to the right device.
9911007
9921008This confirms:
9931009- The annotation was recognized by the container runtime on the node.
@@ -1120,7 +1136,7 @@ If non-StackGres workloads must coexist on the same nodes, they need
11201136
11211137## Limitations and caveats
11221138
1123- # ## One-shot device glob expansion
1139+ ### Why target physical disks and not dm devices
11241140
11251141The container runtime (CRI-O or containerd) resolves each class's
11261142 ` Devices ` globs ** once** , when it loads ` blockio.yaml ` at service
@@ -1130,9 +1146,15 @@ The container runtime (CRI-O or containerd) resolves each class's
11301146 startup are not in the class; devices that appear later are ** not**
11311147 retroactively added.
11321148
1133- For block-storage stacks where per-PVC devices are created on demand
1134- (TopoLVM, LVMS, OpenEBS LVM-LocalPV --any LVM-based CSI driver), this
1135- has two practical implications :
1149+ That property is benign for ** physical block devices** (` /dev/sda ` ,
1150+ ` /dev/nvme0n1 ` , ...) because their device names are stable from boot
1151+ and don't come or go in normal operation. It is hostile to
1152+ ** device-mapper devices created on demand by LVM CSI drivers**
1153+ (` /dev/dm-N ` ), which appear when a PVC is bound and disappear when the
1154+ corresponding LV is removed.
1155+
1156+ There are two failure modes if you glob ` /dev/dm-* ` against a dynamic
1157+ LVM-CSI stack:
11361158
113711591 . ** Cold-start blind spot.** On a fresh node where no PVCs have been
11381160 provisioned yet, ` /dev/dm-* ` matches nothing. Every class loads with
@@ -1143,62 +1165,35 @@ For block-storage stacks where per-PVC devices are created on demand
11431165 at startup; see [ step 4.1 step 4] ( #41-cri-o-direct-configuration )
11441166 for the explicit check.
11451167
1146- 2. **New-device blind spot.** Each new PVC the CSI driver provisions on
1147- a node typically gets the next free minor --`/dev/dm-N+1`. Even after
1148- a restart that picked up `/dev/dm-0..N`, devices beyond `N` were
1149- absent at expansion time and are not in the class. PVCs created later
1150- are not throttled until the next runtime restart on that node.
1151-
1152- **Mitigation: seed enough dm slots at node-provisioning time so the
1153- BlockIO class covers all the devices the node will ever produce in
1154- normal operation.** The unit of seeding is **one placeholder per dm
1155- device slot**, not one per storage class : each placeholder PVC binds
1156- to one `/dev/dm-N`, and the class ends up containing exactly the dm
1157- minors that existed at runtime startup. Subsequent production PVCs
1158- that the kernel assigns to those same minors (after the placeholders
1159- are released) are throttled; PVCs assigned to higher minors are not.
1160-
1161- The practical recipe :
1162-
1163- 1. **Pre-provision a generous number of placeholder PVCs per node**
1164- (e.g. 32 --pick a number comfortably above the maximum concurrent
1165- StackGres pod count you expect that node to ever host). Each
1166- placeholder is a small PVC of the relevant StorageClass, bound by a
1167- short-lived placeholder Pod scheduled to that node, just long enough
1168- to materialize the LV and therefore the `/dev/dm-N`. Verify with
1169- ` ls -la /dev/dm-*` that you have 32 dm devices on the node.
1170- 2. **Restart the runtime** (step 4 of this runbook). The BlockIO class
1171- now contains 32 dm entries.
1172- 3. **Tear the placeholders down.** The LVs go away, the dm minors are
1173- freed --but they remain in the BlockIO class. Production PVCs that
1174- the kernel allocates into those freed slots inherit the throttle.
1175-
1176- Bundle steps 1--3 into the node-provisioning runbook, before the node
1177- is marked "ready for workloads". Treated this way, the seeding is a
1178- one-time setup step rather than a recurring operational burden. The
1179- only failure mode is "more concurrent dm devices on a node than were
1180- seeded", which is bounded by a number you control --pick it
1181- generously.
1182-
1183- > **Footgun: pin the placeholder Pod with `nodeSelector`, not
1184- > `nodeName`.** TopoLVM, LVMS, and OpenEBS LVM-LocalPV all create their
1185- > StorageClass with `volumeBindingMode: WaitForFirstConsumer`, which
1186- > relies on the scheduler to inform the PVC of the chosen node. A Pod
1187- > with `spec.nodeName` set directly **bypasses the scheduler**, so the
1188- > PVC stays `Pending` with a `waiting for first consumer to be created
1189- > before binding` event --- forever, despite the Pod itself being
1190- > scheduled. Pin the placeholder Pod with
1191- > `spec.nodeSelector: { kubernetes.io/hostname: <node-name> }` (or an
1192- > equivalent `nodeAffinity` block) so the scheduler is the one placing
1193- > it.
1194-
1195- For workloads where 32 (or whatever number you seed) is genuinely not
1196- enough --either because pod count is unbounded or because you don't
1197- want to trust kernel dm-minor reuse semantics --see
1198- [Static provisioning as a robust alternative](#static-provisioning-as-a-robust-alternative)
1199- below. The in-cluster approach coming in **StackGres 1.19** also
1200- sidesteps this entirely : it writes the cgroup `io.max` entries directly
1201- per pod and per device, with no class-load timing dependency.
1168+ 2 . ** Stale-entry container-create failure.** If ` /dev/dm-* ` matches at
1169+ startup but the matching dm devices later disappear (e.g. the LV is
1170+ removed or its activating Pod terminates), the class still contains
1171+ their ` (major:minor) ` entries. The next container creation will try
1172+ to write throttle parameters for those stale entries; the kernel
1173+ returns ` ENODEV ` and ` crun ` fails the container create with
1174+ ` write 'rbps': No such device ` . The Pod can get stuck in
1175+ ` Init:CreateContainerError ` indefinitely. Verified end-to-end during
1176+ runbook validation.
1177+
1178+ ** Both failure modes vanish if you glob the * underlying physical
1179+ disks*** (` /dev/sd* ` , ` /dev/nvme*n* ` ) as recommended in
1180+ [ step 3] ( #3-author-the-blockio-class-ladder ) . The kernel propagates
1181+ the BIO's cgroup tag through ` dm ` 's clone/split path, so a throttle
1182+ on the physical device caps I/O the workload submitted via its LV.
1183+ Each pod's per-cgroup limit is enforced independently --multiple
1184+ capped pods on the same node, sharing the same physical disk, each
1185+ hold their own cap (validated: two pods, 5000 IOPS cap each, fio
1186+ concurrently, both observed 5003 IOPS).
1187+
1188+ This is why [ step 3] ( #3-author-the-blockio-class-ladder ) recommends
1189+ physical-disk globs only. If you have an unusual stack where the
1190+ workload writes to a dm device that is itself the underlying device
1191+ (no further block layer beneath), see
1192+ [ Static provisioning as an alternative] ( #static-provisioning-as-an-alternative )
1193+ below for a dm-stable approach. The in-cluster approach coming in
1194+ ** StackGres 1.19** also sidesteps the class mechanism entirely: it
1195+ writes the cgroup ` io.max ` entries directly per pod and per device,
1196+ with no class-load timing dependency.
12021197
12031198### conmon and cgroup-path variations
12041199
@@ -1209,16 +1204,19 @@ The exact cgroup path where the throttle ends up depends on CRI-O's
12091204 pod's slice and ` grep -l riops= ` to locate the one with throttle
12101205 entries, rather than to assume a fixed path.
12111206
1212- # ## Static provisioning as a robust alternative
1207+ ### Static provisioning as an alternative
1208+
1209+ The physical-disk-glob approach from
1210+ [ step 3] ( #3-author-the-blockio-class-ladder ) is the recommended default
1211+ for LVM-based CSI stacks. ** Static provisioning** is an alternative
1212+ worth considering if your team prefers strict capacity planning over
1213+ dynamic expansion, or if you have a stack where physical-disk
1214+ throttling isn't appropriate (e.g. dm-multipath where the workload
1215+ device is the multipath dm and the underlying paths are several sd
1216+ devices that you don't want to throttle individually).
12131217
1214- For workloads where the placeholder-seeding approach above is not a
1215- fit --either because the maximum concurrent pod count per node is
1216- unbounded, because relying on the kernel's "reuse lowest free dm
1217- minor" behavior is uncomfortable, or because the team prefers strict
1218- capacity planning over dynamic expansion --**static provisioning is
1219- a clean alternative.** Pre-create a fixed number of LVs at known
1220- names by hand (or by an Ansible / MachineConfig step at node-prep
1221- time) :
1218+ Pre-create a fixed number of LVs at known names by hand (or by an
1219+ Ansible / MachineConfig step at node-prep time):
12221220
12231221``` bash
12241222# on each db node, at provisioning time:
@@ -1235,14 +1233,14 @@ Expose each LV as a static `PersistentVolume` (one per LV) with
12351233 i.e. ` provisioner: kubernetes.io/no-provisioner ` ).
12361234
12371235Each LV's ` /dev/dm-N ` is fixed at LV creation time and doesn't move
1238- unless the LV is removed. The BlockIO class is loaded once at runtime
1239- startup and binds to all of them deterministically. There is no
1240- placeholder dance, no kernel-reuse assumption, no restart-on-growth
1241- — but you lose the dynamic-expansion property of CSI provisioning.
1236+ unless the LV is removed. The BlockIO class can then safely glob
1237+ ` /dev/dm-* ` and bind deterministically. No restart-on-growth concern,
1238+ but you lose the dynamic-expansion property of CSI provisioning.
12421239
1243- This is the right path for environments where the workload's storage
1240+ This is appropriate for environments where the workload's storage
12441241 layout is known up-front and stable, and where the cost of pre-sizing
1245- storage slots is acceptable.
1242+ storage slots is acceptable. For most StackGres deployments with
1243+ dynamic provisioning, the physical-disk approach in step 3 is simpler.
12461244
12471245### ZFS-backed PVs
12481246
@@ -1254,11 +1252,21 @@ ZFS does not correctly attribute buffered writes to the originating
12541252
12551253## Troubleshooting
12561254
1257- **Annotation silently ignored --`io.max` shows no throttle.** The most
1258- common cause is the
1259- [one-shot glob expansion limitation](#one-shot-device-glob-expansion)
1260- above. Check the CRI-O journal for `WARN : device wildcard ... does not
1261- match any device nodes` first.
1255+ ** Annotation silently ignored --` io.max ` shows no throttle.** Most
1256+ likely the class loaded but its ` Devices: ` glob matched nothing. Check
1257+ the CRI-O journal for `WARN: device wildcard ... does not match any
1258+ device nodes` (see
1259+ [ Why target physical disks and not dm devices] ( #why-target-physical-disks-and-not-dm-devices )
1260+ for the underlying explanation and the recommended remedy: glob the
1261+ physical disks, not the dm devices).
1262+
1263+ ** Pod stuck in ` Init:CreateContainerError ` with
1264+ ` write 'rbps': No such device ` .** A ` Devices: ` glob matched some
1265+ device at runtime startup that has since been removed. The kernel
1266+ rejects the stale entry, container creation fails. This happens
1267+ mainly when the glob covers dm devices (LV created at boot, removed
1268+ later); using a physical-disk glob avoids it. See
1269+ [ Why target physical disks and not dm devices] ( #why-target-physical-disks-and-not-dm-devices ) .
12621270
12631271If that's not it, the container runtime may not have loaded the BlockIO
12641272 config file. Causes to check, in order:
0 commit comments