Cassandra CompactionにおけるDirect I/O:読み込みレイテンシのp99を5倍削減
Apache Cassandra 6のパッチにより、コンパクション時の読み込みレイテンシのp99が5倍改善されました。
これは、コンパクションがアプリケーションにとって不要なデータをページキャッシュに蓄積するため、Direct I/Oを利用してページキャッシュをバイパスすることで実現しました。
Direct I/Oはファイルシステムブロックサイズに合わせたメモリバッファを必要としますが、コンパクションによるキャッシュ汚染を軽減し、読み込みレイテンシの改善に大きく貢献します。
特に、読み込みレイテンシのテール部分(p99)において顕著な効果が見られ、システムの停止時間も大幅に削減されました。
分散型データベースCassandraのパフォーマンス改善に関する技術的な解説記事が発表されました。今回の改善は、Cassandraのバックグラウンド処理である「コンパクション」において、ディスクI/Oの処理方法を根本的に変更したものです。これにより、特に遅延が長いクエリ(p99)の応答速度が最大5倍に短縮されることが報告されています。
コンパクションとページキャッシュの衝突
Cassandraは高速な書き込みを実現するために、データをディスク上で非同期に整理する「コンパクション」という処理を常に行っています。しかし、このコンパクション処理が、OSが管理するメモリキャッシュ(ページキャッシュ)を大量の不要なデータで汚染してしまうという問題がありました。ページキャッシュは、頻繁にアクセスされる「ホットデータ」を保持するための重要な領域です。コンパクションの読み取り対象となるデータは一度しか使われないため、このキャッシュを圧迫し、本来ホットなデータが追い出されてしまう(エビクション)原因となっていました。
Direct I/Oによるキャッシュバイパス
この問題を解決するため、開発者は「Direct I/O」という技術を導入しました。Direct I/Oは、アプリケーション(Cassandra)がOSのページキャッシュを介さずに、ディスクと直接ユーザー空間のバッファ間で読み書きを行う仕組みです。これにより、コンパクション処理がページキャッシュを汚染することが完全に回避されます。コンパクションの読み取りパスにDirect I/Oを適用することで、キャッシュの競合が解消され、データベースの安定したパフォーマンス維持に貢献しているとのことです。
実証実験で確認された効果
実際のベンチマーク実験では、このDirect I/Oの導入が明確な効果をもたらしました。ホットなデータセットがページキャッシュ内に収まる状況でも、p99(99パーセンタイル)の読み取り遅延は、従来のバッファリング方式と比較して5.2倍に改善したと報告されています。これは、極端に遅いクエリの応答時間が大幅に短縮されたことを意味し、ユーザー体験の向上に直結すると見られています。
結論
今回の改善は、データベースの内部処理とOSのメモリ管理機構との間の根本的な衝突を解消する、高度な技術的アプローチです。Direct I/Oの活用は、大規模な分散データベースのパフォーマンスを限界まで引き上げるための重要な一手となるでしょう。
原文の冒頭を表示(英語・3段落のみ)
A patch I contributed to Apache Cassandra 6 cuts p99 read latency by 5x during compaction.Compaction pollutes the page cache with data the application knows is throwaway, but the kernel does not. Compaction is unavoidable, the price Cassandra pays for fast writes. Data isn't sorted on the way in; it's sorted later, in the background, by merging files on disk.Reducing compaction throughput or increasing node memory can dampen the effect on tail query latencies. The first costs throughput, the second costs money. Both are compromises. Direct I/O allows Cassandra to live in better harmony with its own housekeeper, bypassing the page cache entirely for compaction reads.Linux Page CacheAny time a file-based read or write occurs (typically via read() and write() system calls), data passes through the page cache, a kernel-managed in-memory cache between the application and storage device.The kernel manages this through two LRU (least-recently-used) lists: an active list and an inactive list. Hot pages live on the active list; cold or read-once pages remain on the inactive list as first candidates for eviction.Buffered I/O: compaction and queries share the page cacheBuffered I/O works well for most applications, benefiting reads through caching and readahead, and writes through deferred, coalesced flushes, freeing the developer from reasoning about I/O sizing and access patterns.For most workloads, the kernel makes good decisions. Not all workloads are most workloads.The page cache is a sacred space, best populated with data likely to be re-accessed soon, or writes that benefit from coalescing before hitting disk.Compaction and the Page CacheCompaction, which merges multiple SSTables into a single SSTable, is a prime example of a page cache pollutant. Input SSTables are read sequentially and discarded; the output SSTable is written in a single sequential pass. Both reads and writes flood the page cache with data unlikely to be accessed again, displacing legitimate hot-page candidates.Displacement alone would be costly. The cost of eviction makes it worse.Clean, read-once pages from the input SSTables can be dropped immediately. Dirty pages of the newly written SSTable must first be flushed to disk before eviction is possible. Buffered writes of single-use pages are more expensive than buffered reads, and the reclaimer pays that expense.A clean page costs nothing to evict; a dirty page costs a disk write.kswapd, the kernel's background memory reclaimer, scans the LRU lists and evicts pages to keep utilisation within configured watermarks. Pages on the inactive list survive only if accessed between scans; repeated accesses earn promotion to the protected active list.Under memory pressure kswapd cycles faster, shrinking the promotion window. When allocations outpace reclamation, free memory falls below the min watermark and the kernel stalls the allocating thread. This is direct reclaim: the thread must free pages from memory itself before its allocation can proceed, blocking the triggering operation.For the compaction thread, a tolerable delay. For a critical read query that triggers a cache miss and must load pages from disk, it is not.Inflated tail latencies are inevitable. The kernel and Cassandra each have mitigations. Neither is enough.Existing MitigationsThe kernel's active/inactive page cache split provides some hot page protection. Read-once pages are contained in the inactive list. Premature eviction of hot page candidates remains the problem.Cassandra uses FADV_DONTNEED to hint to the kernel that compaction pages can be dropped, but only once an SSTable is fully processed. The pollution occurs during processing; the hint arrives too late.FADV_DONTNEED was adopted in 2010 in this Jira after both fadvise and Direct I/O were evaluated. Direct I/O showed no improvement in average read latency, the metric of focus at the time, but the wrong one.Introducing Direct I/ODirect I/O allows the application to read and write directly between disk and a userspace buffer, bypassing the page cache entirely. It requires both disk operations and off-heap memory buffers to be aligned to the filesystem block size.Control of disk operations is transferred from the kernel to the application, eliminating writeback storms and protecting the page cache from pollution by readahead and read-once workloads.Compaction is a prime candidate for Direct I/O on both the read and write path, with the read path addressed in this post. Input SSTables are read-once by definition; once compaction completes, that data will never be accessed again. The output SSTable, while not throwaway, is unlikely to see much read traffic. Freshly written SSTables are typically superseded by further compaction before they see meaningful access. Neither benefits from page cache residency.The loss of kernel readahead is mitigated by Cassandra's own chunk readahead buffer, introduced in Cassandra 5 by Jon Haddad and Jordan West. Jon Haddad, a long-time Cassandra contributor and consultant who writes on Cassandra internals at his blog, also filed the Jira to bring Direct I/O support to the compaction read path.I picked up the work, landing in this PR targeting Cassandra 6.BenchmarkingEnvironment: Ubuntu 22.04, Linux 6.8.0-106-generic, 6 GB cgroup, 3 GB heap (~3 GB page cache). RAID1 NVMe, readahead 4 KB. Classic active/inactive LRU (MGLRU disabled).Data: Cassandra 6.0-alpha2-SNAPSHOT, 2×65 GB SSTables (chunk_length_kb=4). Major compaction with cursor compaction enabled (default), unthrottled.Workload: 10K reads/s across a variable number of hot partitions (100K–10M, ~100 MB–10 GB). Page cache dropped and Cassandra restarted before each run.Headline numbersStarting with the 100 MB hot set, comfortably within the 3 GB page cache:
Metric
Direct I/O
※ 著作権に配慮し、引用は冒頭3段落までです。続きは元記事をご覧ください。