Conceptio › Archive › arXiv CS
arXiv CS open access

eBPF-Driven Adaptive Lifetime-Aware Data Placement for Shared FDP SSDs

Hajin Moon et al. · arxiv_cs
arXiv CS · Papers
Open Source ↗Direct PDF ↓
kerneloperating-systemsvirtualization
NVMe Flexible Data Placement (FDP) reduces SSD write amplification by separating data with different lifetimes to minimize garbage collection (GC) copying. However, on shared servers, multiple tenants generate more lifetime classes than the available placement handles. Existing approaches rely on st
Conceptio Open Archive

Analyze & explore related research in Conceptio

Connect this record directly into your research workflow or AI agent. Explore citations across open repositories, query full text, and export structured proof bundles.

eBPF-Driven Adaptive Lifetime-Aware Data Placement for Shared FDP SSDs Hajin Moon1 , Taewan Noh1 , Chaeeun Yoon1 , Jeonghun Lee1 , Junyoung Han2 , Joo-Young Hwang2 , Youngjae Kim1

arXiv:2611.06461v1 [cs.OS] 4 Nov 2026

1 Sogang University, Seoul, Korea

Abstract—NVMe Flexible Data Placement (FDP) reduces SSD write amplification by separating data with different lifetimes to minimize garbage collection (GC) copying. However, on shared servers, multiple tenants generate more lifetime classes than the available placement handles. Existing approaches rely on static mappings or application-specific knowledge, failing to capture runtime lifetimes and adapt to changing workloads. The challenge is to identify which data die together and determine which classes should be separated to maximize GC savings under a limited handle budget. We present eFDP, an eBPF-based system that automatically learns data lifetimes and manages FDP placement for unmodified applications on a stock Linux kernel. By tracking writes and invalidations, eFDP identifies lifetime classes and estimates the GC copying saved by separating them. It first assigns each tenant a handle, then allocates the remaining handles to the separations offering the greatest savings. It also re-plans placement when active tenants change. With four database tenants on an emulated FDP SSD, eFDP eliminates twice as much excess write amplification as tenant-level isolation and improves every writing tenant’s throughput by 19–23%. On an enterprise FDP SSD, given RocksDB’s levels, it matches a manually optimized placement using three handles instead of five. After a tenant switch, eFDP re-plans within seven minutes and maintains 25% lower write amplification than the original layout even after an hour. Index Terms—Flexible Data Placement, NVMe, eBPF, write amplification, multi-tenant storage

I. I NTRODUCTION How an SSD groups data decides how much garbage collection (GC) copies. Flash cannot be overwritten in place, so an SSD writes every update to a fresh page and leaves the old copy stale, or dead. GC reclaims dead pages only by erasing a whole block, after copying out every page still alive [1]. These copies raise the write amplification factor (WAF), the flash writes per host write [2], [3], and take flash time from every application on the SSD [4], [5]. Grouping data that die together lets their blocks empty on their own [6]. NVMe Flexible Data Placement (FDP), part of the standard SSD interface, hands this grouping to the host, but only through a few handles, write destinations the SSD keeps open [7]. Each write names a handle, and writes to different handles land in different blocks [8], [9]. The devices in prior FDP studies [10], [11] and ours offer only eight at most. With more kinds of data than handles, some must share one, and FDP pays off only when the data sharing a handle also die together. The question is therefore who decides which data share each handle, and with what knowledge. Corresponding author.

2 Samsung Electronics Co.

Motivation. Today, a person or a fixed rule decides in advance by drawing a map of which data share a handle: manual assignment. Its limits show most clearly on a shared node, a server where an operator runs several tenants, each an application with its files, on one SSD without changing their code. There, each existing route covers only part of the decision. A developer can build a map into one application, as the RocksDB key-value store does. However, on Linux’s common file systems, ext4 and XFS, that map reaches the SSD only through raw device commands that bypass the page cache, the kernel’s in-memory copy of file data. An operator can give each tenant a handle of its own, but only in a patched kernel and from a map written by hand [11]. A flash file system such as F2FS, in turn, applies one fixed rule to all tenants, whatever their data’s lifetimes [12]. Section III measures three reasons these routes fall short. First, a lifetime-aware map, which groups data by how long they live, needs a costly study. A lifetime-blind map, drawn without one, gains little and can even raise WAF. Delivering the map from user space also costs the page cache, which made RocksDB’s reads 8× slower. Second, lifetimes also differ inside a tenant, so one handle per tenant cannot separate them. Third, even a lifetime-aware map goes stale once the writing tenants change. Placement on a shared node is therefore not simply labeling data hot or cold, but an allocation problem. The tenants’ data fall into more lifetime classes, groups of writes that die at about the same time, than there are handles. Someone must decide which get their own handle. That decision needs two facts that exist only at run time: which data die together, and how much GC copying each separation saves on this device, next to these tenants. No person sees both: a developer sees one application, an operator only whole tenants. Our Insight. The kernel already sees what both facts rest on, because every write and every death passes through it. Data die when they are overwritten or discarded, when the file system tells the SSD they were deleted. By charging each death to the file, thread, and tenant that wrote the data, the kernel can learn which data die together. The share of each class still alive when GC reclaims its block then estimates how much separating that class would save. Extended Berkeley Packet Filter (eBPF), small verified programs run inside an unmodified kernel, lets us observe and attribute these deaths. Our Solution. Building on this insight, we present eFDP

Application

Host NVMe write cmd 1 Command Dword 13 RGID: 1

PHNDL: 1

SSD SSD Controller

RUH

0

3

1

2

2

1

2

RUH 0

RU

RU

RU

RUH 1

RU

RU

RU

RUH 2

RU

❹ RU

RU

Page Cache

...

passthrough

writeback thread

direct I/O

Block Layer

eBPF

...

PHNDL

File System

NAND Array

PHNDL → RUH mapping table

RG 0

RG 1 3

(a) FDP inside the SSD

RG 2

NVMe Driver FDP SSD

(b) The Linux write path

Fig. 1: How a write’s handle is set inside the SSD (a), and where on the Linux write path (b). (a) The PID holds a placement handle (PHNDL) and a reclaim group (RGID) (❶). The controller maps PHNDL to a reclaim unit handle (RUH), our handle (❷), and RGID to a group (❸). The data land in that handle’s open RU (❹). (b) The routes that can set a write’s stream today (gray), and where eFDP sets it (blue). (Figure 5), which performs the allocation for unmodified applications on a stock Linux kernel, neither patched nor rebuilt. It only loads a small module. To our knowledge, it is the first FDP placement policy that dynamically learns lifetime classes from the deaths an unmodified kernel observes. Its three components answer those three reasons in turn: ① The eFDP Monitor replaces the lifetime study (Section IV-B). Its eBPF programs label each write by file, thread, and, in a key-value store, the merges its data passed through. They observe when the write dies, and set its handle on the normal I/O path, keeping the page cache. ② The eFDP Planner turns these measurements into a layout, which classes share which handle (Section IV-C). It gives each tenant a handle first and spends the remaining handles where separation saves the most GC copying. ③ The eFDP Loader keeps the layout current (Section IV-D). It installs each layout so that no write ever sees half of one, and plans again when the set of tenants that write changes. Experiments on an FEMU-emulated [13] and an enterprise FDP SSD confirm all three answers. First, labels inferred from kernel events alone did as well as exact labels from a patched RocksDB. Second, with four databases, eFDP removed 45% of the excess WAF, the extra writes GC adds, twice the 22% that isolation, one handle per tenant, remove. Every writing tenant also ran 19–23% faster than under isolation. The same number of handles split differently removed only 37%, so where handles go matters as much as how many. Third, on the emulated SSD, after a tenant switch, eFDP installed a new layout within about 7 min with no one redrawing the map. An hour later WAF stayed a quarter below the old layout’s (2.51 vs. 3.35). This costs 8–9% more CPU per small write, slowing only CPU-bound writers. II. FDP AND L INUX K ERNEL M ECHANISMS A. NVMe Flexible Data Placement FDP [8], [9], [14], [15] lets the host choose which writes share flash, while the SSD still runs GC. The SSD erases flash in groups of erase blocks called reclaim units (RUs). It keeps one RU open for each reclaim unit handle (RUH), which we call a handle. A write names this handle through the placement identifier (PID) in its command. From this PID,

