# Ceph EC Pool Optimization for Mixed HDD Sizes — 2026-07-13

## Problem

When a Ceph cluster has HDDs of different sizes (1 TB and 3 TB) and uses
erasure coding, the 3 TB drives are severely underutilized. This is because
EC stores equally-sized chunks per PG on each OSD — regardless of OSD
capacity. A 1 TB OSD and a 3 TB OSD in the same PG each store 1/(k+m) of
the object.

### Why `ceph osd reweight` Doesn't Help for EC

With EC k=4, m=1 (5 OSDs per PG) and 6 HDD OSDs total, CRUSH selects 5 of 6.
`ceph osd reweight` only changes the probability of being selected — but
with 5/6 selection, even a heavily downweighted OSD is chosen in most PGs.
The effect is marginal compared to replicated pools where each OSD
independently participates.

### Symptoms

| Signal | Cause |
|--------|-------|
| 3 TB OSD at 2% while 1 TB OSD at 94% | Equal chunk sizes fill small OSDs first |
| Same PG count on 3 TB and 1 TB OSDs | CRUSH weight not proportional, or EC masking the effect |
| `backfill_toofull` on small OSDs | Small OSDs hit full ratio before large ones fill |
| `ceph osd reweight` has no visible effect | EC selection ratio too tight (5/6) |

## Architectural Solution

Separate large and small HDDs into different device classes, place them
under a dedicated CRUSH root, and create an EC pool that only uses the
large HDDs with a smaller k+m profile.

### New CRUSH Topology

```
root default                    root hdd-root (NEW)
├── host proxmox2-5 (SSDs)       ├── host proxmox1    → osd.1  (hdd-large, 3.6 TB)
│                               ├── host ubuntu       → osd.6  (hdd-large, 2.8 TB)
│                               │                   └ → osd.8  (hdd-large, 2.8 TB)
│                               ├── host n5pro        → osd.11 (hdd-large, 2.8 TB)
│                               ├── host proxmox6     → osd.10 (hdd-small, 1 TB)
│                               └── host proxmox7     → osd.7  (hdd-small, 1 TB)
```

### Key Insight: `take` with vs without Class Filter

- `take hdd-root` (no class filter) → selects ALL HDDs (large + small) → for replicated pools
- `take hdd-root~hdd-large` → selects only large HDDs → for EC pool

This allows large HDDs to serve both EC and replicated pools, while small
HDDs only participate in replicated pools.

### New EC Profile

| Parameter | Old (ec41-hdd) | New (ec31-hdd-large) |
|-----------|----------------|---------------------|
| k | 4 | **3** |
| m | 1 | **1** |
| OSDs per PG | 5 | **4** |
| Overhead | 20% | **33%** |
| Available OSDs | 6 (2 too small) | **4 (all fully utilized)** |
| Failure tolerance | 1 OSD | 1 OSD |

Trade-off: EC overhead increases from 20% → 33%, but all 4 large OSDs are
fully utilized. Net usable capacity is HIGHER because the 3 TB drives
actually fill up instead of sitting at 2%.

## Migration Plan (6 Phases)

### Phase 0: Wait for Current Rebalance

```bash
ceph -s  # until "misplaced 0%" and "recovery: 0 B/s"
```

### Phase 1: CRUSH Structure (Triggers Remapping)

```bash
# 1a. Device classes
ceph osd crush set-device-class hdd-large osd.1 osd.6 osd.8 osd.11
ceph osd crush set-device-class hdd-small osd.7 osd.10

# 1b. New root
ceph osd crush add-bucket hdd-root root

# 1c. Move HDD hosts to hdd-root (triggers remapping!)
ceph osd crush move proxmox1 root=hdd-root
ceph osd crush move ubuntu root=hdd-root
ceph osd crush move n5pro root=hdd-root
ceph osd crush move proxmox6 root=hdd-root
ceph osd crush move proxmox7 root=hdd-root
```

⚠️ Step 1c triggers IMMEDIATE remapping of all HDD PGs. For gentler
migration: move hosts one at a time with 30-min intervals between each,
waiting for rebalance to stabilize.

### Phase 2: New Rules + EC Profile

```bash
# 2a. New EC profile (k=3, m=1, large HDDs only)
ceph osd erasure-code-profile set ec31-hdd-large \
  k=3 m=1 \
  crush-root=hdd-root \
  crush-device-class=hdd-large \
  crush-failure-domain=osd \
  plugin=jerasure technique=reed_sol_van

# 2b. New EC rule
ceph osd crush rule create-erasure media_ec_large ec31-hdd-large

# 2c. New replicated rule (ALL HDDs, no class filter)
ceph osd crush rule create-replicated replicated_hdd_all hdd-root host
```

### Phase 3: Switch Replicated Pools

```bash
ceph osd pool set hdd_disk crush_rule replicated_hdd_all
ceph osd pool set rbd crush_rule replicated_hdd_all
ceph osd pool set tm_disks crush_rule replicated_hdd_all
```

### Phase 4: EC Pool Migration (Critical)

