Skip to content

Commit 76965d0

Browse files
doc: add design proposal for topology-aware cluster selection
Add a design document describing topology-aware multi-cluster volume provisioning. This enables the CSI driver to dynamically select the appropriate Ceph cluster at CreateVolume time based on the node's topology zone. The proposal introduces two configuration mechanisms: - topologyDomainLabels field in config.json cluster entries - clusterIDs StorageClass parameter (comma-separated list) Ref: #5177 Signed-off-by: WMP <example@example.com>
1 parent 8df46e3 commit 76965d0

1 file changed

Lines changed: 311 additions & 0 deletions

File tree

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# Topology-Aware Multi-Cluster Volume Provisioning
2+
3+
Currently Ceph-CSI supports only a single Ceph cluster per StorageClass. The
4+
`clusterID` parameter in the StorageClass is mandatory and points to exactly one
5+
cluster entry in `config.json`. This works well for single-cluster environments,
6+
but creates a significant limitation for distributed Kubernetes deployments
7+
spanning multiple geographic zones, each backed by a separate Ceph cluster.
8+
9+
In such deployments administrators must create a separate StorageClass per
10+
zone/cluster, and application teams must manually select the correct
11+
StorageClass depending on where their workloads run. This defeats the purpose of
12+
Kubernetes topology-aware scheduling and creates operational overhead.
13+
14+
Reference: https://github.com/ceph/ceph-csi/issues/5177
15+
16+
## Problem
17+
18+
Consider a Kubernetes cluster with nodes spread across two zones, each served
19+
by a separate Ceph cluster:
20+
21+
- `zone-poland` with Ceph cluster `cluster-poland` (monitors: `10.0.1.1:6789`)
22+
- `zone-france` with Ceph cluster `cluster-france` (monitors: `10.0.2.1:6789`)
23+
24+
Today, the administrator must create two StorageClasses:
25+
26+
```yaml
27+
apiVersion: storage.k8s.io/v1
28+
kind: StorageClass
29+
metadata:
30+
name: csi-rbd-poland
31+
provisioner: rbd.csi.ceph.com
32+
parameters:
33+
clusterID: "cluster-poland"
34+
pool: replicapool
35+
---
36+
apiVersion: storage.k8s.io/v1
37+
kind: StorageClass
38+
metadata:
39+
name: csi-rbd-france
40+
provisioner: rbd.csi.ceph.com
41+
parameters:
42+
clusterID: "cluster-france"
43+
pool: replicapool
44+
```
45+
46+
Application teams must then know which StorageClass to use based on where their
47+
pods will be scheduled. If a pod moves to a different zone, the PVC might point
48+
to a remote cluster, losing data locality.
49+
50+
The goal is to have a **single StorageClass** that automatically selects the
51+
correct Ceph cluster based on the node's topology zone.
52+
53+
## Proposed Solution
54+
55+
### Configuration Changes
56+
57+
#### config.json
58+
59+
Each cluster entry in `config.json` gains an optional `topologyDomainLabels`
60+
field that maps Kubernetes topology label keys to their expected values:
61+
62+
```yaml
63+
apiVersion: v1
64+
kind: ConfigMap
65+
data:
66+
config.json: |-
67+
[
68+
{
69+
"clusterID": "cluster-poland",
70+
"topologyDomainLabels": {
71+
"topology.kubernetes.io/zone": "zone-poland"
72+
},
73+
"monitors": [
74+
"10.0.1.1:6789"
75+
],
76+
"rbd": {
77+
"radosNamespace": ""
78+
},
79+
"cephFS": {
80+
"subvolumeGroup": "csi"
81+
}
82+
},
83+
{
84+
"clusterID": "cluster-france",
85+
"topologyDomainLabels": {
86+
"topology.kubernetes.io/zone": "zone-france"
87+
},
88+
"monitors": [
89+
"10.0.2.1:6789"
90+
],
91+
"rbd": {
92+
"radosNamespace": ""
93+
},
94+
"cephFS": {
95+
"subvolumeGroup": "csi"
96+
}
97+
}
98+
]
99+
metadata:
100+
name: ceph-csi-config
101+
```
102+
103+
Clusters without `topologyDomainLabels` are ignored during topology-based
104+
selection and continue to work exactly as before.
105+
106+
#### StorageClass
107+
108+
A new parameter `clusterIDs` is introduced as a comma-separated list of
109+
candidate cluster IDs. The StorageClass **must** use
110+
`volumeBindingMode: WaitForFirstConsumer` so that Kubernetes provides topology
111+
hints to the CSI driver via `AccessibilityRequirements` in the `CreateVolume`
112+
request.
113+
114+
```yaml
115+
apiVersion: storage.k8s.io/v1
116+
kind: StorageClass
117+
metadata:
118+
name: csi-rbd-topology
119+
provisioner: rbd.csi.ceph.com
120+
parameters:
121+
clusterIDs: "cluster-poland,cluster-france"
122+
pool: replicapool
123+
imageFeatures: layering
124+
csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret
125+
csi.storage.k8s.io/provisioner-secret-namespace: ceph-system
126+
volumeBindingMode: WaitForFirstConsumer
127+
reclaimPolicy: Delete
128+
```
129+
130+
> **Note:** The existing `clusterID` parameter continues to work as before.
131+
> When `clusterID` is present, it takes priority and the topology-based
132+
> selection is not used. The `clusterIDs` parameter is only consulted when
133+
> `clusterID` is absent.
134+
135+
### How PV Creation Works
136+
137+
Topology-aware cluster selection relies on the Kubernetes topology mechanism
138+
built into the CSI specification. Understanding how topology information flows
139+
from nodes to the `CreateVolume` call is key to understanding the design.
140+
141+
#### Topology Discovery
142+
143+
When the CSI node plugin (DaemonSet) starts on each node, Kubernetes calls
144+
`NodeGetInfo`. The driver reads the node's Kubernetes labels (configured via
145+
the `--domainlabels` flag) and returns them as `AccessibleTopology` segments.
146+
Kubernetes stores this information in the `CSINode` object.
147+
148+
For example, a node with the label `topology.kubernetes.io/zone=zone-poland`
149+
reports:
150+
151+
```json
152+
{
153+
"accessible_topology": {
154+
"segments": {
155+
"topology.kubernetes.io/zone": "zone-poland"
156+
}
157+
}
158+
}
159+
```
160+
161+
#### WaitForFirstConsumer Binding
162+
163+
The StorageClass **must** use `volumeBindingMode: WaitForFirstConsumer`. This
164+
tells Kubernetes to delay volume provisioning until a pod consuming the PVC is
165+
scheduled to a specific node. Without this, Kubernetes calls `CreateVolume`
166+
immediately (with `Immediate` binding) and does not know which node the pod
167+
will run on — so no `AccessibilityRequirements` are provided and topology-based
168+
selection cannot work.
169+
170+
#### AccessibilityRequirements: Preferred vs Requisite
171+
172+
When Kubernetes calls `CreateVolume` after scheduling the pod, it includes
173+
`AccessibilityRequirements` with two lists of topologies:
174+
175+
- **Preferred** — an ordered list of topologies where the volume should ideally
176+
be created. The first entry is the topology of the node where the pod was
177+
scheduled. This is what we use for data locality — placing storage close to
178+
compute.
179+
180+
- **Requisite** — a list of all topologies where the volume is allowed to be
181+
created (hard constraints). This includes all nodes that have capacity to
182+
serve the volume.
183+
184+
For example, when a pod is scheduled on a node in `zone-poland` in a cluster
185+
that also has nodes in `zone-france`:
186+
187+
```
188+
Preferred: [zone-poland] ← the pod's node
189+
Requisite: [zone-poland, zone-france] ← all eligible zones
190+
```
191+
192+
The driver checks Preferred first (for data locality), and falls back to
193+
Requisite only if no Preferred topology matches any cluster.
194+
195+
#### End-to-End Flow
196+
197+
When a pod is scheduled on a node in `zone-poland` and requests a PVC from the
198+
topology-aware StorageClass, the following happens:
199+
200+
1. Kubernetes sees `volumeBindingMode: WaitForFirstConsumer` and delays
201+
provisioning until the pod is scheduled to a specific node.
202+
203+
2. Once the pod is bound to a node, Kubernetes calls `CreateVolume` with
204+
`AccessibilityRequirements` containing the node's topology segments
205+
(e.g. `topology.kubernetes.io/zone: zone-poland`).
206+
207+
3. The CSI driver first tries to resolve `clusterID` from the StorageClass
208+
parameters. Since it is not present, the driver falls back to
209+
topology-based cluster selection.
210+
211+
4. The driver parses the `clusterIDs` parameter to get the list of candidate
212+
clusters: `["cluster-poland", "cluster-france"]`.
213+
214+
5. For each candidate, the driver reads the `topologyDomainLabels` from
215+
`config.json` and matches them against the `AccessibilityRequirements`.
216+
All labels defined in the cluster's `topologyDomainLabels` must be present
217+
and have matching values in the topology segments.
218+
219+
6. Preferred topologies (from the CO's scheduling preference) are checked
220+
first. If no match is found, requisite topologies (hard constraints) are
221+
checked as a fallback.
222+
223+
7. The first matching cluster is selected. In this example, `cluster-poland`
224+
matches because its `topologyDomainLabels` contain
225+
`topology.kubernetes.io/zone: zone-poland`, which matches the node's zone.
226+
227+
8. The selected `clusterID` is used to resolve monitors from `config.json`.
228+
The driver connects to the Ceph cluster in Poland and creates the RBD image
229+
(or CephFS subvolume) there.
230+
231+
9. The selected `clusterID` is encoded into the `volumeHandle`, so all
232+
subsequent operations (NodeStage, ExpandVolume, DeleteVolume) resolve the
233+
correct cluster automatically, without needing topology selection again.
234+
235+
### Multi-Dimensional Topology
236+
237+
The `topologyDomainLabels` field supports multiple labels for multi-dimensional
238+
matching. For example, a cluster can be associated with both a region and a
239+
zone:
240+
241+
```json
242+
{
243+
"clusterID": "cluster-poland-az1",
244+
"topologyDomainLabels": {
245+
"topology.kubernetes.io/region": "europe",
246+
"topology.kubernetes.io/zone": "poland-az1"
247+
}
248+
}
249+
```
250+
251+
All labels must match for the cluster to be selected.
252+
253+
## Impact on Existing Operations
254+
255+
The topology-based cluster selection only affects the `CreateVolume` operation.
256+
All other CSI operations are unaffected because the `volumeHandle` already
257+
contains the selected `clusterID`:
258+
259+
- **NodeStageVolume / NodePublishVolume** — the node plugin decodes the
260+
`clusterID` from the `volumeHandle` and connects to the correct cluster.
261+
No topology resolution needed.
262+
263+
- **DeleteVolume / ControllerExpandVolume** — the controller decodes the
264+
`clusterID` from the `volumeHandle`. Same behavior as today.
265+
266+
- **CreateSnapshot** — uses the source volume's `clusterID`.
267+
268+
The provisioner pod (Deployment) must have network access to monitors of all
269+
Ceph clusters listed in `config.json`. This is already the case when multiple
270+
clusters are configured today. The node plugin pods (DaemonSet) also mount the
271+
same `ceph-csi-config` ConfigMap and can connect to any cluster whose volumes
272+
they need to mount.
273+
274+
Connection lifecycle is unchanged — the driver uses the existing connection pool
275+
(`conn_pool.go`) which manages connections by `monitors|user|keyfile`
276+
combination and auto-recycles unused connections.
277+
278+
## Backward Compatibility
279+
280+
- Existing `config.json` entries without `topologyDomainLabels` work unchanged.
281+
The new field uses `omitempty` in JSON serialization.
282+
283+
- StorageClasses with a single `clusterID` parameter use the existing fast
284+
path. The topology selection code is never reached.
285+
286+
- The `clusterIDs` parameter is purely additive. No existing parameters or
287+
validation rules are removed.
288+
289+
- Volumes created with topology-based selection are indistinguishable from
290+
volumes created with an explicit `clusterID` — the `volumeHandle` format is
291+
identical.
292+
293+
## Limitations
294+
295+
- `volumeBindingMode: WaitForFirstConsumer` is required when using `clusterIDs`.
296+
With `Immediate` binding, Kubernetes does not provide
297+
`AccessibilityRequirements` and the driver cannot determine the target
298+
topology.
299+
300+
- The pool name must be the same across all candidate clusters (since a single
301+
`pool` parameter is specified in the StorageClass). If pools have different
302+
names, the existing `topologyConstrainedPools` mechanism can be combined with
303+
this feature in a future iteration.
304+
305+
## Future Work
306+
307+
- Make `clusterID` fully optional when `clusterIDs` is provided (currently both
308+
are accepted, but at least one is required).
309+
- Combine topology-based cluster selection with `topologyConstrainedPools` for
310+
selecting both cluster and pool based on topology.
311+
- Add E2E tests with a multi-cluster topology setup.

0 commit comments

Comments
 (0)