the SSD then maps its placement handle (PHNDL) to one RUH, writing into the corresponding RU (Fig. 1(a)). Writes with different handles thus never share an RU. Separating writes helps because, before it erases an RU, GC must copy every page in it that is still valid, that is, not yet overwritten or deleted. Thus, how much it copies depends on each page’s lifetime, the time from its write until it becomes invalid, compared with the reclaim age Â, the time from a page’s write until GC reclaims its RU. Data that die well before  leave nothing to copy, while data that outlive  must be copied. An RU that mixes the two thus forces GC to copy the long-lived pages. Separating them lets the RUs of the shortlived data empty on their own [3], [16]. Separating data by lifetime, however, is constrained by the number of placement handles. Each handle holds an open RU and consumes device resources such as write-buffer space [17]. Although vendor sources describe up to 128 or 256 handles [9], [17], the devices in published studies and our evaluation expose only 7 or 8 [10], [11]. This constraint becomes more acute when multiple tenants, independently managed workloads, share an SSD. Each tenant may write data with different lifetimes. We call writes from the same tenant that die at similar times a class, and define its survival as the fraction of bytes still valid at reclaim age Â. Separating classes with different survival reduces GC copying, but the number of classes can exceed the available handles. The host must therefore identify lifetime classes and allocate scarce handles to the separations that save the most GC copying. B. Placement in the Linux Write Path Linux provides a mechanism to convey placement decisions to FDP SSDs, but no policy that fits the workload. Since Linux 6.16, each write bio carries a stream identifier (bi_write_stream), which the NVMe driver maps to an FDP PID [18], [19]. However, the kernel neither measures lifetime classes nor allocates the limited streams among tenants. Existing approaches require explicit placement decisions, each at a cost (Fig. 1(b)). Applications can issue NVMe passthrough commands through io_uring, but bypass the page cache [20]. F2FS maps hot, warm, and cold data to three streams by one fixed rule, whereas ext4 and mainline XFS provide no such mapping [12], [21]. Operators can assign

No placement Shipped hints 2.95 2.71 2.02

Lifetime-blind By lifetime 2.06

2.42 1.31

SSD vendor, RocksDB

io.stream, fio

(a) Published WAF

1200 Reads (K/s)

WAF

3.5 3.0 2.5 2.0 1.5 1.0

1,021

800 400 0

127

64

Page TorFS Direct cache I/O

(b) RocksDB reads

Fig. 2: WAF with and without placement by lifetime (a), and RocksDB reads with and without the page cache (b). (a) Published by an SSD vendor [27] and io.stream [11]. (b) Enterprise SSD, 8 GiB working set, 2 GiB block cache. streams to cgroups, but only through kernel modifications, as in io.stream [11]. To avoid these changes while preserving the regular I/O path, placement must be enforced at the block layer. Each write reaches this layer as a bio, where its stream can be set before the NVMe driver constructs the command. However, kernel flushers may submit buffered writes, whose bios no longer say which file or thread produced them, so a policy must capture that identity earlier. C. Extended Berkeley Packet Filter (eBPF) eBPF lets users add small programs to a running kernel without patching or rebooting it. It is widely used to trace the kernel, filter network packets, and enforce security policies [22]. eFDP needs it to observe a write’s identity where the kernel still knows it, carry that identity to the block layer, and set the stream there (Section IV-B1). eBPF programs run at hooks, fixed points in the kernel, such as a file open or a write entering the block layer [22], [23]. They also keep state in key-value tables (eBPF’s maps) that carry what one hook observed to another. For example, the label a file gets at open is looked up when its writes reach the block layer. User programs can read and fill the same tables, so the lifetimes eFDP measures go out to user space and its placement decisions come back in (Section IV-C). This hand-off lets planning, which sorts lifetimes and searches over groupings, run in user space, since the kernel’s verifier rejects at load time any program that could loop without bound [24]. Planning also need not run per write, since a layout changes only when the workload does. A program at a tracing hook may read kernel data, but never change them [25]. As a result, setting the stream needs a kfunc, a kernel function exposed to eBPF [26]. eFDP adds one in a 124-line loadable module. This kfunc only writes the stream of the bio it is given, at a hook that runs before the NVMe driver builds the command (Section IV-B1). None of these pieces requires patching or rebuilding the kernel. III. C HALLENGES IN FDP P LACEMENT Manual FDP placement faces three challenges on shared SSDs, each yielding a requirement: learning lifetimes without losing the page cache (Section III-A), allocating streams to classes finer than a tenant (Section III-B), and adapting as the writing tenants change (Section III-C).

We study these challenges using RocksDB [28], PostgreSQL [29], SQLite [30], and MongoDB [31], each running in its own cgroup and directory on a shared ext4 file system backed by an FDP SSD. Experiments use FEMU [13] or an enterprise FDP drive (Section V-A). Section III-B examines this four-tenant setting, while Sections III-A and III-C use one RocksDB instance and a switch between two instances, respectively. These workloads expose distinct data lifetimes, even within a tenant. RocksDB writes data into sorted SST files through flushes, and its compactions later rewrite them into deeper levels of an LSM tree. Shallow SST files become invalid within seconds, whereas deeper files survive longer. A. Manual Assignments Are Costly Assigning streams by hand cuts WAF but brings two problems: a costly lifetime study to draw the map, and slower reads from bypassing the page cache. a) Costly Lifetime Profiling: The study makes a map lifetime-aware, grouping writes by how long they live relative to the reclaim age. On an SSD vendor’s FDP drive, such a per-level map reduced RocksDB’s WAF about four times as much as its shipped hints, lifetime tags chosen by each file’s level (Fig. 2(a)) [27], [32]. Achieving these gains, however, requires device-specific reclaim ages and application-specific lifetime labels. The vendor compared each level’s lifetime with its own drive’s reclaim age [27], but the reclaim age changes with the device and the load [3], by 14× between our two SSDs. The required labels also come from application internals: our per-level map relied on a patched RocksDB that tags each SST file with its level (Section V-A). Nor can the study be skipped. A map drawn without it is lifetime-blind. It groups writes by a fixed rule or a label easier to know than lifetime, and gains little or even raises WAF. RocksDB’s shipped hints group level 4, which dies near the reclaim age, with level 5, which outlives it. On our enterprise SSD, they left WAF at 2.19, against 1.30 for a per-level map (Section V-B). In io.stream’s own experiment [11], 14 cgroups ran fio over seven data streams. Hashing the cgroups onto streams raised WAF from 2.06 with no placement to 2.42, while grouping them by lifetime lowered it to 1.31 (Fig. 2(a)). Automatic schemes count rewrites per address as a proxy for lifetime [33]–[35]. On ext4, however, the file system reuses an address for other files’ data, so the count follows the address rather than any file’s lifetime. On RocksDB, placing by this count removed far less excess WAF than lifetime classes did (Section V-B). b) Page-Cache Bypass: Reads also slow down because, on ext4 or XFS, an application can apply even a lifetimeaware map only through passthrough, which bypasses the page cache (Section II-B). A read that misses the application’s cache then goes to the device, not the page cache. This detour costs whenever the working set fits in memory but not in that cache. On the enterprise SSD (8 GiB working set; 2 GiB block cache, RocksDB’s read cache), RocksDB read 8× faster through the page cache than through TorFS [36], a passthrough FDP plugin. Direct I/O, which skips only the page cache, was slower still (Fig. 2(b)).

100 75 50 g0–3: 0% 25 0 0.1 1

Â

g4: 37%

10

100 0.1

(a) RocksDB

tables: 96%

Â

WAL: 45%

1 10 100 0.1 Age at death (s) (b) PostgreSQL

Â

DB: 94%

WAL: 0.5%

1

10

100

(c) SQLite

Fig. 3: Lifetimes inside the tenants of the four-tenant shared node: share of bytes still valid at each age, measured by eFDP (emulated SSD, Â = 81.3 s). Labels: survival at Â; gN: RocksDB generation N (Section III-B). Finding 1. A host policy must learn lifetimes on the node, from what the kernel sees, and place writes on the regular I/O path that keeps the page cache (Requirement R1). Lifetime-blind maps gain little or even raise WAF, and delivery from user space turns pagecache hits into device reads.