EC pools cannot change their k/m profile. Must create a new pool and
migrate data.

#### RBD Online Migration (preferred — VMs keep running)

```bash
# 4a. Create new EC pool
ceph osd pool create media_ec_new 128 128 erasure ec31-hdd-large
ceph osd pool set media_ec_new crush_rule media_ec_large
ceph osd pool set media_ec_new ec_overwrites
ceph osd pool application enable media_ec_new rbd

# 4b. Create new metadata pool
ceph osd pool create media_meta_new 32
ceph osd pool set media_meta_new crush_rule replicated_ssd

# 4c. Migrate each RBD image (online, VM keeps running)
for img in vm-114-disk-0 vm-136-disk-0 vm-137-disk-0 vm-138-disk-1 vm-143-disk-0; do
  rbd migration prepare media_meta/${img} media_meta_new/${img} \
    --data-pool media_ec_new
  rbd migration execute media_meta_new/${img}
  rbd migration commit media_meta_new/${img}
done
```

Per-image timeline: `prepare` (instant, starts copy) → `execute` (copies
data, 30-60 min per 500 GiB image) → `commit` (atomic switch, old image
deleted). VMs continue running throughout — the migration is transparent.

#### Offline Alternative (faster, VM stopped)

Stop VM, `rbd copy --data-pool media_ec_new old_pool/img new_meta_pool/img`,
update VM disk config, start VM. See `rbd-pool-migration-2026-07.md`.

### Phase 5: Cleanup

```bash
ceph osd pool delete media_ec --yes-i-really-really-mean-it
ceph osd crush rule rm media_ec
ceph osd crush rule rm replicated_hdd
ceph osd crush rm proxmox       # empty host bucket
ceph osd crush rm px-tmp20      # empty host bucket
ceph osd erasure-code-profile rm ec41-hdd
ceph osd pool rename media_ec_new media_ec
ceph osd pool rename media_meta_new media_meta
```

### Phase 6: Correct CRUSH Weights

```bash
# Now safe — small HDDs no longer in EC pool
ceph osd crush reweight osd.7 0.96   # was 0.50, should be ~0.96 for 1 TB
ceph osd crush reweight osd.10 0.96  # was 0.30, should be ~0.96 for 1 TB
```

## Capacity Analysis

| Pool | Raw Capacity | Usable | Currently Used | Free |
|------|-------------|--------|----------------|------|
| media_ec (EC 3+1) | 4× ~2.9 TB = 11.6 TB | 8.7 TB | 764 GiB | ~7.9 TB |
| hdd_disk (Repl 3) | 15 TB | 5.0 TB | 1.0 TiB | ~4.0 TB |

## Safety-Net

- Backup CRUSH map: `ceph osd getcrushmap -o /tmp/crushmap.backup`
- RBD snapshots of critical images before migration
- Rollback: restore CRUSH map, revert pool rules

## Pitfalls

### EC Pools Cannot Change k/m In Place

An EC pool's erasure-code profile is fixed at creation. Changing k or m
requires creating a new pool and migrating data. Do NOT attempt
`ceph osd pool set media_ec erasure-code-profile ec31-hdd-large` — it
will fail or corrupt data.

### `rbd migration prepare` Needs Both Pools

The `prepare` command takes `source_image dest_image` where dest is in
the new metadata pool. The `--data-pool` flag specifies where the EC data
chunks go. The metadata pool stores RBD headers (small, replicated).

### Moving Hosts Between Roots Triggers Full Remap

Every `ceph osd crush move` changes the CRUSH hash inputs for all PGs in
that subtree. Moving 5 hosts at once causes massive simultaneous remapping.
Move hosts individually with rebalance gaps for production clusters.

### Device Class Must Match Reality

`ceph osd crush set-device-class hdd-large osd.X` overrides the
auto-detected class. If the OSD is later recreated, it reverts to
auto-detected `hdd`. Re-apply the custom class after any OSD rebuild.

## Gitea Issue Creation Pattern

This plan was saved as Gitea issue #14 in `dominik/iac-homelab`. Pattern
for large issue bodies:

```bash
# 1. Write JSON to temp file locally
cat > /tmp/issue.json << 'EOF'
{"title":"...", "body":"...markdown...", "labels":[]}
EOF

# 2. SCP to Gitea host (CT108 on proxmox6)
scp -i ~/.ssh/id_ed25519_proxmox /tmp/issue.json root@10.0.30.105:/tmp/

# 3. POST via curl on the Gitea host (avoids remote curl auth issues)
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.30.105 \
  "curl -s -X POST -H 'Authorization: token TOKEN' \
   -H 'Content-Type: application/json' \
   -d @/tmp/issue.json \
   http://localhost:3000/api/v1/repos/dominik/iac-homelab/issues"
```

Using a file (`-d @file`) avoids shell escaping issues with large markdown
bodies containing backticks, quotes, and special characters.

See also: `references/gitea-api-and-skills-versioning-2026-07.md` for
token generation and full API patterns.
