Skip to content

Commit c0beaa7

Browse files
Support direct I/O for background SSTable writes
Adds an opt-in O_DIRECT write path for background SSTable producers, bypassing the OS page cache for data that is unlikely to be re-read soon after being written. Memtable flushes remain buffered. Enabled via two new YAML knobs: - background_write_disk_access_mode: standard (default) | direct - direct_write_buffer_size: 1MiB (default; aligned up to FS block size, auto-grown to fit a worst-case compressed chunk) The path is gated by config, table compression being enabled, and an OperationType allowlist in DataComponent. The allowlist is exhaustive over OperationType: any new value left unclassified fails static initialization. Operations on the DIO path: COMPACTION, MAJOR_COMPACTION, TOMBSTONE_COMPACTION, ANTICOMPACTION, GARBAGE_COLLECT, CLEANUP, UPGRADE_SSTABLES, WRITE, STREAM (chunked receiver only), RELOCATE, UNKNOWN (offline sstablesplit). Operations off the DIO path: - FLUSH (policy: just-flushed data is hot, keep in page cache) - SCRUB (correctness: tryAppend needs mark/resetAndTruncate) - Zero-Copy Streaming (bypasses DataComponent.buildWriter) - Uncompressed writers (only CompressedSequentialWriter has a DIO subclass in this change) StartupChecks fails fast if 'direct' is requested on a platform/FS that does not support O_DIRECT. patch by Sam Lightfoot; reviewed by Ariel Weisberg, Dmitry Konstantinov for CASSANDRA-21134
1 parent f33c113 commit c0beaa7

27 files changed

Lines changed: 3449 additions & 96 deletions

conf/cassandra.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,25 @@ commitlog_disk_access_mode: legacy
693693
# - direct: use direct I/O for compaction reads, bypassing the OS page cache
694694
# compaction_read_disk_access_mode: auto
695695

696+
# Set the disk access mode for writing compressed SSTables during background operations
697+
# (compaction, streaming, cleanup, repair, etc.). The allowed values are:
698+
# - standard: use buffered I/O (default)
699+
# - direct: use direct I/O, bypassing the OS page cache
700+
# Only applies to compressed tables. Uncompressed tables always use buffered I/O.
701+
# Memtable flushes always use buffered I/O regardless of this setting, as flushed
702+
# data typically benefits from page cache for subsequent reads (workloads that
703+
# rarely read recently-flushed data, e.g. write-heavy time series, may see less
704+
# benefit from caching flush output).
705+
# background_write_disk_access_mode: standard
706+
707+
# Size of the in-memory staging buffer for Direct IO background writes. Trades off syscall
708+
# frequency against per-flush blocking latency on the compaction thread.
709+
# Aligned up to filesystem block size; auto-expands to fit a single compressed chunk + CRC
710+
# + one block when chunk_length exceeds this value.
711+
# Allocated per concurrent background writer (e.g. one per active compaction thread,
712+
# streaming session, etc.), so total off-heap usage scales with concurrency.
713+
# direct_write_buffer_size: 1MiB
714+
696715
# Compression to apply to SSTables as they flush for compressed tables.
697716
# Note that tables without compression enabled do not respect this flag.
698717
#

conf/cassandra_latest.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,21 @@ commitlog_disk_access_mode: auto
700700
# - direct: use direct I/O for compaction reads, bypassing the OS page cache
701701
# compaction_read_disk_access_mode: auto
702702

703+
# Set the disk access mode for writing compressed SSTables during background operations
704+
# (compaction, streaming, cleanup, repair, etc.). The allowed values are:
705+
# - standard: use buffered I/O (default)
706+
# - direct: use direct I/O, bypassing the OS page cache
707+
# Only applies to compressed tables. Uncompressed tables always use buffered I/O.
708+
# Memtable flushes always use buffered I/O regardless of this setting, as flushed
709+
# data benefits from page cache for subsequent reads.
710+
background_write_disk_access_mode: direct
711+
712+
# Size of the in-memory staging buffer for Direct IO background writes. Trades off syscall
713+
# frequency against per-flush blocking latency on the compaction thread.
714+
# Aligned up to filesystem block size; auto-expands to fit a single compressed chunk + CRC
715+
# + one block when chunk_length exceeds this value.
716+
# direct_write_buffer_size: 1MiB
717+
703718
# Compression to apply to SSTables as they flush for compressed tables.
704719
# Note that tables without compression enabled do not respect this flag.
705720
#