B. Classes Within Tenants Outnumber Streams Separating writes by lifetime within tenants creates more classes than available streams, and these classes differ in placement benefit. Yet tenant-level placement, as used by io.stream [11], cannot distinguish these classes in the first place. A host policy must therefore identify lifetime classes within tenants and allocate scarce streams to the most beneficial splits across the node. a) Tenant-Level Placement Is Insufficient: io.stream [11] groups writes by cgroup, treating each tenant as a placement unit. However, a tenant can produce data with widely different lifetimes. Assigning one stream per tenant mixes such data, even when their lifetimes warrant separate placement. SQLite and PostgreSQL each write a short-lived log and long-lived data. Both first record changes in a write-ahead log (WAL). At each checkpoint, they apply the logged changes to their data and restart the log. On our shared node, SQLite’s log had a survival of only 0.5%, compared with 94% for its database file. PostgreSQL’s log likewise died much sooner than its tables (Fig. 3(b,c)). File names distinguish these writes, whereas cgroups do not. RocksDB requires finer-grained information. Only a file’s generation, the number of compactions its data has undergone since flush, distinguishes files by lifetime. File names alone cannot, since every level produces the same kind of SST file, and a compaction thread writes both shallow and deep files. Generations 0–3 die within seconds, generation 4 near the reclaim age, and generation 5 long afterward (Fig. 3(a)). Because compaction typically moves data about one level deeper, generation tracks level. eFDP can infer it from the files read by each compaction (Section IV-B1). b) Not All Lifetime Classes Deserve a Stream: Separating writes within tenants creates more lifetime classes than available streams. Our four tenants form eight classes: a log and a data class each for SQLite and PostgreSQL, three for RocksDB, and one for the idle MongoDB. Yet only seven streams are available. More importantly, separating these classes yields different benefits. Compared with tenant isolation (one stream per

1.8 WAF

Still valid (%)

g5: 88%

1.6

Static (A's layout) Two streams per tenant Re-derived

1.4 1.2 1.0

0

1

2 3 Time since switch (h)

4

5

Fig. 4: WAF of three maps after a tenant switch, to show what a stale map costs (enterprise SSD, 20-min windows); the load moves from the first RocksDB (A) to the second. Two streams per tenant: a hand-drawn map giving each RocksDB two of the four streams. The re-derived run starts from the device state the static run left. tenant), separating SQLite’s log saved 0.25 GC copies per page written (WAF−1), separating RocksDB’s generations saved 0.16, and separating PostgreSQL’s log saved only 0.02 (Section V-D). Thus, assigning streams independently within each tenant cannot maximize the benefit of a limited stream budget. The host must prioritize lifetime splits by their expected reduction in GC copying across the node. Finding 2. A host policy must form classes from measured lifetimes, within tenants as well as across them, and allocate scarce streams to the splits that save the most GC copies across the node (Requirement R2). Tenant-level placement mixes data with different lifetimes, while finer-grained classes outnumber available streams and differ in placement benefit.

C. Lifetime-Aware Maps Become Stale Even a lifetime-aware map can become stale when the active writing tenants change. Streams reserved for an idle tenant provide no benefit, while writes from a newly active tenant may mix data with different lifetimes. Thus, a map can become ineffective even when data lifetimes remain unchanged. Consider a workload that switches from one RocksDB instance to another. The original map still assigns most streams to the first instance, leaving the second instance’s shallow and deep files sharing a single stream. On our enterprise SSD, WAF settled at 1.71 after the switch. A static map that reserved two streams for each RocksDB instance offered little improvement (Fig. 4). In contrast, installing a map re-derived offline from the second instance’s levels at the switch reduced WAF by 35%, to 1.11, and improved throughput by 10%. Finding 3. A host policy must re-derive placement mappings on the node when the active writing tenants change (Requirement R3). Static mappings waste streams on idle tenants, while replanning restores the benefits of lifetime-aware placement.

D. Requirements and Existing Systems Our observations establish three requirements for effective FDP placement: transparent lifetime profiling on the regular I/O path (R1), benefit-aware stream allocation across tenants (R2), and replanning as active writing tenants change (R3). No existing FDP system satisfies all three requirements (Table I). Existing approaches neither learn lifetimes on an unmodified software stack nor independently allocate streams

across tenants. eFDP addresses R1–R3 without modifying the kernel source (Section IV). It automatically replans when the active writing tenants change, while a new device or workload change requires an operator-triggered replan. TABLE I: Existing FDP systems checked against an unmodified stack and R1–R3. ✓: yes; △: only via rules, labels or assignments a person supplies; ◦: partly, in some modes or through a lifetime proxy; ✗: no. Stock kernel: no patch or rebuild (eFDP loads a 124-line kfunc module). Unmodified

R1

System

App as is

Stock kernel

Page cache

TorFS [36] CacheLib FDP [10] SlimIO [37] RocksDB hints [27] XFS streams [21] FDPFS [38] io.stream [11] F2FS [12]

✗ ✗ ✗ ◦ ✗ ◦ ✓ ✓

✓ ✓ ✓ ◦ ✗ ✓ ✗ ✓

eFDP

✓

✓

R2

R3

Lifetimes

Divides tenant

Allocates

Adapts

✗ ✗ ✗ ✓ ✓ ◦ ✓ ✓

△ △ △ △ △ △ △ ◦

✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓

✗ △ ✗ ✗ ✗ △ △ ✗

✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗

✓

✓

✓

✓

✓

IV. D ESIGN We first formulate the placement problem and outline eFDP’s feedback loop. We then describe its three components: the eFDP Monitor, which profiles write lifetimes (R1); the eFDP Planner, which allocates limited placement streams (R2); and the eFDP Loader, which installs and updates placement mappings as workloads change (R3). A. Problem Formulation and Overview Fig. 5(a) draws the problem on our shared node, one part per requirement. Each tenant, a cgroup plus a directory, writes data that fall into classes. The file and thread that tell its writes apart, however, do not give those classes or their survival. eFDP must thus combine them with the overwrites and discards it observes (R1). The four tenants form eight classes for seven free streams, so which two share one is an allocation (R2). Here, RocksDB’s generations 4 and 5 (g4, g5) share, because splitting them is worth less than any split that got a stream (Section V-D), although it pays on RocksDB alone (Section V-B). The layout reflects the tenants observed while profiling, so a change of active tenants can leave it unfit and triggers a re-plan (R3). Problem. N tenants share an FDP SSD with B free streams. Tenant i’s writes fall into ki classes. Class c has bc bytes, a fraction sc still P valid at the reclaim age Â. Classes and sc must be learned, and i ki may exceed B. Choose which classes share a stream, never across tenants unless the operator waives it, to minimize the live bytes GC copies. Change no application, file system, or kernel source. Choose again when the writing tenants change.

eFDP solves this problem with a feedback loop (Fig. 5(b)). The placer, an eBPF program in the block layer, sets every write’s stream. During a profiling window, the eFDP Monitor also records when writes die. From this profile of bytes and ages at death (❶), the eFDP Planner derives a layout: file and thread rules plus a class-to-stream table (❷). The eFDP Loader

installs it in seconds (❸), and a check once per reclaim age restarts the loop when the writing tenants change (❹). B. eFDP Monitor The eFDP Monitor must tell the eFDP Planner which kind of write each write is and when it dies (R1). However, at the block layer, where streams are set, a write has lost most of its identity, and its lifetime shows only at death, long after. The eFDP Monitor therefore keeps each write’s identity on three signals the kernel sees, its file pattern, thread name, and generation, and links deaths to writes. It calls a write’s value on a signal its label, e.g., *.sst. We follow one write, W1: RocksDB’s flush thread writing the new SST file 000123.sst. 1) Reconstructing Write Identity: The block layer usually keeps only a write’s tenant, which the placer needs to keep tenants apart. Hence, the other labels must be captured where they are still known and joined at the bio. The bio is usually submitted by a writeback thread, so it carries neither the path nor the writing thread (Section II-B). As the Linux kernel charges memory per page but attributes writeback per file [39], it assigns each file’s inode, the kernel’s record of the file, to the cgroup that modifies most of its cached pages. Every bio that writes the file back carries that cgroup. Since tenants write only their own files, the cgroup names the tenant. The file label is therefore taken at open, the last point that knows the path. It is then stored, so that no write resolves its path again. When a regular file is opened for writing, an eBPF classifier matches its path against the layout’s file patterns, e.g., *.sst for 000123.sst, and records the result for the file. The eFDP Planner makes it a rule only if a pattern’s purity, the share of its profiled bytes that fall in its most common class, is at least 80% (Table II). During profiling, a second program records each path at open, so the eFDP Planner never scans directories. Both programs use under 2% of the verifier’s limit. The eFDP Monitor infers an SST file’s level [28], [40], which neither the path nor the writing thread reveals, from what the writer read. A generation tracker records which SST files each thread opens for reading, and a new file gets one more than the lowest generation its thread read. A flush reads none, so its file gets generation 0. Because a compaction usually writes one level below its shallowest input, generation follows level. At the block layer, the placer uses small, fixed tables to join the tenant from the bio’s cgroup, the file’s labels, and the submitting thread’s name. A buffered write finds the file’s labels through the page being written. Direct I/O has no such page, so a small program at submission records them for the request instead. Writes that kernel writeback threads submit get no thread label. Bounding the tables, at most 32 file-pattern rules, 8 thread rules, 8 classes per signal, and 16 tenants, lets the verifier accept the loops over them, and every layout in Section V fits. With these labels, W1 reaches its handle on the normal I/O path. As RocksDB writes SST files with direct I/O or syncs them from the writing thread, W1’s bio comes from the flush

