UBLK
Write Linux block-device targets in Ruby. UBLK uses a C extension, liburing,
and the kernel's UBLK_F_USER_COPY interface so target implementations only
exchange Ruby strings.
[!WARNING] This is experimental. Never use it for a root filesystem, swap, or data you cannot recreate. A Ruby GC pause stops device I/O, and a crashed server makes outstanding I/O fail unless recovery was enabled.
Requirements
- Linux 6.6 or newer with
CONFIG_BLK_DEV_UBLK /dev/ublk-controland normallyCAP_SYS_ADMIN- Ruby 3.2 or newer
- liburing headers (
liburing-devon Debian/Ubuntu,liburing-develon Fedora)
The gem deliberately fails to build without liburing. Once installed,
require "ublk" remains safe on unsupported hosts and UBLK.supported?
reports whether the running kernel exposes USER_COPY.
bundle install
bundle exec rake compile
RAM disk
require "ublk"
class RamDisk < UBLK::Target
def initialize(size)
@data = "\0".b * size
super(size:)
end
def read(offset, length) = @data.byteslice(offset, length)
def write(offset, data)
@data[offset, data.bytesize] = data
data.bytesize
end
end
device = UBLK::Device.create(RamDisk.new(256 * 1024 * 1024))
puts device.path
device.run
run blocks until another thread calls stop or the process receives an
interrupt. For a background server, call start, use device.path, then call
delete. Created devices are also deleted by an at_exit hook.
Targets must implement read and write. flush succeeds by default;
discard and write_zeroes default to EOPNOTSUPP. Raising an Errno::*
exception returns that errno to the kernel; other exceptions become EIO.
Device.create locks current and future memory by default to avoid a paging
deadlock. Pass mlock: false only for disposable development devices. Multiple
hardware queues use one Ruby thread each, but callbacks remain serialized by
the GVL.
See examples/ for RAM, file-backed, HTTP read-only, encrypted, tracing, and
fault-injection targets.
Recovery
Create with recovery: true, then reconnect after a server restart:
device = UBLK::Device.recover(target, id: 0)
device.run
Outstanding requests fail during this v1 recovery mode; they are not reissued.
Unsupported environments
- macOS and Windows: no Linux ublk driver
- Docker Desktop: its VM normally does not expose
/dev/ublk-control - containers without the host ublk device and the required capability
- WSL2 kernels without
CONFIG_BLK_DEV_UBLK - Linux before 6.6: no required USER_COPY interface
- standard GitHub-hosted jobs: no usable privileged ublk device
Tests
bundle exec rake compile
bundle exec rake test:unit
bundle exec rbs validate
The destructive system suite only runs when explicitly enabled inside a VM. It covers 64 MiB raw reads, maximum-I/O boundary crossing, ext4 and fsck, flush/discard callbacks, errno propagation, device deletion, SIGKILL recovery, four-way verified fio, a one-minute RSS leak check, and 1,000 control opens.
tools/vm/run.sh v6.6
tools/vm/run.sh v6.12
Run 4K random-read, random-write, and sequential-read fio workloads in the same VM:
UBLK_BENCH=1 bundle exec ruby tools/bench.rb
UBLK_BENCH=1 UBLK_BENCH_GC_STRESS=1 bundle exec ruby tools/bench.rb
UBLK_BENCH=1 UBLK_BENCH_DEVICE=/dev/ublkb0 bundle exec ruby tools/bench.rb
The last form measures an externally started device, such as ublksrv's C
loop target, with the identical fio arguments. fio's JSON includes IOPS and
p99 latency. No hardware-independent number is claimed: CPU, Ruby, GC, and VM
acceleration materially change the result, and Ruby callbacks remain bounded
by the GVL.
Reference benchmark
One reproducible comparison was run on Linux 6.12.0 under QEMU TCG (2 vCPUs,
1.5 GiB RAM), using Ruby 3.2, fio 3.36, and upstream ublksrv 1.6.1
(abbfea2b5918). Both loop targets used a 256 MiB sparse file on /tmp, one
queue of depth 128, and buffered backing-file I/O. fio used its io_uring engine,
4 KiB direct I/O, iodepth 32, and one 30-second run per workload.
| Target | Workload | IOPS | completion p99 |
|---|---|---|---|
| Ruby loop | random read | 33,774 | 2.900 ms |
| Ruby loop | random write | 33,896 | 2.376 ms |
| Ruby loop | sequential read | 26,801 | 3.850 ms |
| C ublksrv loop | random read | 64,213 | 0.938 ms |
| C ublksrv loop | random write | 56,456 | 1.090 ms |
| C ublksrv loop | sequential read | 62,011 | 0.987 ms |
Ruby loop, GC.stress |
random read | 9.72 | 6.610 s |
Ruby loop, GC.stress |
random write | 12.61 | 4.178 s |
Ruby loop, GC.stress |
sequential read | 7.42 | 7.952 s |
These are diagnostic VM results, not production performance claims. In this
run the C loop target delivered roughly 1.7–2.3 times the IOPS of normal Ruby,
while GC.stress demonstrated that stop-the-world GC can make latency
unacceptable for block storage.
License
MIT