Volume Tiering

SeaweedFS Enterprise places every write on the storage it deserves and keeps moving it as it cools. A volume class routes each object by its path — table-format metadata to fast replicated SSD, bulk data to capacity disks — and a tiering profile ages each class down a ladder: replicated → erasure-coded → cloud. The rules for Iceberg, Delta, and Hudi layouts are built in, so a lakehouse workload usually needs only two profiles and two bindings.

Object write …/metadata/*.json …/metadata/*.avro …/data/*.parquet volume class path glob → class meta data SSD · replicated hot metadata, stays put HDD · replicated fresh data HDD · EC cold, ~1.4× cloud store coldest cools → cools → tiering profile: age down the ladder
Selectors classify each write, the class picks its landing tier, and a profile ages the class down a ladder as it cools.

Disk types are the tiers

A tier is just a tag on a volume server. Start servers with -disk=ssd, -disk=hdd, or any label you like (nvme, hdd_bulk, …), one per data directory:

weed volume -dir=/fast -disk=ssd  -mserver=:9333 ...
weed volume -dir=/d1,/d2,/d3 -disk=hdd,hdd,hdd -mserver=:9333 ...

A volume’s identity includes its disk type (and its class), so tiers never share storage: the master places and grows volumes per type.

Volume classes route each write

Selector rules map object paths to a class with glob patterns, first match wins. The table-format conventions ship built in — configure nothing and these already resolve:

Class Built-in patterns
meta **/metadata/**, **/_delta_log/**, **/.hoodie/**, **/*.metadata.json, **/version-hint.text, **/*.avro
data **/*.parquet, **/*.orc

The ordering is deliberate: a Delta checkpoint is a .parquet file under _delta_log/ and correctly classifies as meta — the trap a hand-written *.parquet → data rule falls into. Your own selectors go above the built-ins and win. Built-in rules stay inert until a binding uses their class, so an upgraded cluster that configures nothing places exactly as before.

Classification happens on the write path, server-side: an S3 PUT and a filer write to the same key classify identically, with no client changes.

Profiles: the ladder

A profile declares where a class’s volumes are born (the landing: disk type + replication) and the tiers they descend into as they go quiet. The whole policy is one document applied with tiering.apply:

{
  "tailClass": "meta",
  "profiles": [
    { "name": "hot-meta",
      "landing": { "diskType": "ssd", "replication": "001" } },
    { "name": "cold-data",
      "landing": { "diskType": "hdd", "replication": "001" },
      "tiers": [
        { "name": "ec", "when": { "quietForSeconds": 604800 },
          "ec": { "diskType": "hdd", "dataShards": 10, "parityShards": 4 } },
        { "name": "archive", "when": { "quietForSeconds": 7776000 },
          "remote": { "backend": "s3.archive" } }
      ] }
  ],
  "bindings": [
    { "collectionPattern": "*", "volumeClass": "meta", "profileName": "hot-meta" },
    { "collectionPattern": "*", "volumeClass": "data", "profileName": "cold-data" }
  ]
}

Each tier is one of three forms: replicated (another disk type + replication code), ec (erasure-coded, e.g. 10+4 ≈ 1.4× storage for the durability of 3× replication), or remote (a cloud backend from master.toml). Triggers combine quiet time, fullness, and minimum size. Volumes only descend; the volume_tiering plugin worker performs the transitions in the background. The guidance for metadata is baked into the model: raise its replication, never erasure-code or off-load it — data takes the full ladder.

A fast tier that cannot overflow

Per-medium pressure rules demote the coldest volumes off a filling disk type ahead of their age triggers, between a high and a low watermark. A small NVMe/SSD tier drains itself instead of filling up — capacity relief, while the age ladder still governs everything else.

Parquet footers stay fast

One leak in a metadata/data split: a .parquet file is data, so its footer — read on every query’s critical path, before any data page — would age onto cold storage with the body. The tailClass setting and the Cache Parquet Footers job close it: the job follows the filer’s metadata stream, and for each new Parquet file caches the footer plus page index into the tail class on fast media, recorded on the entry separately from the body chunks. The cached extent is floored at 64 KiB of tail because that is the speculative read most engines open with (Arrow’s default) — the opening read is served entirely from the fast tier, and only the row groups a query actually touches come off cold storage. The body still erasure-codes and off-loads on schedule.

Configure from the Admin UI

Everything in the document above can be managed interactively under Storage → Volume Tiering. The Profiles tab shows each profile as a card — where its data currently rests on the ladder, the landing placement, and the rung table — with the binding order below it and a probe box to test which profile a collection name resolves to:

The Admin UI Volume Tiering page, Profiles tab. Two profile cards: hot-meta with landing ssd 001 and no tiers, showing 6 volumes and 11.1 KB resting on its landing; cold-data with landing hdd 001 and two rungs — ec after quiet 7d becoming ec hdd 10+4, and archive after quiet 90d becoming remote s3.archive. Below, the Binding order section lists selector rules and two active bindings: collection vec class meta to hot-meta, and collection vec class data to cold-data, with a box to test a collection name.
The Profiles tab: each card shows where the profile's data rests on its ladder and its rung table; below, the binding order — first match wins. The selector list shows configured rules only; the built-in Iceberg/Delta/Hudi rules apply beneath them.

Editing a profile opens a full editor — identity and landing placement, one card per rung with its triggers and target form, and a live verdict of what the saved ladder would do to the volumes currently bound to it:

The Admin UI profile editor for cold-data: fields for profile name, landing disk type hdd, landing replication 001, optional landing data center; a Tier ladder section with Rung 1 named ec — trigger fields Quiet for seconds 604800, fullness ratio, minimum size MB — and What it becomes: form ec with disk type hdd, data shards 10, parity shards 4, optional shard placement. A sidebar shows Where its data rests per rung and a check that every bound volume is where this ladder wants it.
The profile editor. Every criterion in a rung's trigger must hold at once; leaving one empty ignores it. The sidebar verdict describes the profile as stored — save to see edits reflected.

Operating it

weed shell
> tiering.apply -file=tiering.json     # validate + apply the document
> tiering.bind -collection=vec -class=data -profile=cold-data
> tiering.status -v                    # classify volumes against their ladders
> tiering.run                          # execute pending moves now

The volume_tiering and tail_cache jobs run on plugin workers (weed worker -admin=...); enable them per job type from the Admin UI. Transitions use the same primitives as the manual commands (ec.encode, volume.tier.move, volume.tier.upload), so everything the worker does you can also do by hand.

Notes

  • Volume tiering, volume classes, and the Parquet footer cache are SeaweedFS Enterprise features. The manual primitives (disk types, ec.encode, volume.tier.move) are open source.
  • A class is a placement preference: if no volume of the class exists and none can be created, the write still succeeds on ordinary volumes — slower reads, never failures.
  • The footer cache picks up files written from the moment it is enabled; pre-existing files are a backfill.

See it run end to end — a table bucket with metadata pinned to SSD, data aged into erasure coding, and a cached footer verified on the fast tier — in the walkthrough blog post. For the configuration document, Admin UI, trigger semantics, and the footer cache’s exact behavior, see the technical reference.

Related: Erasure Coding · S3 Table Buckets · Sealed Directories · Data Movement