Skip to content

feat: feat(command): add FLUSHWAL to flush and fsync the RocksDB WAL - #3603

Closed
pipinstall1 wants to merge 1 commit into
apache:unstablefrom
influere:pr/flushwal-command
Closed

feat: feat(command): add FLUSHWAL to flush and fsync the RocksDB WAL#3603
pipinstall1 wants to merge 1 commit into
apache:unstablefrom
influere:pr/flushwal-command

Conversation

@pipinstall1

Copy link
Copy Markdown

Closes #3602

Repo: apache/kvrocks · Branch: pr/flushwal-command (in /home/dev/work/clipstream/kvrocks-upstream) · Base: unstable (1418e23d) · Files: src/commands/cmd_server.cc (+21)

Motivation

With rocksdb.write_options.sync yes, every command's write batch is fsynced individually. On a disk with an ~12 ms fsync floor (measured: 4 KiB rewrite, n=60, mean 12.0 ms, p50 11.0 ms), that caps a single connection at ~85 writes/s regardless of pipelining, and a Lua script issuing 4 writes per call at ~20 calls/s. Concurrency does not help because each redis.call fsyncs on its own:

config writers appends/s (4 writes each) ack p50 ack p99 durable on power loss
sync yes 32 21.6 1354 ms 3385 ms yes
sync yes 300 19.4 11022 ms 27169 ms yes
sync no 32 5442 5.1 ms 15.2 ms no
sync no 300 5992 44.8 ms 137.5 ms no

With sync no, kvrocks leaves RocksDB's manual_wal_flush at its default (false), so every WAL record is write()n before the command replies (a kill -9 loses nothing: 2000/2000 keys present after restart, with or without a flush). The exposure is power loss / kernel panic, and it is unbounded in time: an strace -y over a 12 s append run counted 21,496 acked appends behind exactly one fdatasync of the WAL (the creation sync at open); the next data sync is whenever the memtable flushes.

RocksDB already has the primitive that bounds this, DB::FlushWAL(sync=true), and kvrocks links it, but nothing in the command surface exposes it. This PR adds FLUSHWAL, which flushes the WAL buffer to the file and fsyncs it, making every write accepted before the call durable.

That enables group-commit durability from the client side: run sync no, append fast, and issue one FLUSHWAL per T ms (or per N writes), either holding acks until the flush returns (ack = fsynced) or accepting a bounded T-ms loss window on power loss. Measured with the same 4-write Lua append and one flusher process on the same disk (35 s windows, 4 client processes):

writers T (ms) appends/s ack p50 ack p99 FLUSHWAL/s FLUSHWAL p50
32 10 1382 21.8 ms 56.3 ms 88.8 11.4 ms
128 5 1937 57.8 ms 160.3 ms 53.8 16.8 ms
200 5 2391 72.4 ms 199.8 ms 47.0 17.1 ms

Sustained 120 s at 200 writers / T=5 ms: 2470 appends/s at ack p99 189 ms with every ack behind an fsync, versus 21.6 appends/s at sync yes: a ~114x throughput difference at the same durability guarantee. The strace count confirms coverage 1:1 (5 FLUSHWAL -> 5 fdatasync; 200 SETs with no FLUSHWAL -> no sync syscalls).

Change

FLUSHWAL
  • Calls srv->storage->GetDB()->FlushWAL(/*sync=*/true); replies +OK, or the RocksDB status string as an error.
  • Rejects with flushwal is meaningless with the disable_wal option when rocksdb.write_options.disable_wal is set (same pattern as the existing disable_wal check in this file).
  • Logs INFO("FLUSHWAL is triggered and executed successfully"), matching FLUSHMEMTABLE / FLUSHBLOCKCACHE.
  • Registered as MakeCmdAttr<CommandFlushWAL>("flushwal", 1, "read-only admin", NO_KEY): it performs no keyspace write and needs no exclusivity (it is safe to run concurrently with writers; that is its purpose). admin follows fix(command): require admin permission for privileged commands #3570's convention for privileged storage-maintenance commands. Reviewer input welcome on whether it should also be no-script; the use case above benefits from being callable from a Lua append script (throttled by a timestamp key) to avoid a separate flusher process, and the command is deterministic from the script's point of view (no keyspace effect, +OK).

The command class sits next to CommandFlushBlockCache in src/commands/cmd_server.cc.

Testing

  • Builds on unstable (cmake -DDISABLE_JEMALLOC=ON, gcc 13.3, RelWithDebInfo); see the build note in INDEX.md for what was and was not run on this box.
  • Behaviour verified on the same patch applied to 2.16.0 (a private instance, sync no): correctness cases above (2000 keys -> FLUSHWAL -> kill -9 -> 2000/2000; FLUSHWAL returns in 11-24 ms, i.e. a real fsync), syscall accounting via strace, and the throughput/latency matrix.
  • A Go integration test in tests/gocase/unit/ would be the natural addition (FLUSHWAL -> OK; FLUSHWAL under disable_wal -> error). Not included in this branch yet; happy to add it if the command shape is accepted.

Notes

  • Apache Kvrocks contribution norms observed: conventional-commit PR title (feat(command): ..., validated by pr-lint), single commit, no CLA required beyond the ASF ICLA where applicable. The repository's issue templates ask for an enhancement issue first ("Search before asking" / "willing to submit a PR"); the operator may want to open one and reference it from the PR. The commit carries a Co-Authored-By line disclosing AI assistance per the project's AI-assisted contribution guidelines.

Expose rocksdb::DB::FlushWAL(/*sync=*/true) as a FLUSHWAL command. With
rocksdb.write_options.sync=no, writes are appended to the WAL buffer per
command but only made durable by background flushes; nothing in the
command surface lets a client force that boundary. FLUSHWAL flushes the
WAL buffer to the file and fsyncs it, making every write accepted before
the call durable across a process or machine crash.

Motivation: group-commit durability. An application tier running with
sync=no can append fast, hold acks, and issue one FLUSHWAL per T ms (or
per N writes), then release every ack accepted before the flush -
ack=fsynced semantics at a fraction of the per-write fsync cost. On a
disk with an ~12 ms fsync floor, a Lua append script doing 4 writes per
call moves from ~20 appends/s (sync=yes, one fsync per write batch) to
~2,400 appends/s with a 5 ms flush window, with the loss window on power
failure bounded by the flush interval (process crash alone loses
nothing: the WAL is still written per command).

The command rejects under rocksdb.write_options.disable_wal (there is no
WAL to flush) and surfaces any RocksDB error. It performs no keyspace
write and needs no exclusivity; registered read-only admin to match the
other storage-maintenance commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Hi @pipinstall1,

Thank you for your pull request. Please review our Contributing Guide.

Please make sure you understand your changes and explain your reasoning in this pull request. Low-quality pull requests may be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a FLUSHWAL command (RocksDB FlushWAL(sync=true)) for group durability with write_options.sync=no

2 participants