CephXCII · Removing Storage NodesRemoving Storage Nodes
CRUSH map hygiene after removals
What you'll learn
- Identify CRUSH residue after removals
- Decide what to clean and what to keep
- Clean safely
- Prevent accumulation
Prerequisites
None — start here.
Verified against Ceph Tentacle 20.2.x · Ceph Squid 19.2.x (supported previous) · cephadm matches the verified Ceph release · podman 4.x · csi-rbd and csi-cephfs current · RBD / CephFS / RGW current (matches Ceph release) · Linux kernel 5.15+ (5.10 minimum) · Ubuntu 24.04 LTS (Ceph host baseline) · Debian 12 (Bookworm) (Ceph host baseline) · Rocky Linux / RHEL / AlmaLinux 9.x (Ceph host baseline) · Proxmox VE 9.x (cross-course integration) · Kubernetes 1.31+ (cross-course integration) · 2026-08-18
Why this matters in production
A CRUSH map accumulates empty buckets, stale rules, and orphaned entries. None breaks anything and together they make the map an unreliable description of the cluster.
The residue
| Residue | Left by |
|---|---|
| Empty host buckets | removing all a host’s OSDs |
| Empty rack or zone buckets | removing the last host in them |
| Unused rules | pools deleted, or rules created for testing |
destroyed OSD entries | --replace where the replacement never arrived |
| Orphaned auth entries | partial OSD removal |
| Device classes with no members | all devices of a class removed |
ceph osd crush tree
ceph osd crush rule ls
ceph osd tree | grep -E 'destroyed|DNE'
ceph osd crush class ls
Finding it
# empty buckets
ceph osd crush tree --format json | python3 -c '
import sys, json
def walk(n, path=""):
p = path + "/" + n["name"]
kids = n.get("children", [])
if n["type"] not in ("osd",) and not kids:
print("empty:", n["type"], n["name"])
for c in kids:
if isinstance(c, dict): walk(c, p)
for r in json.load(sys.stdin)["nodes"]:
if r.get("type") == "root": walk(r)'
# rules not used by any pool
used=$(ceph osd pool ls detail | grep -oE 'crush_rule [0-9]+' | awk '{print $2}' | sort -u)
ceph osd crush rule ls | while read r; do
id=$(ceph osd crush rule dump "$r" | python3 -c 'import sys,json;print(json.load(sys.stdin)["rule_id"])')
echo "$used" | grep -qx "$id" || echo "unused rule: $r (id $id)"
done
# destroyed OSDs with no replacement
ceph osd tree | grep destroyed
What to clean and what to keep
| Item | Decision |
|---|---|
| Empty host bucket, host gone permanently | remove |
| Empty host bucket, host returning | keep |
| Empty rack, no plans for it | remove |
| Empty rack, expansion planned into it | keep |
| Unused rule, no plans | remove |
| Unused rule, used by a documented procedure | keep and document |
destroyed OSD, replacement coming | keep — the ID is reserved for it |
destroyed OSD, no replacement | purge to release the ID |
# the destroyed OSD id from the `ceph osd tree | grep destroyed` output above:
OSD_ID=13
ceph osd purge "$OSD_ID" --yes-i-really-mean-it
Cleaning safely
# a bucket must be empty before removal
RULENAME=rulename
ceph osd crush remove ceph-07
# a rule must be unused
ceph osd crush rule rm ${RULENAME}
Both commands refuse if the object is in use, which makes the cleanup safe: an attempt that would break something fails rather than succeeding.
# verify the tree afterwards
ceph osd crush tree
ceph -s
Preventing accumulation
Make cleanup part of the removal procedure rather than a separate task:
remove OSDs
remove the host
remove the host bucket
remove the rack bucket if now empty
purge any destroyed IDs not awaiting a replacement
# a periodic audit
ceph osd crush tree | grep -B1 -A1 'rack\|host' | head -40
ceph osd tree | grep -cE 'destroyed|DNE'
Quiz
Knowledge check · 4 questions
Q1. What does a `destroyed` OSD entry in the CRUSH tree reserve?
Q2. CRUSH cleanup commands require careful verification because they can break placement.
Q3. Audit a CRUSH map after several years of changes.
A cluster has had many hosts added and removed over several years. The CRUSH tree contains empty buckets and there are several destroyed OSD entries of unknown age.
Q4. Why does CRUSH residue matter if it breaks nothing?
Passing score: 75%. Answers are checked in this browser.
Production discipline
Add CRUSH cleanup to the removal procedure rather than treating it as a separate task — empty buckets and destroyed entries accumulate and make failure domain calculations wrong. The cleanup commands refuse when the object is in use, so the audit is low-risk.
Cross-course references
- Kubernetes: stale resource definitions accumulate the same way and mislead the same way
- Linux: configuration describing hardware that no longer exists misleads every reader