src/java/org/apache/cassandra/config/Config.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ public MemtableOptions()
362362

363363
public DataStorageSpec.IntKibibytesBound compressed_read_ahead_buffer_size = new DataStorageSpec.IntKibibytesBound("256KiB");
364364

365+
// Direct IO for background SSTable writes (compaction, streaming, cleanup, etc.)
366+
// When 'direct' is set, background writes bypass the OS page cache using O_DIRECT.
367+
// Memtable flushes always use buffered I/O regardless of this setting.
368+
// Default is 'standard' (buffered I/O) - users must opt-in to Direct IO
369+
public DiskAccessMode background_write_disk_access_mode = DiskAccessMode.standard;
370+
371+
// Size of the in-memory staging buffer for Direct IO background writes. Trades off syscall
372+
// frequency against per-flush blocking latency on the compaction thread.
373+
// Aligned up to filesystem block size; auto-expands to fit a single compressed chunk + CRC
374+
// + one block when chunk_length exceeds this value.
375+
public DataStorageSpec.IntKibibytesBound direct_write_buffer_size = new DataStorageSpec.IntKibibytesBound("1MiB");
376+
365377
// fraction of free disk space available for compaction after min free space is subtracted
366378
public volatile Double max_space_usable_for_compactions_in_percentage = .95;
367379

@@ -1275,8 +1287,7 @@ public enum DiskAccessMode
12751287
legacy,
12761288

12771289
/**
1278-
* Direct-I/O is enabled for commitlog disk only.
1279-
* When adding support for direct IO, update {@link org.apache.cassandra.service.StartupChecks#checkKernelBug1057843}
1290+
* When adding support for Direct I/O, update {@link org.apache.cassandra.service.StartupChecks#checkKernelBug1057843}
12801291
*/
12811292
direct
12821293
}

src/java/org/apache/cassandra/config/DatabaseDescriptor.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ public class DatabaseDescriptor
224224

225225
private static DiskAccessMode compactionReadDiskAccessMode;
226226

