Skip to content

How it fits together

For anyone running this. What the moving parts are, what they touch, and what happens when something goes wrong.

The pieces

flowchart TB
    subgraph clients["users"]
        XR["xarray / zarr readers"]
    end

    subgraph gateway["S3 gateway"]
        GW["Versity"]
        EV[("event table<br/>Postgres")]
    end

    subgraph disk["hot pool"]
        DATA[("data buckets<br/>zarr stores")]
        MAN[("manifest bucket<br/><b>the interface</b><br/>one JSON per store")]
    end

    TAPE[("tape")]

    subgraph services["services"]
        BM["blobmap<br/>decides the cuts"]
        BT["blobtier<br/>owns tier state"]
        ST[("state table<br/>Postgres")]
    end

    XR -->|"GET chunk"| GW
    GW --> DATA
    GW -->|"one row per request"| EV

    BM -->|"LIST + read metadata"| DATA
    BM -->|"writes manifests"| MAN
    BM -->|"polls: new stores?"| EV

    BT -->|"reads manifests at startup"| MAN
    BT -->|"polls: what was read?"| EV
    BT --> ST
    BT -->|"archive / restore"| TAPE
    TAPE -.->|"restore"| DATA

blobmap decides which objects belong together and writes that down. It runs occasionally: once per store, plus a check when a store changes.

blobtier decides what actually moves, and when. It runs continuously.

The manifest bucket is the interface between them. It is small, it is plain JSON, and it is rebuildable by re-running blobmap.

What each thing needs access to

component data buckets manifest bucket event table state table tape
blobmap read write read
blobtier read read read write read/write

blobmap never writes to the data buckets. That is deliberate and is what lets it manage data DKRZ does not own.

Service accounts

Each component needs its own S3 principal, and blobtier must be told to ignore its own and blobmap's.

This is not cosmetic. The tiering job reads every object in a blob in order to move it to tape. Without principal filtering, archiving a blob marks it as freshly accessed, so it immediately looks hot again and never gets archived. The same applies to restores, verification sweeps and backups.

flowchart LR
    A["user reads"] -->|counted| S["last_read"]
    B["blobmap metadata reads"] -.->|ignored| S
    C["blobtier archive reads"] -.->|ignored| S
    D["backup sweep"] -.->|ignored| S

What runs when

flowchart TB
    NEW["new store lands"] --> P1["blobmap partitions it once"]
    P1 --> M["manifest written"]

    APP["data appended"] --> NOOP["nothing runs<br/>the rule already covers it"]

    VAR["new variable added"] --> EVT["metadata write seen"]
    EVT --> DEB["wait for quiet"]
    DEB --> P2["repartition: adds blobs,<br/>never moves existing ones"]

    NIGHT["nightly"] --> POL["blobtier: which blobs are cold?"]
    POL --> ARC["archive"]

    READ["user reads cold data"] --> RES["restore whole blob<br/>one tape mount"]

The important one is the second row. Appending to a store needs no repartition, no manifest change, and no service to notice, because the blob id is computed arithmetically from the chunk number.

Configuration that matters

MinIO notifications must use format=access, not namespace. access gives an append-only log; namespace keeps one upserted row per object key, which is useless for "what was read".

queue_dir must be set, so a database blip spools to local disk rather than dropping events silently.

The event table needs a retention policy. An append-only table with no cleanup is the thing most likely to break this. blobtier reduces roughly 50,000 raw events into a couple of state rows, so the raw table can be truncated aggressively once both consumers have passed a point.

Failure modes

symptom likely cause
nothing ever gets archived principal filtering not configured, so the tiering job's own reads keep everything looking hot
a store is never archived it has no manifest; run blobmap scan to find stores marked NEW
a new variable stays hot forever repartition never ran; the metadata-write event was missed or the store is never quiet
open_zarr hangs a metadata object or coordinate got archived; this should be impossible, check hot_always in the manifest
restores are slow and frequent blobs are too small, or the cut does not match how people read
event table growing without bound no retention policy
scan --partition appears to hang on one store it is walking millions of chunk objects. Run with -v to see progress every 100k objects. A HEALPix store at zoom 9 legitimately takes minutes
stale chunks ... will skew sizing in the log objects exist outside the declared chunk grid, left over from an append or rechunk. The width was chosen from a wrong average; clean up or repartition with --force

Recovering

Manifest bucket lost. Re-run blobmap scan --partition over everything. The cuts will be recomputed, but blob ids may differ from the originals, so blobtier's state must be rebuilt too. Worth backing up, since it is small.

State table lost. Everything looks cold. Data is still readable, just slowly, and it re-warms as people use it. This degrades rather than breaks.

A manifest is wrong. blobmap partition <scope> --force recomputes it. This is the only operation that can orphan a tape copy, and it says what it moved.

Sizing

Two numbers drive everything, and both come from the tape system rather than from blobmap:

  • mount plus positioning time. Sets the floor. Below roughly 10 GB, a restore is mostly overhead.
  • acceptable restore latency. Sets the ceiling. A 100 GB blob at typical drive speeds is a few minutes; a 1 TB blob is not.