How to Configure Volume IO Limits (QoS)¶
Piraeus Datastore can enforce per-volume IO limits, restricting the read and write bandwidth and IOPS available to the containers using a volume. This prevents a single noisy workload from saturating the storage of a node.
Two components work together to enforce the limits:
- The LINSTOR CSI driver reads the limits from the StorageClass parameters and stores them with the volume. Whenever
the volume is attached to a node, the driver publishes the limits and the device path in the
VolumeAttachmentmetadata. - The nri-volume-qos plugin runs on every node and hooks into the container runtime using the Node Resource
Interface (NRI). When a container using the volume starts, the plugin applies the limits to the container's
cgroup using the Linux cgroupv2
io.maxinterface. Both filesystem volumes and raw block volumes are supported.
To complete this guide, you should be familiar with:
Prerequisites¶
- Piraeus Datastore v2.10.8 or later, which deploys LINSTOR CSI v1.11.3 or later.
- Kubernetes 1.26 or later.
-
A container runtime with NRI support: containerd 1.7 or later, or CRI-O 1.26 or later.
In containerd 1.7, NRI support is disabled by default. Enable it by adding the following snippet to
/etc/containerd/config.tomland restarting containerd on every node:[plugins."io.containerd.nri.v1.nri"] disable = falseStarting with containerd 2.0, and in all supported CRI-O releases, NRI is enabled by default.
-
A Linux kernel with the cgroupv2 IO controller, version 5.14 or later.
Deploying the NRI Plugin¶
Deploy the nri-volume-qos plugin using the Helm chart published on the GitHub Container Registry:
$ helm install nri-volume-qos oci://ghcr.io/piraeusdatastore/nri-volume-qos --namespace kube-system
Every release includes a manifest.yaml, containing the Helm chart rendered with
default values. Apply it to deploy the plugin in the kube-system namespace:
$ kubectl apply --server-side -f https://github.com/piraeusdatastore/nri-volume-qos/releases/latest/download/manifest.yaml
Then, wait for the plugin to be ready on all nodes:
$ kubectl --namespace kube-system rollout status daemonset nri-volume-qos
daemon set "nri-volume-qos" successfully rolled out
🗒️ NOTE: Some distributions place the kubelet directory somewhere other than
/var/lib/kubelet, for example k0s and MicroK8s. Set thehostPaths.kubeletPodsDirHelm value to the pods directory used by your distribution. See the chart documentation for all available values.
Configuring IO Limits in a StorageClass¶
IO limits are configured as StorageClass parameters. Create a StorageClass, adding the limits that should apply to every volume provisioned from it:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: piraeus-storage-qos
provisioner: linstor.csi.linbit.com
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
parameters:
linstor.csi.linbit.com/storagePool: pool1
qos.linbit.com/rbps: "40Mi"
qos.linbit.com/wbps: "20Mi"
qos.linbit.com/riops: "4000"
qos.linbit.com/wiops: "2000"
The following parameters are available. All of them are optional: leaving out a parameter leaves that dimension unlimited.
| Parameter | Description |
|---|---|
qos.linbit.com/rbps |
Maximum read bandwidth, in bytes per second |
qos.linbit.com/wbps |
Maximum write bandwidth, in bytes per second |
qos.linbit.com/riops |
Maximum read operations per second |
qos.linbit.com/wiops |
Maximum write operations per second |
Values are plain integers ("41943040") or Kubernetes quantities
("40Mi", "500k").
🗒️ NOTE: The limits are stored with the volume when it is provisioned. Changing the parameters of a StorageClass only affects newly provisioned volumes. See Changing the Limits of an Existing Volume for existing volumes.
The limits apply to each container individually: every container using the volume receives its own io.max entry,
so two containers sharing a volume can each use the full configured limit.
Verifying the Limits¶
Create a volume using the StorageClass and a Job running the fio disk benchmark:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qos-test-volume
spec:
storageClassName: piraeus-storage-qos
accessModes: [ ReadWriteOnce ]
resources:
requests:
storage: 1Gi
---
apiVersion: batch/v1
kind: Job
metadata:
name: qos-test
spec:
template:
spec:
restartPolicy: Never
containers:
- name: fio
image: nixery.dev/fio
command:
- fio
- --name=qos-test
- --filename=/volume/test.dat
- --size=256m
- --rw=randrw
- --bs=128k
- --direct=1
- --ioengine=libaio
- --iodepth=8
- --runtime=30
- --time_based
volumeMounts:
- name: volume
mountPath: /volume
volumes:
- name: volume
persistentVolumeClaim:
claimName: qos-test-volume
While the Job is running, verify that the CSI driver published the limits in the VolumeAttachment resource:
$ kubectl get volumeattachments -o yaml | grep "qos.linbit.com"
qos.linbit.com/device: /dev/drbd1000
qos.linbit.com/rbps: "41943040"
qos.linbit.com/riops: "4000"
qos.linbit.com/wbps: "20971520"
qos.linbit.com/wiops: "2000"
Once the Job completes, the reported bandwidth matches the configured limits of 40 MiB/s read and 20 MiB/s write:
$ kubectl logs job/qos-test | grep -E "READ|WRITE"
READ: bw=40.1MiB/s (42.0MB/s), 40.1MiB/s-40.1MiB/s (42.0MB/s-42.0MB/s), io=1204MiB (1262MB), run=30020-30020msec
WRITE: bw=20.0MiB/s (21.0MB/s), 20.0MiB/s-20.0MiB/s (21.0MB/s-21.0MB/s), io=601MiB (630MB), run=30020-30020msec
Changing the Limits of an Existing Volume¶
The limits are stored as auxiliary properties on the LINSTOR resource definition. To change them for an existing volume, set the properties using the LINSTOR client, referencing the volume by its PersistentVolume name:
$ kubectl -n piraeus-datastore exec deploy/linstor-controller -- \
linstor resource-definition set-property --aux pvc-1e24339c-ee8d-4c0e-a68e-cbc85c866704 qos.linbit.com/wbps 10Mi
The new limits take effect the next time the volume is attached: delete the Pods using the volume, wait for the volume to be detached from the node, and start the Pods again.