227+
private static DiskAccessMode backgroundWriteDiskAccessMode;
228+
227229
private static AbstractCryptoProvider cryptoProvider;
228230
private static IAuthenticator authenticator;
229231
private static IAuthorizer authorizer;
@@ -897,6 +899,8 @@ else if (conf.repair_session_space.toMebibytes() > (int) (Runtime.getRuntime().m
897899
if (conf.hints_directory.equals(conf.saved_caches_directory))
898900
throw new ConfigurationException("saved_caches_directory must not be the same as the hints_directory", false);
899901

902+
initializeBackgroundWriteDiskAccessMode();
903+
900904
if (conf.memtable_flush_writers == 0)
901905
{
902906
conf.memtable_flush_writers = conf.data_file_directories.length == 1 ? 2 : 1;
@@ -3406,6 +3410,71 @@ public static void initializeCommitLogDiskAccessMode()
34063410
commitLogWriteDiskAccessMode = accessModeDirectIoPair.left;
34073411
}
34083412

3413+
public static DiskAccessMode getBackgroundWriteDiskAccessMode()
3414+
{
3415+
return backgroundWriteDiskAccessMode;
3416+
}
3417+
3418+
@VisibleForTesting
3419+
public static void setBackgroundWriteDiskAccessMode(DiskAccessMode diskAccessMode)
3420+
{
3421+
backgroundWriteDiskAccessMode = diskAccessMode;
3422+
conf.background_write_disk_access_mode = diskAccessMode;
3423+
}
3424+
3425+
public static DataStorageSpec.IntKibibytesBound getDirectWriteBufferSize()
3426+
{
3427+
return conf.direct_write_buffer_size;
3428+
}
3429+
3430+
/**
3431+
* The directories opened with O_DIRECT for writing. Startup checks that must reason about O_DIRECT
3432+
* write targets (e.g. the kernel-bug 1057843 check) derive their path set from here.
3433+
*/
3434+
public static Set<Path> getDirectIOWritePaths()
3435+
{
3436+
Set<Path> paths = new HashSet<>();
3437+
3438+
if (getCommitLogWriteDiskAccessMode() == DiskAccessMode.direct)
3439+
paths.add(new File(getCommitLogLocation()).toPath());
3440+
3441+
if (getBackgroundWriteDiskAccessMode() == DiskAccessMode.direct)
3442+
for (String dataDir : getAllDataFileLocations())
3443+
paths.add(new File(dataDir).toPath());
3444+
3445+
return paths;
3446+
}
3447+
3448+
@VisibleForTesting
3449+
public static void initializeBackgroundWriteDiskAccessMode()
3450+
{
3451+
DiskAccessMode providedMode = conf.background_write_disk_access_mode;
3452+
3453+
if (providedMode == DiskAccessMode.direct)
3454+
{
3455+
// DataStorageSpec already rejects negatives at parse time; zero is the remaining
3456+
// nonsense value. The writer's Math.max would silently coerce it to minRequiredSize,
3457+
// which masks a likely operator mistake — fail fast instead.
3458+
if (conf.direct_write_buffer_size.toBytes() <= 0)
3459+
throw new ConfigurationException("direct_write_buffer_size must be > 0 when background_write_disk_access_mode is 'direct'. " +
3460+
"Got: " + conf.direct_write_buffer_size, false);
3461+
3462+
// Create the data directories up front (as we do for the commit log) so the kernel-bug 1057843
3463+
// startup check can stat each O_DIRECT write target. Direct I/O support is validated separately
3464+
// by the directio_support startup check.
3465+
if (!toolInitialized)
3466+
for (String dataDir : getAllDataFileLocations())
3467+
PathUtils.createDirectoriesIfNotExists(new File(dataDir).toPath());
3468+
}
3469+
else if (providedMode != DiskAccessMode.standard)
3470+
{
3471+
throw new ConfigurationException("Unsupported disk access mode for background_write_disk_access_mode " +
3472+
"(options: standard/direct): " + providedMode, false);
3473+
}
3474+
3475+
backgroundWriteDiskAccessMode = providedMode;
3476+
}
3477+
34093478
public static String getSavedCachesLocation()
34103479
{
34113480
return conf.saved_caches_directory;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.cassandra.io;
19+
20+
/**
21+
* Classifies an operation's eligibility for a direct-IO (O_DIRECT) data path, encoding both
22+
* the answer and the rationale class. Consumers maintain their own per-operation classification
23+
* and apply this alongside their own gates (e.g. compression, configuration mode);
24+
* {@link #SUPPORTED} is necessary but not sufficient.
25+
*/
26+
public enum DirectIoSupport
27+
{
28+
/**
29+
* Eligible for the direct-IO data path.
30+
* */
31+
SUPPORTED,
32+
33+
/**
34+
* The direct-IO path is mechanically incompatible with this operation. Removing this
35+
* exclusion requires code changes, not policy.
36+
*/
37+
UNSUPPORTED_CORRECTNESS,
38+
39+
/**
40+
* Direct IO would work but is deliberately disabled for performance or cache-residency
41+
* reasons. Removing this exclusion requires re-evaluating the policy, not code changes.
42+
*/
43+
UNSUPPORTED_POLICY,
44+
45+
/**
46+
* The operation never constructs a data-file writer, so the direct-IO question does not apply.
47+
* Exists so a consumer's classification can be total over its operation enum rather than partial.
48+
*/
49+
NOT_A_WRITER;
50+
51+
public boolean isSupported()
52+
{
53+
return this == SUPPORTED;
54+
}
55+
}

0 commit comments

Comments
 (0)