RocksDB classes

g0–3

PostgreSQL

g4

g5

WAL

tables

SQLite WAL

MongoDB DB

User

tenants

data

Unmodified kernel g0–3

g4

2

g5 3

short-lived

WAL

tables

WAL

DB

data

4

5

6

7

8

dies near Â

long-lived

(a) Problem: 8 classes, 7 free streams

2 layout

Planner

3 tables VFS

streams

Loader

block layer

classifier

generation tracker

placer

1 profile

profiling placer 4 drift check

NVMe driver

Device

hook

kfunc module

FDP SSD

Monitor

8 handles

(b) eFDP on an unmodified Linux stack

Fig. 5: Overview of eFDP: the problem it solves (a) and its loop (b). (a) Four tenants’ writes form eight classes for seven free streams, so two classes, here g4 and g5, share one. (b) The loop (shaded) on an unmodified Linux stack: profile (❶), layout (❷), tables (❸), drift check (❹). thread with labels *.sst, rocksdb:high and generation 0. The class-to-stream table then maps it to stream 2. Because the placer’s hook runs before the NVMe command exists (Section II-C), it sets the stream through our kfunc, which skips any bio that already has one. As a result, a stream set by another layer is never overridden. The unmodified NVMe driver then puts stream 2’s PID into the command. 2) Attributing Deaths to Writes: A page dies long after its write, often in another thread, but at the same disk address. The eFDP Monitor uses this to link the two. A lifetime is thus the time from a write the kernel sees to the overwrite or discard of its address. During a profiling window, a profiling placer replaces the placer and stores, for the disk range each write starts in, a 64-byte owner record of its file, thread, tenant, class, and time. A later write or discard over that range means the old bytes died, and they are charged to the recorded owner. When compaction deletes W1’s file, a kernel thread issues the discard, yet the death is charged to 000123.sst and RocksDB’s flush thread. Two guards keep these charges nearly correct and their memory bounded. First, each record notes its file’s creation time, so a reused inode never inherits an old file’s deaths. Second, each owner range, 64 KiB or capacity/222 if larger (Table II), keeps only its latest owner, so the records never exceed 256 MiB. A write that overlaps a record charges its owner’s whole extent. A newer owner in the same range displaces an older one, whose later deaths go uncounted. What the eFDP Monitor hands the eFDP Planner is small: one histogram of age at death per label, whose six bins, with edges at 0.1, 1, 10, 60 and 300 s, bracket the emulated SSD’s reclaim age. The last bin also holds bytes never seen dying, which count as alive. When a window ends, a user-space agent reads the histograms through the bpf() system call. It then hands them, with the paths captured at open, to the eFDP Planner. C. eFDP Planner To meet R2, the eFDP Planner must decide which splits of a tenant’s data save the most GC copying and which get the few streams. Each split is a boundary, dividing one tenant’s labels on one signal into two classes. The profile usually shows more boundaries than the SSD has streams, and the eFDP

Planner gets no help from the device, which never reports what a split would save. Since sorting, floating-point, and openended search are hard in eBPF and unneeded per write, it runs in user space once per layout. This in turn leaves about ten table lookups per write in the kernel. 1) Finding and Valuing Lifetime Boundaries: A boundary helps only where lifetimes cross the reclaim age, because GC copies what is still valid then (Section II-A). The eFDP Planner therefore scores each set of labels X by its survival rate, sX = 1 − dX /bX ,

(1)

the share of the bX bytes written in the window still valid at the scoring age,  by default (Section V-F varies it). The bytes dead by then, dX , come from the age bins.1 It estimates  as capacity over the window’s media write rate, from the mediawritten counter of the device’s FDP statistics log. Unlike median age at death, survival separates a class that dies just before  from one that outlives it (Section V-B). Candidate boundaries lie only between labels that are neighbours in survival (for generations, in number). Each side requires at least 5% of the tenant’s bytes, the minimum side size, as smaller sides save little (Section V-F tests 1–20%). From survival, the eFDP Planner estimates a boundary’s worth, the GC copies it would save, w = bL sL (sL − sS ),

(2)

where L is the longer-lived side and S the shorter-lived one. Intuitively, if both sides share RUs, the shorter-lived side’s deaths make units eligible for GC [3], [41]. The longerlived side’s pages in them are still valid, which GC later copies. bL sL bounds those copies, and the survival gap thus approximates how many units GC picks. The model assumes greedy GC, which reclaims the unit with the fewest valid pages, and a steady load. Section V-D checks its ranking. 2) Allocating the Stream Budget under Tenant Isolation: Algorithm 1 first gives each tenant its own stream, tenantfirst allocation, then spends the remaining streams by worth per added stream, w/κ, within the rule limits. Worth alone 1 Spread log-uniformly within each bin. Assuming a steady write rate, a death at age a in a window of length W is weighted by W/(W −a), because only bytes written in the window’s first W − a could die at that age within it.

Algorithm 1 Tenant-first stream allocation. Input: each tenant’s labels per signal (file, thread, generation), sorted by survival s at  (generations by number); stream budget B Output: file and thread rules; a class-to-stream table per tenant 1: Give each tenant its own stream; if tenants > B, the B − 1 largest by bytes get one each and the rest share one 2: free ← B − streams used in line 1 3: loop 4: Per tenant and signal, find the allowed boundary (≥5% of the tenant’s bytes per side) of highest worth w; κ ← streams it adds 5: Discard boundaries with κ > free or that exceed a rule limit 6: if none remain then break 7: Commit the boundary with the highest w/κ; free ← free − κ 8: Number each tenant’s classes by survival; class 0 (longest-lived) uses the tenant’s own stream and takes writes that match no rule 9: Rules are shared across tenants: if two map one file pattern or thread name to different classes, keep the mapping of the tenant that wrote more bytes under it 10: Output the rules and class-to-stream table; reject the layout if a class can match no write

TABLE II: eFDP’s parameters, their defaults, and how each is set. B and  are measured; the layout does not depend on the minimum side size, purity, or scoring age over wide ranges (Section V-F). Parameter

Default

How it is set

Streams B Reclaim age  Window W Drift check

7 (emulated) 81–96 s; ent. ≈23 min 4Â every Â

Driver’s streams, minus one for unplaced writes Capacity ÷ flash write rate (host writes + GC copies) during profiling Longer windows change no stream assignment A tenant is active if it wrote ≥5% of all tenants’ bytes in the last Â; trigger delay ≤ Â Each side holds ≥5% of the tenant’s bytes A pattern becomes a rule only if ≥80% of its bytes fall in one class max(64 KiB, capacity/222 ), a power of two; one 64-byte record each, ≤256 MiB

Min. side size 5% File-pattern 80% purity Owner range 64 KiB

cannot decide which boundaries get streams, since a write’s class combines its labels on every signal. If RocksDB already has two thread classes, a boundary between its generations makes 2 × 2 = 4 classes and costs κ = 2 streams. Tenant-first is a guarantee the operator chooses. With at most B tenants (beyond that, the overflow tenants share one stream without it; Algorithm 1, line 1), an installed layout never sends two tenants’ writes to one stream, so no RU the host fills mixes tenants. The guarantee stops at the host. Writes share the first handle during a swap (Section IV-D), and the enterprise SSD’s handles are initially isolated, so its GC may combine valid data from different handles. Where streams are scarce, the guarantee costs WAF, e.g., a stream kept for an idle tenant (Section V-D). An operator who puts WAF first can instead cluster the same labels with k-means, ignoring tenants, after PCStream’s clustering [42], though not its program-context extraction.

