Notice
This document is for a development version of Ceph.
EC Support for Sparse Reads and Logical Allocation - Design
1. Overview
This document describes the design for supporting reliable sparse reads on Erasure Coded (EC) pools. The solution enables sparse read operations to accurately report allocation state and avoid transferring data for unallocated regions. This also provides guaranteed logical allocation preservation for use cases that require it (such as encryption).
2. Terminology
Stripe
A chunk-aligned cross-section of the object across all shards. A stripe spans
chunk_size × k bytes of object data (where k is the number of data
shards). It is the unit of EC encoding and decoding.
Slice
A cross-section of the object at the same LBA range across all shards, at any
alignment. A stripe is a special case of a slice that is aligned to
chunk_size. In this design, slices are always aligned to EC_ALIGN_SIZE
(4K). A single stripe contains chunk_size / EC_ALIGN_SIZE slices.
3. Current State
EC pools currently have the following limitations:
Sparse Reads
EC treats all reads as fully allocated, transferring data for the entire requested range regardless of actual allocation state
MAPEXT Operation
MAPEXT is not supported on EC pools (returns
-EOPNOTSUPP)
ZERO Operation
CEPH_OSD_OP_ZEROis intended to deallocate storage (punch a hole), but the current EC implementation instead writes a buffer of literal zeros through the normal encode-and-write path. The deallocation never reaches the object store, so no storage is reclaimed and sparse reads cannot detect the hole.
Zero-Block Handling During Recovery
During recovery and reconstruction, EC pools cannot distinguish between:
Zeros that were explicitly written by a client
Regions that were never allocated
Note: This is a fundamental property of erasure coding mathematics, not an implementation shortcoming. When data is reconstructed from parity chunks, the erasure coding algorithm can only regenerate the data values: it has no information about whether a block of zeros was explicitly written or never allocated. Additional metadata is required to preserve this distinction.
EC recovery currently:
Reads available shards
Reconstructs missing data
Detects blocks of zeros in reconstructed data
Marks zero blocks as unallocated to save space
This means:
A client that writes zeros may later find those regions reported as unallocated during sparse reads
Allocation state is not preserved across recovery/reconstruction
Clients cannot rely on sparse reads returning accurate allocation information
Parity Deallocation During Recovery
A further consequence of zero-detect during recovery is that parity shards may be deallocated when they would not otherwise be. When all data shards in a slice contain zeros (whether explicitly written or force-allocated), the reconstructed parity for that slice is also all zeros. The zero-detect rule then deallocates the parity extents — even when the data content is unchanged.
Impact
These limitations prevent use cases that require guaranteed allocation preservation, such as encryption (where data may encrypt to all zeros) and other applications that depend on accurate logical allocation state.
4. Problem Statement
The requirement is to provide:
Sparse Read Support: Enable EC pools to accurately report allocation state during sparse reads by querying BlueStore and correctly merging results across shards. This is supported unconditionally: zero-detect does not need to be enabled.
Logical Allocation Preservation: Track which 4K-aligned blocks contain explicitly written zeros, ensuring they remain allocated through recovery and reconstruction. This requires zero-detect to be enabled.
The core technical challenge is that EC pools maintain allocation information at 4K resolution during normal operations, but this information is lost during recovery/reconstruction when data is regenerated from coding parities.
Without these mechanisms, clients that rely on accurate logical allocation information cannot use EC pools reliably.
Primary Use Case: Encryption
The primary use case for this feature is encrypted data (RBD with LUKS, CephFS with fscrypt). For encrypted workloads, the probability of a 4K block encrypting to all zeros is pathologically low (theoretically 1 in 2^(4096*8)). This design optimizes for this property, assuming that genuine blocks of zeros are extremely rare, allowing for efficient metadata storage and fast zero detection with early exit.
5. Proposed Solution
The solution tracks which 4K-aligned blocks contain all zeros when written,
stores this information in object metadata (force_allocated_extents), and
uses it during recovery/reconstruction to preserve the distinction between
allocated-zeros and unallocated regions.
Key Design Decisions:
Zero detection in PrimaryLogPG: Zero detection is performed in
PrimaryLogPGbefore the operation is dispatched to the backend. This keeps the logic common to both EC and replicated pools. For fast EC pools, the results are stored inforce_allocated_extentsand used during recovery. For replicated pools,force_allocated_extentsis not used; however, zero detection results may be useful to replicated pool applications in future (for example, to enable efficient storage by avoiding writes of known-zero blocks).Two mechanisms to enable zero detection: Zero detection can be enabled by either of two independent mechanisms:
Pool property (
preserve_allocation): enables zero detection for every write to every object in the pool. Simple to deploy — no client changes required — but applies unconditionally to all workloads on that pool, including unencrypted ones. In a mixed pool where only a subset of images are encrypted, all writes pay the zero-detection cost and all objects accumulateforce_allocated_extentsmetadata, regardless of whether they need it. In the worst case (large objects with highly fragmented zero patterns), the per-object OI overhead could be significant.MOSDOp flag (
CEPH_OSD_FLAG_PRESERVE_ALLOCATION): a flag carried on the outerMOSDOpmessage, set by the client on a per-request basis. This allows clients that know their data requires zero tracking (e.g. RBD with encryption enabled) to opt in on a per-I/O basis, without affecting unencrypted images sharing the same pool. Requires client-side support to set the flag; older clients that do not set it receive no tracking.
Both mechanisms are supported. Zero detection is performed in PrimaryLogPG whenever either the pool flag is set or the MOSDOp flag is present on the incoming request.
4K block granularity: Aligned with EC operations, LUKS2, and fscrypt block sizes.
``force_allocated_extents`` is fast EC only: The metadata stored in
force_allocated_extentshas no utility for replicated pools. Replicated pools maintain correct allocation state through BlueStore directly and do not lose it during recovery.
6. Metadata Storage
Object Info Extension
Force-allocated extents are stored in object_info_t as a
force_allocated_extents_t, a custom type that holds an adaptive
representation (interval set or bitmap — see section 6.1) with deferred
decode semantics (see section 6.2):
struct object_info_t {
// ... existing fields ...
force_allocated_extents_t force_allocated_extents;
// ... existing fields ...
};
Note: force_allocated_extents is only populated and consulted for fast
EC pools. It is never set or read for replicated pools.
6.1. Adaptive Representation
The representation used for force_allocated_extents is chosen dynamically
on each encode: whichever of the two representations is smaller is used. No
fixed threshold or hysteresis is applied — the decision is made purely on
encoded size at the time of writing. Both directions of conversion must be
supported (interval_set → bitmap and bitmap → interval_set) since the optimal
choice can change as extents are added or removed.
interval_set<uint32_t> (sparse representation)
Each interval stores a start block index and a length in units of 4K blocks, using 32-bit values. Encoded size is proportional to the number of intervals. This is the smaller representation when extents are few and clustered.
Bitmap (dense representation)
A bit-count field is stored at the beginning of the bitmap, followed by one
bit per 4K block of the object. Bit n is set if block n is
force-allocated. The bitmap size is fixed by the object size rounded up to a
4K boundary, regardless of how many bits are set. This is the smaller
representation when extents are numerous and fragmented.
The leading bit-count field allows the decoder to distinguish a bitmap from an interval_set encoding and to allocate the correct buffer without needing a separate tag byte.
Storage Characteristics
For encrypted workloads, the probability of a 4K block encrypting to all zeros is pathologically low (theoretically 1 in 2^(4096×8)), so the interval_set will almost always be empty and the bitmap will never be chosen.
For other workloads, force-allocated extents are relatively rare; the interval_set will be smaller in almost all realistic cases.
The bitmap is a safety net for adversarial or highly fragmented patterns. The dynamic selection means there is no sharp cliff — as fragmentation increases, the representation transitions smoothly to bitmap at exactly the point it becomes beneficial.
OBC Cache Memory Requirement
object_info_t is cached in the Object Context (OBC) cache in memory for
each open object. The encoded force_allocated_extents field contributes to
the in-memory size of every cached OBC. The representation must therefore
remain small: an unbounded or linearly-growing structure would cause memory
pressure proportional to the number of concurrently cached objects. The bitmap
representation imposes a hard upper bound of object_size / 4K / 8 bytes
per object (e.g. 512 bytes for a 4 MiB RBD object), which is acceptable. The
adaptive selection between interval_set and bitmap ensures this bound is
respected regardless of access pattern.
6.2. Deferred Decode and Zero-Copy Re-encode
force_allocated_extents_t uses a lazy decode strategy to avoid paying
decode and re-encode costs on operations that never touch the FAE field.
Storage layout
The object holds two representations simultaneously:
raw_— abufferlistcontaining the encoded bytes exactly as they were read from the OI xattr. This is the authoritative copy while the field has not been accessed.bitmaps_— the live in-memorymap<uint64_t, fae_bitmap_t>used for all queries and mutations. This is populated on first access and becomes authoritative after any mutation.dirty_— a flag that tracks which representation is authoritative. Whendirty_isfalseandraw_is non-empty, the encoded bytes inraw_are authoritative andbitmaps_has not yet been populated. Oncematerialise()runs (populatingbitmaps_fromraw_), or once any mutation is applied,dirty_is set totrueto indicate thatbitmaps_is now authoritative.
Decode path (materialise)
On first access to any query or mutation method, materialise() is called.
If dirty_ is already true, bitmaps_ is already authoritative and
the call returns immediately with no work done. Otherwise it parses raw_
into bitmaps_, clears raw_, and sets dirty_ = true.
OI decode fast path
When object_info_t is decoded from an xattr, the FAE field bytes are
consumed from the stream and stored in raw_ verbatim. bitmaps_ is left
empty and dirty_ is set to false. No bitmap parsing takes place at
this point. Operations that read or modify other OI fields (e.g. size, mtime)
pay zero cost for the FAE field.
Encode fast path
When object_info_t is serialised back to an xattr:
If
dirty_isfalseandraw_is non-empty, the original bytes are appended directly to the outputbufferlistwith no re-encoding. This path is taken whenever the FAE field was decoded from storage but never modified.If
dirty_istrue, the livebitmaps_are encoded intoraw_and then appended. The result is cached inraw_so that subsequent encodes of the same object also take the fast path.
The net effect is that the common case — an OI read followed by a write that
does not touch FAE — touches the FAE bytes only twice: once to copy them into
raw_ on decode, and once to copy them back out on encode.
7. Recovery Rules
During EC recovery and backfill, the following rules apply:
If a block is force-allocated according to OI:
If data has been recovered for the shard, it will be written as-is (no zero-detect)
If no data has been recovered, a zero block will be written
Otherwise (block not in force_allocated_extents):
If data has been recovered for the shard:
If the recovered block is all zeros, deallocate it
Otherwise write it as-is
If no data has been recovered, deallocate (do not write a zero block)
Parity shards:
Zero-detection is applied to parity shards in the same way as data shards. If a parity shard extent is all zeros after encoding or reconstruction, it is deallocated (left as a hole).
Zero-detection on parity is the responsibility of the EC plugin, which may optimise the detection (for example by determining during encoding that parity is zero without a separate pass). Early implementations may perform zero-detection on the encoded parity output after the fact.
This deallocation of parity means that thick provisioning (
rbd --thick-provision) is not supported on EC pools: a subsequent ZERO op against a thick-provisioned image will reclaim the reserved parity storage. See section 11.
Code Locations
The recovery rules are implemented in:
src/osd/ECCommon.cc— recovery push path (ec_sparse_finish_read, FAE consultation during reconstruction)src/osd/PrimaryLogPG.cc— SPARSE_READ and MAPEXT dispatch
7.1. Whiteout Objects
Whiteout objects are cache-tier markers created when an object is deleted in a cache tier. They have no real data content.
Recovery path
force_allocated_extents shall be ignored for whiteout objects during
recovery. Consulting FAE on a whiteout would cause spurious zero blocks to be
pushed to shards where no data should exist.
Delete / evict path
When an object transitions to a whiteout (on delete or evict), any existing
force_allocated_extents on that object shall be discarded. This ensures
that if the object is later promoted from the backing tier it starts with a
clean FAE state.
8. Modified RADOS Operations
8.1. SPARSE_READ
CEPH_OSD_OP_SPARSE_READ
SPARSE_READ is the indirect way to query allocation state - it returns both data and an extent map showing which regions contained data.
Current Behavior (EC):
Legacy EC reads: treat all reads as fully allocated, transfer entire requested range
EC Direct reads: provide limited sparse read support on good path only
During recovery/reconstruction: may incorrectly report written-zeros as unallocated
New Behavior:
SPARSE_READ on EC pools has two code paths depending on the release gate and pool type:
Legacy / pre-Vampire fallback:
When require_osd_release has not yet reached Vampire, or when the pool
does not have EC optimisations enabled (Classic EC), objects_sparse_read_async
returns -EOPNOTSUPP and SPARSE_READ falls back to issuing a normal full
async read. The result is wrapped in a synthetic extent map that reports the
entire requested range as a single allocated extent.
This fallback does not violate any allocation-accuracy promise: the Vampire
release gate and the preserve_allocation / CEPH_OSD_FLAG_PRESERVE_ALLOCATION
tracking mechanisms are both inactive on these paths, so no FAE metadata has
been accumulated and no accuracy guarantee has been made to the client.
Good Path (No Recovery):
Query BlueStore for physical allocation on each shard
Merge allocation results across shards
Return allocation state directly from BlueStore
No need to consult
force_allocated_extents— BlueStore already maintains correct allocation state
Recovery/Reconstruction Path:
When one or more shards must be recovered via erasure decoding, the sparse-read path cannot query BlueStore directly for extent state — the physical allocation on individual shards reflects shard-level storage, not the logical object. Instead a two-stage approach is used:
Stage 1 — Uniform read and decode
Issue a uniform read across all healthy data shards covering the requested range.
Decode the missing shards using the erasure code.
Stage 2 — FAE-guided rescan
For each decoded shard, scan the recovered data block by block:
A block that contains non-zero data is allocated.
A block that contains only zeros is allocated only if it is covered by the shard-projected
force_allocated_extents; otherwise it is an unallocated hole.
The shard-projected FAE is derived by mapping the logical-object FAE through the EC stripe geometry to obtain per-shard block indices.
The resulting per-shard extent maps are merged to produce the logical extent map returned to the client.
Note: The force_allocated_extents metadata is only required during
recovery and reconstruction operations. During normal operations, BlueStore
maintains allocation state correctly, so sparse reads can query BlueStore
directly without consulting the force-allocated extent map.
Note on Direct Reads: Direct EC reads (using
CEPH_OSD_FLAG_EC_DIRECT_READ) already correctly handle allocation
information by collecting and merging results from each shard. This existing
behavior does not require modification.
8.2. MAPEXT
CEPH_OSD_OP_MAPEXT
MAPEXT is the direct way to query logical extent state, similar to the POSIX
fiemap() system call. It returns a map showing which byte ranges of an
object are allocated versus unallocated (holes).
Current Behavior:
Replicated pools: Calls
store->fiemap()to query BlueStore and returns the extent mapEC pools: Returns
-EOPNOTSUPP(Operation Not Supported) - MAPEXT is completely non-functional on EC pools
The lack of MAPEXT support on EC pools is due to:
Objects are split across multiple shards on different OSDs
No mechanism exists to query and merge allocation information across shards
During recovery/reconstruction, allocation information is lost
New Behavior:
MAPEXT is implemented for fast EC pools (those with EC optimisations enabled)
following the same rules as SPARSE_READ. Classic EC pools (without EC
optimisations) continue to return -EOPNOTSUPP permanently, regardless of
the release gate, because they lack the shard-level fiemap infrastructure
required to construct an accurate extent map.
Good Path:
Query BlueStore allocation state on each shard
Merge results across shards
Return combined extent map
Recovery/Reconstruction Path:
MAPEXT reuses the same two-stage decode+rescan path as SPARSE_READ (see section 8.1), but discards the data buffer after the rescan. Only the derived extent map is returned to the client; no object data is transferred.
8.3. WRITE
CEPH_OSD_OP_WRITE
Behavior when tracking is enabled:
Check if op needs zero tracking (pool has
preserve_allocationset, orCEPH_OSD_FLAG_PRESERVE_ALLOCATIONis set on the MOSDOp)If zero tracking is needed, perform zero-detection on the write data in
PrimaryLogPGbefore dispatching to the backendUpdate object data as normal
For fast EC pools only: add any detected all-zero 4K blocks to
force_allocated_extentsNon-zero data can overlay existing force-allocated extents without updating the extent set (the zeros are now overwritten with non-zero data)
Persist updated OI with object
Rationale for not removing extents on non-zero overwrite:
Two reasons justify leaving stale force_allocated_extents entries in place
when non-zero data is written over a previously force-allocated region:
Overhead: Removing entries on every non-zero overwrite would require intersecting the write range against
force_allocated_extentson the hot write path. Since the entries are harmless once the region contains non-zero data — they will simply cause recovery to write an allocated zero block that will then be immediately overwritten by the real data during normal operation — the cost of maintaining them precisely is not justified.Fragmentation of large zero writes: A thick-provisioned image or a large initialising write may write zeros across the entire object, producing a single large interval in
force_allocated_extents. If subsequent partial overwrites with non-zero data were to carve out sub-ranges from that interval, the result would be a highly fragmented interval_set — potentially pushing the representation toward the bitmap, increasing OI size, and adding encode/decode cost on every subsequent write. Leaving the interval intact avoids this fragmentation entirely.
8.4. WRITEFULL
CEPH_OSD_OP_WRITEFULL
Behavior when tracking is enabled:
For fast EC pools only: clear all existing
force_allocated_extents(since WRITEFULL replaces all object data)Update object data
Perform zero-detection on the new data
For fast EC pools only: set
force_allocated_extentsto cover any all-zero 4K blocks in the writePersist updated OI
8.5. WRITESAME
CEPH_OSD_OP_WRITESAME
Behavior when tracking is enabled:
If writing non-zero data: no action needed on
force_allocated_extentsIf writing zeros and pool is fast EC: add the written extent to
force_allocated_extentsPersist updated OI
8.6. TRUNCATE / TRIMTRUNC
CEPH_OSD_OP_TRUNCATE, CEPH_OSD_OP_TRIMTRUNC
Behavior when tracking is enabled:
For fast EC pools only: remove
force_allocated_extentsentries beyond the new sizePerform truncate operation as normal
Persist updated OI
8.7. ZERO
CEPH_OSD_OP_ZERO
CEPH_OSD_OP_ZERO is semantically a deallocation operation — it punches a
hole in the object, freeing the underlying storage. It is not a write of zeros.
PrimaryLogPG converts a ZERO op at the end of an object to a truncate. The EC layer stripes the remaining ZERO range across the data shards and sends ZERO ops down to the object store.
For parity calculation, zeroed data extents are treated as a write of zeros. Any resulting parity that is all zeros is deallocated using the same mechanism as normal writes.
ZERO ops are permitted at any byte alignment. EC deallocates at 4K granularity. Any sub-4K-aligned portion of the range that cannot be deallocated is written as literal zeros.
ZERO ops clear force_allocated_extents entries for any fully covered
4K-aligned extent.
8.8. COPY_FROM
CEPH_OSD_OP_COPY_FROM
COPY_FROM is a server-side copy operation that copies an object from a source OID (potentially in a different pool) to the destination object.
Behavior when tracking is enabled:
Whether FAE is tracked on the destination is determined entirely by the
destination pool: tracking is active when the destination is a fast EC pool
(allows_ecoptimizations()) and the release gate has been reached. The
source pool type and whether the source object has FAE are irrelevant.
For each chunk of data received during the copy, two paths exist:
Sparse chunk (source provided an extent map): only the non-hole extents are written to the destination; zero-detection runs on those extents only. Holes in the source extent map are preserved as holes on the destination.
Non-sparse chunk (Classic EC source or old peer): a single contiguous write; zero-detection runs on the full chunk buffer.
In both cases the detected zero-block results are accumulated in a temporary
force_allocated_extents on the in-progress copy state.
When the copy completes (finish_copyfrom), the accumulated FAE is installed
on the destination object’s OI. FAE is never inherited from the source object.
Classic EC destination pools do not have FAE set because the
allows_ecoptimizations() check on the destination fails. Cross-pool copies
(e.g. replicated source → fast EC destination) are handled correctly because
the FAE decision is based solely on the destination pool.
This feature is gated on the Vampire release (see section 9).
8.9. Snapshots and Clones
FAE preservation across snapshot and rollback operations
When a selfmanaged snapshot is armed and the head object is written, the OSD
creates a clone of the head. The clone shall inherit the head’s
force_allocated_extents at the moment the snapshot is taken, so that sparse
reads on the snapshot OID return the allocation state that was current at
snapshot time.
Rollback
Rolling back to a snapshot shall restore the force_allocated_extents from
that snapshot onto the head object. After rollback, sparse reads on the head
OID reflect the allocation state captured at snapshot time.
9. Upgrade and Release Compatibility
All new OSD-side behaviour introduced by this design — FAE tracking on writes,
MAPEXT support, SPARSE_READ reconstruction, and FAE handling on copy-from — is
gated behind the Vampire release flag (require_osd_release >= Vampire).
Behaviour before Vampire is reached
OSDs running the new code but operating in a cluster whose
require_osd_releaseis below Vampire behave identically to unmodified OSDs. Zero detection is not performed,force_allocated_extentsis never written to object info, and MAPEXT continues to return-EOPNOTSUPPon EC pools.Pre-Vampire OSDs will therefore never encounter the
force_allocated_extentsfield in an object info record.
Rolling-upgrade safety
During a rolling upgrade from a pre-Vampire release, the cluster passes through
a mixed-version state. Because all new behaviour is disabled until
require_osd_release is advanced by the operator, mixed-version clusters
operate correctly throughout the upgrade:
New OSDs do not write
force_allocated_extentsuntil the gate opens.Old OSDs never see an unknown OI field.
Recovery code on new OSDs checks the release gate before consulting FAE, so no shard will be incorrectly retained or discarded during a mixed-version recovery.
The gate is advanced by the operator with:
ceph osd require-osd-release vampire
after all OSDs have been upgraded to the Vampire release or later.
10. Development Notes
10.1. Enabling Zero Detection
Zero detection in PrimaryLogPG is triggered by either of the two mechanisms described in section 5. Both can be used simultaneously.
Pool property (``preserve_allocation``)
A pool-level boolean property that enables zero detection for all writes to all objects in the pool. This is a coarse-grained mechanism: it applies to every write regardless of which client or image issued it. It is appropriate when all workloads on the pool require zero tracking, or when deploying without client changes.
MOSDOp flag (``CEPH_OSD_FLAG_PRESERVE_ALLOCATION``)
A flag on the outer MOSDOp message, set by the client on individual
requests. PrimaryLogPG checks for this flag in m->get_flags() when
processing a write operation. If present, zero detection is performed for that
request regardless of pool configuration. This is a fine-grained mechanism:
only writes from clients that explicitly set the flag are tracked, avoiding
unnecessary overhead and force_allocated_extents accumulation for
unencrypted workloads sharing the same pool.
RBD implementation requirements
Once sparse read support is implemented, the following RBD client changes are needed:
EC pool validation: When enabling encryption (
rbd encryption format), verify that the EC data pool haspreserve_allocationset. Return a clear error if not.CLI guidance: Update
rbd encryption formatto check pool configuration and provide helpful error messages guiding users to enable the property.
Documentation
Full user-facing documentation of how to enable each mechanism (pool property CLI syntax, librados API for setting the MOSDOp flag, and client-specific guidance for RBD and CephFS) will be provided in a separate documentation update once the feature is implemented.
10.2. Block Size and Zero-Detection Algorithm
The implementation uses 4K block granularity, aligned with EC operations, LUKS2, and fscrypt block sizes.
Zero detection uses the existing mem_is_zero() implementation. Improving
the zero-detection algorithm (e.g. using platform-optimised SIMD routines) is
a separate concern and out of scope for this design.
10.3. Testing Requirements
The primary unit test suite for this feature is
ceph_test_rados_api_sparse_reads_pp, built from
src/test/librados/sparse_reads_cxx.cc. It covers FAE tracking, MAPEXT,
SPARSE_READ, copy-from, snapshots, and the per-request flag across replicated
and fast EC pools. It is run as part of the RADOS API workunit
(qa/workunits/rados/test.sh).
Most tests can be done without encryption by using regular writes of zero data and verifying allocation state is preserved through recovery.
Test Cases:
Basic zero-block preservation:
Write zeros to EC pool object with tracking enabled
Trigger recovery
Verify zeros remain allocated (sparse read reports them as allocated)
Mixed zero and non-zero data:
Write pattern of zeros and non-zero data
Trigger recovery
Verify allocation state matches original pattern
Overwrite scenarios:
Write zeros (become force-allocated)
Overwrite with non-zero data
Trigger recovery
Verify non-zero data is preserved, force-allocated extent removed
WRITEFULL clears tracking:
Write zeros (become force-allocated)
WRITEFULL with non-zero data
Verify force-allocated extents cleared
ZERO operation deallocates storage:
Write non-zero data to a slice-aligned region
Issue ZERO covering the complete region
Verify the region is reported as unallocated (hole) by sparse read without triggering recovery — deallocation must happen immediately
Write non-zero data spanning a partial slice at each edge
Issue ZERO covering a range that leaves partial slices at both ends
Verify data shards for the zeroed portion are deallocated and parity shards are updated (not deallocated) for the partial-slice edges
Write zeros to a region (become force-allocated via tracking)
Issue ZERO on the same region
Verify
force_allocated_extentsentry is removedTrigger recovery and verify region is reported as unallocated
TRUNCATE removes extents:
Write zeros at various offsets
Truncate to smaller size
Verify force-allocated extents beyond truncate point removed
MAPEXT returns correct allocation:
Write pattern of zeros and non-zero data
Query with MAPEXT
Verify extent map matches write pattern
SPARSE_READ returns correct allocation:
Write pattern of zeros and non-zero data
Read with SPARSE_READ
Verify extent map matches write pattern
Recovery path sparse reads:
Write zeros
Trigger recovery
Perform sparse read during recovery
Verify zeros reported as allocated
Direct reads with force-allocated extents:
Write zeros to object spanning multiple shards
Trigger recovery on subset of shards
Perform direct read
Verify allocation state correctly merged across shards
Performance benchmarks:
Measure write path overhead with tracking enabled
Compare encrypted vs non-encrypted workloads
Verify < 1% impact for encrypted workloads
Two-stage zero detection validation:
Write 4K block with first byte zero, second byte non-zero
Verify block NOT marked as force-allocated (quick check fails)
Write 4K block with first 8 bytes zero, remaining bytes non-zero
Verify block NOT marked as force-allocated (full check fails)
Write 4K block with first 8 bytes zero, remaining bytes zero
Verify block IS marked as force-allocated (both checks pass)
Write 4K block with all bytes zero
Verify block IS marked as force-allocated
Stress tests:
Large objects with various zero-block patterns
Concurrent recovery and writes
Multiple recovery cycles
Verify allocation state remains consistent
ZERO op coverage in ceph_test_rados_io_sequence:
ceph_test_rados_io_sequenceexercises random sequences of RADOS operations against EC and replicated pools and verifies correctness. The ZERO operation must be added to the operation set so that the following edge cases are exercised automatically:ZERO combined with writes in the same op: include ZERO and WRITE sub-ops in the same
MOSDOpto verify correct ordering and that allocation state after the compound op is consistent.Misaligned ZERO: issue ZERO ops whose start and/or end are not 4K-aligned, verifying that sub-4K head and tail regions are written with literal zeros while the 4K-aligned interior is deallocated.
Multiple ZERO ops in one sequence step: issue several ZERO ops covering overlapping or adjacent ranges within a single compound op, verifying that the combined deallocation is handled correctly.
ZERO combined with TRUNCATE: issue a ZERO followed by a TRUNCATE (or vice versa) within the same op sequence step, verifying that
force_allocated_extentsentries and physical allocation are both updated correctly when the two operations interact at range boundaries.
11. Features NOT Implemented
The following features are explicitly out of scope for this design:
Thick Provisioning Support
Thick provisioning (rbd --thick-provision) requires that all storage is
physically allocated upfront and remains allocated even if zeros are written
back into the image. This design does not support that guarantee: ZERO ops
deallocate storage (via zero detection on encoded shard extents), so any
subsequent ZERO issued against a thick-provisioned image would reclaim the
reserved space. Thick provisioning on EC pools is out of scope for this design.
Alternative Block Size Support
Only 4K block size is supported. Legacy 512-byte block sizes (LUKS1) are not supported. Supporting 512-byte blocks would require:
Zero detection at 512-byte granularity
Preventing EC read-modify-write from growing 512-byte allocated blocks to 4K
More complex extent tracking
This can be added in a future iteration if there is demand.
12. Optional Performance Improvement: ISA-L Zero Detection
mem_is_zero() currently uses a portable C implementation with compile-time
platform selection. An optional performance improvement is to replace it with
calls into the ISA-L isal_zero_detect() function, which provides
runtime-dispatched SIMD implementations (AVX-512, AVX2, AVX, SSE on x86-64;
NEON and SVE variants on aarch64).
Pre-check on the first ``uint64_t``
Before calling isal_zero_detect(), test the first 8 bytes of the buffer.
If they are non-zero the block can be rejected immediately without invoking
ISA-L at all. This pre-check is essentially free for two reasons:
The memory access is not wasted: loading the first 8 bytes brings the first cache line into L1, which ISA-L will read anyway for the all-zero case. The pre-check simply makes use of a cache line that was going to be fetched regardless.
It is a correctness requirement for some ISA-L implementations. Certain aarch64 implementations in ISA-L require a minimum buffer size (greater than 64 bytes) and assume the caller will not pass a trivially non-zero buffer. The pre-check on the first
uint64_tacts as a guard that makes it safe to callisal_zero_detect()unconditionally for any buffer that passes the initial test.
For encrypted workloads — the primary use case — the probability of the first 8 bytes being zero is negligible, so almost all blocks are rejected at the pre-check stage with a single comparison.
This improvement will be implemented if performance measurements indicate that
mem_is_zero() is a bottleneck in the zero-detection path. It will be
delivered under an independent PR, separate from the main sparse read
implementation. It applies to all callers of mem_is_zero() throughout
Ceph, not just the zero-detection path introduced here.
13. Rejected Implementation Approaches
13.1. Client-Side Zero Detection
Approach:
Add a new operation CEPH_OSD_OP_WRITE_ZERO_DETECTED where the client
performs zero-detection and passes an interval_set of zero-block extents along
with the write data.
Advantages:
Data is hot in CPU cache immediately after client processing (e.g., after encryption)
Only checks when client determines it’s necessary
Can be optimized per client type
Disadvantages:
Requires new OSD operation
Each client type needs separate implementation (RBD, CephFS, etc.)
Adds complexity to client code
Does not benefit future RADOS clients automatically
Rejection Rationale:
PrimaryLogPG detection provides a centralized implementation that benefits all current and future RADOS clients via a single code path shared between EC and replicated pools. This also positions zero detection as a shared building block for future optimisations that are not specific to EC.
13.2. Full Allocation Tracking
Approach:
Instead of tracking force-allocated extents (regions with explicitly written zeros), track all allocated regions in an interval_set in the OI.
Advantages:
Simpler conceptual model (just track what’s allocated)
No zero-detection overhead
Works equally well for all workload types
Disadvantages:
Larger metadata overhead (interval_set typically non-empty)
Must update interval_set on every write operation
More complex interval_set management (merging, splitting)
Larger OI size for most objects
Rejection Rationale:
The primary use case is encryption where zeros are pathologically rare. Force- allocated extent tracking optimizes for this case with minimal metadata overhead. Zero-detection is highly optimized (ISA-L, early exit) and operates on data already in CPU cache from EC encoding.
Brought to you by the Ceph Foundation
The Ceph Documentation is a community resource funded and hosted by the non-profit Ceph Foundation. If you would like to support this and our other efforts, please consider joining now.