docs.fermihdi.io — filter_node.md

/ › hd › 2.0.4 › md

Download filter_node.mdView raw

The BloomRF Filter Node

A storage cluster reads every batch a query's time window names, on every Storage Node, whatever the query's predicates say. The BloomRF filter node is the optional role that changes that: it keeps a small filter per batch and, before the Storage Cluster Controller (SCC) fans a query out, answers one question about it — of these batches, which cannot possibly match? Those batches are never read. Most deployments run without one, and a cluster with no filter node is the normal case rather than a degraded one.

What it is

A Storage Node that writes an index instead of records. The filter node joins the SCC's ingest broadcast exactly as a Storage Node does, computes the same chain hash over the same records, and verifies that hash with the SCC over the same stream. Where a Storage Node's second path writes the records to a device, this one folds them into a per-batch filter and discards them. Its store lives in memory, is marked usable only once the SCC has confirmed the batch's hash, and is bounded: the oldest batches are evicted first, and an evicted batch simply reads as "no filter".

Two structures are built in the same single pass over each batch:

Structure Answers Cost
Zone map — the minimum and maximum of every column equality and ranges on a column whose values are clustered in time, such as a timestamp or a counter a few bytes per column per batch
BloomRF — a presence filter per column, shaped by the column's width equality on any column, and ranges on columns of up to eight bytes; a column of one or two bytes is answered exactly by default 32 bits per record for an eight-byte column, 8 KiB per batch flat for a two-byte one

The zone map is asked first, because it is a handful of comparisons. Only a sub-query the zone map cannot settle reaches the BloomRF. A filter is built for every column unless the node is told which ones, and the zone map covers every column regardless.

What it never does

Every client checks. The SDK is the only component that sees both the filter's verdict and the records that arrive, so it compares them on every query: a batch the filter said could not match that nonetheless delivers a record is logged as an error the moment the query completes. A range filter is allowed no false negatives at all — one is a defect to report, not a rate to tolerate.

The three modes

The SCC's trust in the filter is a setting of the whole HD instance, pushed by the DMS to every SCC at once, so a cluster moves between modes in one action:

Mode The SCC Use it
off never consults the filter to take a filter node out of the query path without stopping it
passive consults the filter, records what it would exclude, and reads every batch anyway to measure a new filter node against real queries before trusting it
active consults the filter and leaves the excluded batches unread in operation

Once a filter node is provisioned for a cluster, the DMS pushes active unless the deployment names a mode; passive and off are what an operator chooses deliberately, on the Deployments page of the WebUX, which pushes the choice to every SCC of the instance at once. In every mode a lookup that fails or exceeds its time budget (10 ms by default) excludes nothing and the query proceeds unfiltered.

The mode in force is visible on every query. The Proxy's manifest carries three batch lists beside the found and missing ones:

Field Meaning
excluded_batches batches that exist and were proven unable to match; nothing will be streamed for them
would_exclude_batches in passive mode, what the filter would have excluded; the batches are read anyway
bloomrf_mode off, passive, active — or mixed while a mode change is still rolling across the SCCs

A client on the current SDK drops excluded batches from its wait. An older client that does not know the field waits for a batch nothing will stream and hangs to its timeout, which is the one reason to upgrade clients before switching a cluster to active.

Provisioning and placement

The filter node is provisioned in the DMS WebUX like a Storage Node — Provision New Node, type BloomRF Filter Node, the instance, and the storage cluster whose SCC it serves — and draws on the licence's HD_BLOOMRF element or on the HD_ANY pool. It registers with its own bootstrap token, like every node, and receives its runtime configuration from the DMS: record size, the hash election, the SCC's address and the dataset schema. A schema change discards every filter and they rebuild from the stream.

Two rules follow from how the SCC learns about it:

The installer knows the role as bloomrf: the interview offers it, the validator sizes it, and Ansible and both Helm charts run it. The steps around it — provisioning, the token, the mode — are in the Administrator's Guide, and the WebUX pages that do them in the WebUX walk-through.