profiled on (Section III-C), R3 asks for a new one when they change. 1) Installing a Layout Safely: The eFDP Loader installs a layout so that all three safety conditions hold. It rejects any layout that uses more streams than the driver reports or, with at most B tenants, leaves a tenant without its own stream. It then attaches the programs and fills their tables, writing the on/off flag last, so no write sees half a layout. At runtime, a class without a stream falls back to its tenant’s own stream, so no class lands on another tenant’s handle. The classifier misses files opened before loading, so one scan of the tenants’ directories labels them. A re-plan replaces a layout by the same procedure rather than editing it in place, so no write is ever placed by a mix of old and new tables. The eFDP Loader unloads the running programs with their tables, then installs the new layout as above. Writes in between carry no stream and go to the first handle. For these seconds, only the tenants share it, as they would without eFDP, and nothing already written moves. The scan then labels the files that stayed open. Generations live in the tables too, so a swap discards them. The swap keeps the table of generations, so files written before it keep theirs. 2) Re-Planning When Tenants Change: eFDP re-plans only when the set of active tenants changes (R3). On every Â, a drift check reads the per-tenant byte counts that the placer already keeps. It calls a tenant active if it wrote at least 5% of all tenants’ bytes in the last Â. In our switch runs, the new writer’s share rose from 0 to about half, well past this threshold (Section V-E). A new tenant that writes less than 5% is not caught. A load change among the same tenants, or a drift in Â, triggers no re-plan; an operator can start one. A re-plan costs one profiling window W = 4Â, since longer windows changed no stream assignment in our profiles (Section V-F). The eFDP Loader swaps in the profiling placer for W . The eFDP Planner then computes a new layout under the same budget, which the eFDP Loader installs with the normal placer. Writes go unplaced only during the swaps themselves. Because a re-plan needs a new set of active tenants and the next check follows the install, re-plans start at most once every W + Â, even when a tenant hovers at the threshold.

D. eFDP Loader

V. E VALUATION A. Experimental Setup Implementation. eFDP runs on an unmodified Linux kernel. Its eBPF programs attach at kernel function entries and tracepoints, and set streams through our 124-line kfunc module. They take about 1,500 lines of C, and its user-space agent, eFDP Planner, and eFDP Loader about 4,000. The agent and Loader load the programs through libbpf. We evaluate it on Linux 7.2-rc4 with a FEMU-emulated FDP SSD and on Linux 7.0 with a real enterprise FDP SSD (Table III). We evaluate four workloads (Table III):

The eFDP Loader must install each layout safely and keep it matched to the writing tenants. Safety means no write sees a half-installed layout, no layout names more streams than the device has, and scarcity never moves one tenant’s class onto another’s handle. Since a layout fits only the tenants it was

• Single RocksDB: one RocksDB instance. • Shared node: the four databases from Section III, sharing 2–7 streams. • Tenant switch: the write load shifts from one RocksDB instance to another.

Software

Workload

Tenants (runs 1,200 s unless noted)

Fill (%)

B

One RocksDB Shared node Switch

22 M 1 KiB keys, overwrite, one thread; ent. 2.5 G, hours RocksDB 5.6 M keys, PostgreSQL scale 641, SQLite 2.4 M rows overwrite; MongoDB 6.4 M docs idles two RocksDB taking turns, 9.8 M keys each, 20 + 30 or 60 min; ent. 1.25 G, hours shared node at 0.92× size, kernel build, 4 KiB reader

74; ent. 77, F2FS 76 72

3–5

65; ent. 85

4

72

7

Mixed node

2–7

• Mixed node: the shared node with an additional kernelbuild loop and a 4 KiB direct random reader. All datasets are initially loaded without placement. Placement begins with subsequent overwrites. We report fill as used space relative to ext4’s usable capacity, excluding its 5% root reserve. We compare eFDP with no placement (all writes on the first handle) and with each kind of map in §III: • Isolation: one stream per tenant, io.stream’s map. • Developer maps: RocksDB’s shipped hints and an exact per-level map from a RocksDB patched to tag each SST file by level, as in SlimIO. • Automatic maps: AutoStream’s rule [33], re-implemented at the same block-layer hook, which streams each write by its 1 MiB range’s decaying write count (halved every 10 s); and k-means as in PCStream [42], which clusters eFDP’s labels by lifetime alone. • Systems with their own I/O path: TorFS [36] and F2FS’s temperatures [12]. As io.stream demands a patched kernel and SlimIO a patched database [11], [37], we run their maps in eFDP’s placer. We report WAF and the share of the excess WAF that a layout removes, (W0 − W )/(W0 − 1), where W0 is the WAF without placement. Ext4’s journal writes stay on the first handle under every layout, so any placement gains about 0.12 WAF by leaving it. We therefore credit a boundary only against another placed layout. On the emulated SSD, runs were paired and their order randomised. Identical configurations differed by 0.025 WAF, so we call a difference real only when its 95% interval lies beyond ±0.025, and negligible under 0.1 WAF. B. Can Inferred Labels Replace an Application’s Own? Labels inferred from kernel events alone did as well as RocksDB’s exact levels. Given the vendor’s grouping, {0–3}{4}{5} (levels 0–3 together, 4 and 5 apart), eFDP sent nearly the same bytes to each stream as the exact per-level map, and matched its WAF, 2.13 against 2.14 (Fig. 6(a)).

F2FS 3.0 2.03 2.5 2.31 2.19 1.85 2.0 1.30 1.32 1.5 1.0

ac

(a) Emulated SSD

pl Sh acem ip pe ent d hi n Pe T ts r-l orF ev el S m ap e F2 F FS DP te m ps

2.14 2.13

o

2.66 2.43

N

3.27

A em ut en Sh oStr t M ipp eam ed ed ia n- hin Pe age ts r-l ra ev nk el m ap eF D P

FS, WAF

3.57

pl

Host

FEMU 9.0 (QEMU 9.0.1) [13] with NVMe FDP SSD, 3.84 TB; FDP; 32 GiB namespace; 8 handles, 3.2 TiB namespace; 8 handles, seven free; RU 128 MiB (256); 8×8 seven free; RU 12 GiB (270) LUNs, 4 KiB pages, read/prog./erase 40/200 µs/2 ms 8 vCPUs, 15 GiB; KVM, 16-core x86; 8-core x86, 2.9 GHz, 24 GiB; Linux 7.2-rc4, unmodified Linux 7.0 ext4, online discard; flash/host pages, ext4 or F2FS; media/host bytes last 900 of 1,200 s RocksDB 9.10 (db bench), PostgreSQL 16 (pgbench), MongoDB 8.0, SQLite 3.45 (WAL mode), fio 3.36 (emulated only)

3.5 3.0 2.5 2.0 1.5 1.0

o

Device

Enterprise

N

Emulated

WAF

TABLE III: Testbeds and workloads of the evaluation.

(b) Enterprise SSD

Fig. 6: WAF of one RocksDB under each placement, testing inferred labels against RocksDB’s exact levels (a) and the Planner’s three streams against a five-stream per-level map (b). (a) Emulated SSD, 128 MiB units, 74% fill. (b) Enterprise SSD, 77% fill (F2FS 76%); the Planner is given RocksDB’s levels, testing only its grouping. Right of the rule: F2FS; dotted: F2FS without placement. Whiskers span the runs. The eFDP Planner also places the boundaries well, whereas rules that rank labels by another statistic can misplace one. The eFDP Planner scores labels at the reclaim age, here about 96 s, where its three classes die before, near, and after reclaim (§III-B). Ranking by median age at death instead merges generations 4 and 5, so generation 5’s live pages sit in units that generation 4’s deaths empty. As a result, WAF rose by 0.31 in paired runs. RocksDB’s shipped hints make the same merge, at 0.53 above eFDP. AutoStream’s address counts instead mix levels. On four streams each, it removed 12% of the excess WAF to eFDP’s 56%. The same grouping carries over to the enterprise SSD, whose reclaim age, about 23 min, is 14× the emulator’s. Given RocksDB’s true levels and their ages at death profiled on the device, so that only the grouping was tested, the eFDP Planner again chose the vendor’s three classes, at any scoring age from 0.5 to 2 Â. On three streams, it matched a five-stream per-level map, 1.32 against 1.30 (Fig. 6(b)). Fixed rules removed far less of each file system’s excess WAF: 9% for the shipped hints on ext4 and 16% for F2FS’s temperatures on F2FS,2 against 76% for the eFDP Planner on ext4. This is because F2FS folds RocksDB’s hints into three temperatures and thus puts level 3 with all deeper levels. C. Is Isolating Tenants Enough? Isolating tenants is not enough, as boundaries inside tenants double the excess WAF that isolation removes. An operator who does not study the workloads gives each tenant one stream, as io.stream does. That map captures the whole gain only if lifetimes differ only between tenants, and it leaves three of our seven streams idle. We compare isolation with no placement and with two seven-stream eFDP layouts from one profiling window. Both keep PostgreSQL’s and SQLite’s logs apart from their data and differ only in RocksDB’s boundary. eFDP-G, which Algorithm 1 chooses, splits generations 0–3 from deeper ones. Meanwhile, eFDP-T spends that stream on RocksDB’s flush thread. 2 We test F2FS only on the enterprise SSD. On the emulated SSD, F2FS with its temperature placement and segment-sized discard had over twice the WAF of ext4 without placement.

P-

G

T

WAF

Normalized throughput

2.13

Unplaced

2.0 1.5 1.0 0.5 0.0

Isolation eFDP-T eFDP-G

D

P-

eF

D

io

n

t

at ol Is

em ac

2.29

PostgreSQL

N

o

pl

2.59

RocksDB

eF

3.05

3.0 2.5 2.0 1.5 1.0

en

WAF

SQLite

(a) WAF by tenant

(b) Throughput by tenant

Fig. 7: Per-tenant WAF (a) and throughput (b) on the fourtenant shared node, comparing isolation with boundaries inside tenants. Emulated SSD, 128 MiB units. (a) GC copies per page written, by tenant, so each bar’s top is its WAF; without placement, copies cannot be attributed to a tenant. (b) Throughput normalised to no placement (dotted line). eFDP-T and eFDP-G divide RocksDB by flush thread and by generation. Whiskers span min–max. Isolation removed 22% of the excess WAF, and eFDP-G twice that, 45% (Fig. 7(a)). Since GC copies take flash time from all tenants, fewer copies also let RocksDB, PostgreSQL, and SQLite run 19–23% faster than under isolation (Fig. 7(b)). Most of the gain over isolation comes from a boundary no one had to name: SQLite’s log. From isolation to eFDP-G, GC copies of SQLite’s data per page written halved, from 0.463 to 0.217, more than half of the node’s drop. The log dies long before the reclaim age while the database outlives it (Section III-B) [43]. As a result, when they share a stream, every unit that the log’s deaths empty still holds database pages to copy. The profile found this log unaided, whereas a per-file rule [44], [45] needs an operator who knows it. The stream count alone does not explain the gain, because eFDP-T used as many streams as eFDP-G, yet removed only 37%. RocksDB’s flush thread writes only level-0 files, while its compaction threads write short-lived shallow and longlived deep levels alike. Splitting by thread left GC copies of RocksDB’s data near isolation’s, 0.74 against 0.79 per page written. Only the generation split separates shallow from deep levels, lowering WAF by a further 0.155, three-quarters on RocksDB’s streams. In our mixed node, non-database tenants did not displace the main boundary, as SQLite’s log again got its own stream. With six tenants and seven streams, the eFDP Planner gave each tenant its own stream, leaving none for RocksDB’s generations. With databases scaled to keep the same fill, this layout removed 35% of the excess WAF, from 2.66 to 2.09. RocksDB and PostgreSQL ran 16% and 26% faster and the reader’s throughput rose 22%, while the build ran within 5% of its rate without placement. SQLite’s change was within noise. D. Who Gets a Stream When Streams Run Out? When streams run out, eFDP’s default, tenant-first allocation, costs almost nothing at seven streams. Fig. 8 sweeps the budget B on the shared node against tenant-blind k-means. At seven streams, the gap was negligible, 2.12 against 2.16,

3.0 2.8 2.6 2.4 2.2 2.0 1.8

No placement

cost of isolation

Tenant-first (eFDP) Tenant-blind (k-means) 2

3

4 5 Number of streams (B)

6

7

Fig. 8: WAF of tenant-first and tenant-blind allocation by stream budget, to show what keeping each tenant on its own streams costs (shaded). At our SSDs’ seven free streams, the cost is under 0.1 WAF. Shared node, emulated SSD, one profile; mean of 2–3 runs, min–max whiskers. even with the tenant guarantee and generations 4 and 5 kept together (Fig. 5(a)), while k-means gave generation 4 its own stream. Under tenant-first allocation, each stream beyond the four tenant streams went where it saved the most, as worth predicted. The fifth went to SQLite’s log and the sixth to RocksDB’s generations, which together lowered WAF by about 0.45, while the seventh, PostgreSQL’s log, changed it by less than 0.1. The three boundaries were worth 9.3 GiB, 6.8, and 4.0. From isolation to eFDP-G, their tenants’ GC copies per page written fell by 0.25, 0.16, and a negligible 0.02. Splitting generation 4 from 5, worth about 2.6 GiB, ranked last and got no stream. Overall, WAF fell from 2.71 at two streams to 2.16 at seven. Tenant-first allocation trails a tenant-blind rule only when streams are scarcer, and then by about 0.2. With three to five streams, k-means was 0.18–0.24 below tenant-first. This gap reflects both the tenant guarantee and the different grouping that k-means draws. At four and five streams, the guarantee spent one stream on the idle MongoDB. Scarcity, however, comes quickly. With two or three classes per tenant, three writing tenants already fill seven streams. Where lifetimes separate cleanly, the two rules agree. On one RocksDB, k-means derives eFDP’s layout byte for byte. E. When Does the Layout Change, and What Does It Recover? eFDP re-plans only when the writing tenants change, within about 7 min, and then beats a static layout. On the emulated SSD, the write load moved from the first RocksDB to the second (Fig. 9). Every run started from the first one’s layout, which puts all of the second’s data on one stream. We compare eFDP’s loop with a static layout that keeps the first layout, as a map drawn once would. We then compare it with installat-switch, which installs the loop’s grouping at the switch to show what waiting for a profile costs. Each ran three times for 30 min after the switch and once for 60 min. The drift check, run every  = 89 s, caught the switch at its first check, 42 s after it; here, the second RocksDB’s share of the writes jumped from 0 to 52–54%. Any activity threshold up to half would have caught it. After a profiling window of 4 (356 s), the new layout went in 417–420 s after the switch in every run. No write landed on another tenant’s handle.

WAF

TABLE IV: Overhead of eFDP’s eBPF programs on the emulated SSD. Key: 95% interval within ±2.5% (≈0), up to x (≤ x), or over 15% wide (–).

Install at switch

Workload

install

switch

trigger

3

measured window

2 1

Overhead vs. no eBPF (%)

Static layout 0

10

eFDP loop

20 30 40 Time since switch (min)

50

60

Fig. 9: WAF per minute across a tenant switch, comparing eFDP’s loop, a static layout, and the loop’s grouping installed at the switch. Emulated SSD, four streams; the first RocksDB writes for 20 min, then the second. Light bands: min–max of three earlier 30-min runs; shaded: the measured window. From then on, the layout derived by the loop alone, not offline as in Fig. 4, kept WAF far below the static layout’s for the whole hour. Between 15 and 30 min after the switch, WAF was 1.97 under the loop and 3.06 under the static layout in the three 30-min runs, and 1.97 and 3.04 in the 60-min run. In the hour’s last 15 min, the loop was still 0.84 below, at 2.51 against 3.35, removing 36% of the static layout’s excess WAF. Waiting for the profile cost no WAF later. Installing the grouping at the switch did no better, at 2.40 between 15 and 30 min and 2.70 in the last 15 min. F. Does eFDP Need Tuning? eFDP needed no per-workload tuning in our tests, because only two inputs shaped its layouts: the stream budget B and the reclaim age Â. Both are measured. When we re-planned the profiles of one RocksDB and of the shared node one parameter at a time, its other parameters (Table II) left the layout unchanged for any minimum side size from 1 to 20% and any file-pattern purity from 60 to 100%. So did scoring survival anywhere from 0.40 to 1.2 Â on the shared node and from 0.13 to 2.6 Â on one RocksDB. Past 1.2 Â, generation 4, which dies near Â, changes side, yet WAF stayed within 0.1. The profiling window of four reclaim ages is long enough. On the shared node, windows longer than 3.75 Â moved no further bytes between streams. On one RocksDB, the first snapshot, at 0.45 Â, was already final. Thus, the earlier 900 s window behind the shared-node layouts changed nothing, nor did their directory walk. Recording paths at open instead changes no byte’s stream. Finally, the eFDP Planner’s choice holds against its nearest rivals. Of the ten ways to cut RocksDB’s six generations into three contiguous classes, its {0–3}{4}{5} matched the two it ranks next, {0–2}{3–4}{5} and {0–1}{2–4}{5}, within 0.1 WAF. The median-age ranking’s {0–2}{3}{4–5} was 0.31 worse. G. What Does Per-Write Placement Cost? Placement costs 8–9% more CPU per small write, direct or buffered, against the same kernel without eBPF (Table IV). The cost is fixed per request. A 4 KiB direct write takes about 4.5 µs more than its 56 µs of CPU, however little data it carries. eBPF programs account for 2.6–3.0 µs of that (the placer 1.45– 1.74), and the rest is unattributed. These costs include the