Sizing

The filter node is an engine role: two cores (one for the ingest broadcast, one for the control plane), 300 × 2 MB hugepages for a 512 MB engine arena, no 1 GB pages and no devices. POSIX networking is enough for it; DPDK is optional as on every engine role.

Memory is the real cost, and it follows the data. A filter is built at a fixed size and folded at batch close to records × bits per key × levels bits, so it never needs the batch size in advance and the stored size follows the batch. Budget the store as retained batches × bytes per batch, where the node's metrics report the bytes held and its data-plane log records filter_bytes per batch. At the defaults an eight-byte column costs 32 bits per record, a two-byte column 8 KiB per batch flat, and a sixteen-byte column 8 bits per record. The store keeps 512 batches by default.

Variable Default
FERMIHDI_DMS_URL, FERMIHDI_NODE_NAME, FERMIHDI_BOOTSTRAP_TOKEN required as every node
FERMIHDI_PRODUCTION_PORT 6987 the ingest broadcast; the SCC's question is answered on this port plus ten, 6997
FERMIHDI_OBSERVABILITY_PORT 8080 /healthz, /readyz, /metrics
FERMIHDI_SEASTAR_MEMORY 512M the engine arena
FERMIHDI_BLOOMRF_RETAIN_BATCHES 512 the store's bound; oldest evicted first
FERMIHDI_BLOOMRF_COLUMNS all comma-separated column names to build a BloomRF for; the zone map covers every column regardless
FERMIHDI_BLOOMRF_BITS_PER_KEY 8 Bloom bits per record per level, after folding
FERMIHDI_BLOOMRF_LEVELS 4 prefix levels for columns of up to eight bytes; 1 answers equality only
FERMIHDI_BLOOMRF_MAX_PROBES 256 the budget for decomposing a range; a range past it gets "no opinion"
FERMIHDI_BLOOMRF_MAX_KIB_PER_COLUMN 1024 build-time allocation per column, folded down at close

On the SCC, FERMIHDI_BLOOMRF_MODE is the bootstrap value the DMS overrides, and FERMIHDI_BLOOMRF_TIMEOUT_MS (10) is the lookup budget.

What to watch

The node serves Prometheus metrics on its observability port. The ones that say whether it is doing its job:

Metric Means
fermihdi_bloomrf_filters_verified / _pending / _rejected filters the SCC has confirmed, is still to confirm, or refused. Pending that never drains means the verification stream to the SCC is not up
fermihdi_bloomrf_filters_bytes the store's size — the figure to budget the memory limit from
fermihdi_bloomrf_filters_evicted batches dropped for space. A steady climb means the retained-batch bound is smaller than the windows queries ask about
fermihdi_bloomrf_lookups, _lookup_candidates, _lookup_evaluated, _lookup_excluded how often the SCC asks, how many batches it asks about, how many had a usable filter, how many were excluded — selectivity is excluded over candidates
fermihdi_bloomrf_lookup_excluded_zone_map / _excluded_bloomrf which structure decided each exclusion
fermihdi_bloomrf_lookup_no_filter candidates with no usable filter — evicted, unverified, or from before the node started
fermihdi_bloomrf_verify_mismatch a hash the SCC disagreed with. Should stay at zero

The SCC logs one bloomrf_lookup line per query with the same figures on its side — mode, candidates, evaluated, no_filter, would_exclude, protocol_error, lookup_ms — which is where to look first when a filter seems to do nothing: no_filter equal to candidates means the node has no verified filters for the window, and mode: off means the SCC was never told about the node.

A filter that excludes nothing on a workload is not necessarily broken. Predicates on the batch-id column become the time window rather than a filter, != proves nothing, a range wider than the probe budget gets no opinion, and a value that is genuinely present in every batch is genuinely present. The filter earns its keep on selective predicates — an address, a port, a small value on a wide column — over windows of many batches.

Download filter_node.mdView raw