4 KiB direct, QD1 4 KiB direct, QD16 4 KiB buffered 128 KiB direct, QD16 4 KiB direct read, QD1

Throughput loss p99 increase CPU per I/O ≈0 7.9 9.5 ≈0 ≈0

≈0 4.5 – ≈0 ≈0

8.0 8.6 8.7 ≤5.5 ≤4.8

CPU: the experiment’s cgroups. One fio tenant writes unplaced under every arm, so the cost is eBPF work alone, including the generation tracker (0.1 µs per placed request).

eFDP Monitor’s signals. Profiling adds 0.5–1.4 µs of eBPF time per small write and 108 MiB of tables on the emulated SSD, but only during a profiling window. Placement’s CPU slows a writer only when it is CPU-bound. At queue depth 16 and in buffered writes, where fio’s thread was nearly saturated, IOPS fell by about as much. At queue depth 1, where the thread mostly waits on the device, IOPS and 99th-percentile latency stayed within ±2.5%. The same held for 128 KiB writes, which spread the same work over 32 times the data, and for reads, which leave the placer early. VI. R ELATED W ORK Lifetime-aware placement. Lifetime-aware systems group data by expected death, but none divides a scarce handle budget among tenants. On multi-stream SSDs [46], FDP’s predecessor, systems inferred lifetimes without application hints. AutoStream [33] infers them from how sequentially, often, and recently an address is written. PCStream [42] does so from each write’s program context and lifetimes that overwrites and discards reveal. SepBIT [16] infers them from invalidation times. Later systems add host structure, such as file-system metadata in FStream [44], object identity in FlashAlloc [47], file paths and observed behavior through a user-space shim in Valet [45], and database semantics [48]. FDP placement policies. Existing FDP systems rely on application hints, operator-defined mappings, or fixed file-system policies (Table I) [10], [12], [36]–[38]. Even io.stream [11], which shows that grouping by lifetime matters more than stream count, assigns lifetimes from domain knowledge and leaves dividing a tenant to future work. eBPF-based storage systems. Prior eBPF storage work speeds up I/O but does not place data. ExtFUSE [49], BPF for Storage [50], XRP [51], and BPF-oF [52] accelerate I/O or push computation closer to storage. The potential for eBPF-based stream allocation was raised at the Linux LSFMM+BPF 2025 summit [7]. eFDP realizes this direction through transparent, workload-adaptive allocation without modifying applications, file systems, or kernel source. VII. C ONCLUSION This work shows that effective FDP placement requires more than isolating tenants: writes must be grouped by lifetime within tenants, scarce placement streams must be allocated by their expected GC savings, and assignments must adapt as active tenants change. These findings challenge the reliance on

static, application-defined placement policies. eFDP demonstrates that such decisions can be made transparently from kernel-observed I/O behavior, without modifying applications, file systems, or kernel source. By making lifetime-aware placement automatic and adaptive, eFDP enables practical FDP use in multi-tenant storage systems. R EFERENCES [1] N. Agrawal, V. Prabhakaran, T. Wobber, J. D. Davis, M. Manasse, and R. Panigrahy, “Design tradeoffs for SSD performance,” in Proceedings of the USENIX Annual Technical Conference (ATC), 2008, pp. 57–70. [2] X.-Y. Hu and R. Haas, “The fundamental limit of flash random write performance: Understanding, analysis and performance modelling,” IBM Research, Research Report RZ 3771, 2010. [3] P. Desnoyers, “Analytic models of SSD write performance,” ACM Transactions on Storage, vol. 10, no. 2, 2014. [4] J. Huang, A. Badam, L. Caulfield, S. Nath, S. Sengupta, B. Sharma, and M. K. Qureshi, “FlashBlox: Achieving both performance isolation and uniform lifetime for virtualized SSDs,” in Proceedings of the 15th USENIX Conference on File and Storage Technologies (FAST), 2017. [5] G. Haas, B. Lee, P. Bonnet, and V. Leis, “SSD-iq: Uncovering the hidden side of SSD performance,” Proceedings of the VLDB Endowment, vol. 18, no. 11, pp. 4295–4308, 2025. [6] B. Van Houdt, “On the necessity of hot and cold data identification to reduce the write amplification in flash-based SSDs,” Performance Evaluation, vol. 82, pp. 1–14, 2014. [7] J. Edge, “Flexible data placement,” LWN.net (report from LSFMM+BPF 2025), May 2025. [Online]. Available: https://lwn.net/Articles/1018642/ [8] NVM Express, “Overcoming the write amplification problem with NVM Express flexible data placement,” NVM Express blog, 2024. [Online]. Available: https://nvmexpress.org/ nvmeflexible-data-placement-fdp-blog/ [9] Samsung Electronics, “Getting started with flexible data placement (FDP),” White paper, 2024. [Online]. Available: https://download.semiconductor.samsung.com/ resources/white-paper/getting-started-with-fdp-v4.pdf [10] M. Allison, A. George, J. González, D. Helmick, V. Kumar, R. R. Nair, and V. Shah, “Towards efficient flash caches with emerging NVMe flexible data placement SSDs,” in Proceedings of the 20th European Conference on Computer Systems (EuroSys), 2025, pp. 1142–1160. [11] S. Son, J. Kim, and K. Lee, “io.stream: cgroup-granular FDP for unmodified workloads,” in Proceedings of the 18th ACM Workshop on Hot Topics in Storage and File Systems (HotStorage), 2026. [12] W. Qi, “f2fs: Map data writes to FDP streams,” linux-f2fs-devel mailing list; in mainline since Linux 7.2, 2026. [Online]. Available: https://lore.kernel.org/linux-f2fs-devel/ [13] H. Li, M. Hao, M. H. Tong, S. Sundararaman, M. Bjørling, and H. S. Gunawi, “The CASE of FEMU: Cheap, accurate, scalable and extensible flash emulator,” in Proceedings of the 16th USENIX Conference on File and Storage Technologies (FAST), 2018, pp. 83–90. [14] NVM Express, Inc., “NVM Express base specification, revision 2.1,” Ratified specification, Aug. 2024, accessed 2026-10-03. [Online]. Available: https://nvmexpress.org/wp-content/uploads/ NVM-Express-Base-Specification-Revision-2.1-2024.08.05-Ratified. pdf [15] X. Zhang, J. Bhimani, S. Pei, E. Lee, S. Lee, Y. J. Seong, E. J. Kim, C. Choi, E. H. Nam, J. Choi, and B. S. Kim, “Storage abstractions for SSDs: The past, present, and future,” ACM Transactions on Storage, vol. 21, no. 1, pp. 1–44, 2025. [16] Q. Wang, J. Li, P. P. C. Lee, T. Ouyang, C. Shi, and L. Huang, “Separating data via block invalidation time inference for write amplification reduction in log-structured storage,” in Proceedings of the 20th USENIX Conference on File and Storage Technologies (FAST), 2022, pp. 429– 443. [17] A. George, “NVMe FDP – a promising new SSD data placement approach,” Samsung Semiconductor Tech Blog, Jan. 2025, accessed 2026-09-30. [Online]. Available: https://semiconductor.samsung.com/news-events/tech-blog/ nvme-fdp-a-promising-new-ssd-data-placement-approach/ [18] C. Hellwig, “Support block layer write streams and FDP,” Linux kernel mailing list, patch series cover letter, Nov. 2024. [Online]. Available: https://lkml.iu.edu/2411.2/02654.html

[19] K. Busch, “nvme: Use FDP streams if write stream is provided,” Linux kernel commit 38e8397dde63, merged for Linux 6.16, May 2025. [Online]. Available: https://github.com/torvalds/linux/commit/ 38e8397dde63 [20] K. Joshi, A. Gupta, J. González, A. Kumar, K. K. Reddy, A. George, S. Lund, and J. Axboe, “I/O passthru: Upstreaming a flexible and efficient I/O path in Linux,” in Proceedings of the 22nd USENIX Conference on File and Storage Technologies (FAST), 2024, pp. 107– 121. [21] K. Joshi and A. Gupta, “XFS write streams (PATCH v4 0/6),” linux-xfs mailing list, Jul. 2026. [Online]. Available: https://lore.kernel. org/linux-xfs/ [22] The Linux Kernel Development Community, “BPF documentation,” The Linux Kernel documentation, 2026. [Online]. Available: https: //docs.kernel.org/bpf/ [23] The kernel development community, “Program types and ELF sections,” The Linux Kernel documentation (libbpf), version 7.3.0-rc5, accessed 2026-10-03. [Online]. Available: https://docs.kernel.org/bpf/ libbpf/program types.html [24] ——, “BPF design Q&A,” The Linux Kernel documentation, version 7.3.0-rc5, accessed 2026-10-03. [Online]. Available: https: //docs.kernel.org/bpf/bpf design QA.html [25] The Linux Kernel Development Community, “Linux 7.2-rc4,” Linux kernel source tree, git tag v7.2-rc4 (commit 1590cf032971), Jul. 2026, accessed 2026-10-03. [Online]. Available: https://github.com/torvalds/ linux/tree/v7.2-rc4 [26] The kernel development community, “BPF kernel functions (kfuncs),” The Linux Kernel documentation, version 7.3.0-rc5, accessed 2026-1003. [Online]. Available: https://docs.kernel.org/bpf/kfuncs.html [27] Samsung Semiconductor, “Optimizing RocksDB write amplification on FDP SSDs,” Samsung Semiconductor Tech Blog, Sep. 2026. [Online]. Available: https://semiconductor.samsung.com/news-events/ tech-blog/optimizing-rocksdb-write-amplification-on-fdp-ssds/ [28] S. Dong, A. Kryczka, Y. Jin, and M. Stumm, “RocksDB: Evolution of development priorities in a key-value store serving large-scale applications,” ACM Transactions on Storage (TOS), vol. 17, no. 4, pp. 1–32, 2021. [29] The PostgreSQL Global Development Group, “PostgreSQL: The world’s most advanced open source database,” 2026, accessed 2026-10-03. [Online]. Available: https://www.postgresql.org/ [30] SQLite Project, “SQLite home page,” Official SQLite website, 2026, accessed 2026-10-03. [Online]. Available: https://www.sqlite.org/index. html [31] MongoDB, Inc., “MongoDB,” 2026, accessed 2026-10-03. [Online]. Available: https://www.mongodb.com/ [32] J. Axboe, “Add support for write life time hints,” Linux kernel patch series, merged in Linux 4.13, 2017. [Online]. Available: https://lwn.net/Articles/726477/ [33] J. Yang, R. Pandurangan, C. Choi, and V. Balakrishnan, “AutoStream: Automatic stream management for multi-streamed SSDs,” in 10th ACM International Systems and Storage Conference (SYSTOR 17), 2017, pp. 1–11. [34] H. Yong, K. Jeong, J. Lee, and J.-S. Kim, “vStream: Virtual stream management for multi-streamed SSDs,” in USENIX Workshop on Hot Topics in Storage and File Systems (HotStorage 18), 2018. [35] J. Bhimani, Z. Yang, J. Yang, A. Maruf, N. Mi, R. Pandurangan, C. Choi, and V. Balakrishnan, “Automatic stream identification to improve flash endurance in data centers,” ACM Transactions on Storage (TOS), vol. 18, no. 2, pp. 1–29, 2022. [36] Samsung Electronics, “TorFS: A plugin that enables RocksDB to access FDP SSDs,” GitHub repository, commit 0a6e399, 2025. [Online]. Available: https://github.com/SamsungDS/TorFS [37] S. Lee, S. Byeon, S. Hwang, J. Park, J.-Y. Hwang, J. Han, J. González, A. Khan, and Y. Kim, “SlimIO: Lightweight I/O path design for write isolation in FDP-backed in-memory databases,” in Proceedings of the SC’25 Workshops of the International Conference for High Performance Computing, Networking, Storage and Analysis (PDSW), 2025, pp. 1375– 1384. [38] P.-X. Chen, D. Seo, and N. Dutt, “FDPFS: Leveraging file system abstraction for FDP SSD data placement,” IEEE Embedded Systems Letters, vol. 16, no. 4, pp. 349–352, 2024. [39] T. Heo, “Control group v2,” The Linux Kernel documentation, version 7.3.0-rc5, 2026, accessed 2026-10-03. [Online]. Available: https://docs.kernel.org/admin-guide/cgroup-v2.html

[40] P. O’Neil, E. Cheng, D. Gawlick, and E. O’Neil, “The log-structured merge-tree (LSM-tree),” Acta Informatica, vol. 33, no. 4, pp. 351–385, 1996. [41] M. Rosenblum and J. K. Ousterhout, “The design and implementation of a log-structured file system,” ACM Transactions on Computer Systems (TOCS), vol. 10, no. 1, pp. 26–52, 1992. [42] T. Kim, D. Hong, S. S. Hahn, M. Chun, S. Lee, J. Hwang, J. Lee, and J. Kim, “Fully automatic stream management for multi-streamed SSDs using program contexts,” in 17th USENIX Conference on File and Storage Technologies (FAST 19), 2019, pp. 295–308. [43] SQLite Project, “Write-ahead logging,” SQLite documentation, 2026, accessed 2026-10-07. [Online]. Available: https://www.sqlite.org/wal. html [44] E. Rho, K. Joshi, S.-U. Shin, N. J. Shetty, J.-Y. Hwang, S. Cho, D. D. G. Lee, and J. Jeong, “FStream: Managing flash streams in the file system,” in Proceedings of the 16th USENIX Conference on File and Storage Technologies (FAST), 2018, pp. 257–268. [45] D. R. Purandare, P. Alvaro, A. Wildani, D. D. E. Long, and E. L. Miller, “Valet: Efficient data placement on modern SSDs,” in Proceedings of the ACM Symposium on Cloud Computing (SoCC), 2025. [46] J.-U. Kang, J. Hyun, H. Maeng, and S. Cho, “The multi-streamed solidstate drive,” in Proceedings of the 6th USENIX Workshop on Hot Topics in Storage and File Systems (HotStorage), 2014. [47] J. Park, S. Choi, G. Oh, S. Im, M.-W. Oh, and S.-W. Lee, “FlashAlloc: Dedicating flash blocks by objects,” Proceedings of the VLDB Endowment, vol. 16, no. 11, pp. 3266–3278, 2023. [48] B. Lee, T. Ziegler, and V. Leis, “How to write to SSDs,” Proceedings of the VLDB Endowment, vol. 19, no. 7, pp. 1469–1483, 2026. [49] A. Bijlani and U. Ramachandran, “Extension framework for file systems in user space,” in Proceedings of the 2019 USENIX Annual Technical Conference (USENIX ATC), 2019, pp. 121–132. [50] Y. Zhong, H. Wang, Y. J. Wu, A. Cidon, R. Stutsman, A. Tai, and J. Yang, “BPF for storage: An exokernel-inspired approach,” in Proceedings of the Workshop on Hot Topics in Operating Systems (HotOS), 2021, pp. 128–135. [51] Y. Zhong, H. Li, Y. J. Wu, I. Zarkadas, J. Tao, E. Mesterhazy, M. Makris, J. Yang, A. Tai, R. Stutsman, and A. Cidon, “XRP: In-kernel storage functions with eBPF,” in Proceedings of the 16th USENIX Symposium on Operating Systems Design and Implementation (OSDI), 2022, pp. 375–393. [52] I. Zarkadas, T. Zussman, J. Carin, S. Jiang, Y. Zhong, J. Pfefferle, H. Franke, J. Yang, K. Kaffes, R. Stutsman, and A. Cidon, “BPFoF: Storage function pushdown over the network,” arXiv preprint arXiv:2312.06808, 2023.

Related documents

Record · ID 1571886 · SHA-256 a2c35195e2092d9d
Retrieved via Conceptio: every document is proof-bundled with source, license, and retrieval metadata.
Bring this data into your agent: Connect via MCP or API · 20 free credits to start · no card required
Read the Docs → · View CLI source on GitHub · Conceptio Open Knowledge Archive