feat(tcmu): support multi-vCPU backends with Photon v0.9 - #458
Open
liulanzheng wants to merge 3 commits into
Open
liulanzheng wants to merge 3 commits into
liulanzheng wants to merge 3 commits into
Conversation
liulanzheng
force-pushed
the
codex/tcmu-multivcpu
branch
30 times, most recently
from
September 10, 2026 14:06
00d97b7 to
1065a3a
Compare
liulanzheng
force-pushed
the
codex/tcmu-multivcpu
branch
4 times, most recently
from
September 10, 2026 17:43
0735c67 to
788c40b
Compare
liulanzheng
marked this pull request as draft
September 11, 2026 03:28
liulanzheng
force-pushed
the
codex/tcmu-multivcpu
branch
19 times, most recently
from
September 14, 2026 15:06
a6ddfc7 to
04b30d2
Compare
liulanzheng
marked this pull request as ready for review
September 14, 2026 15:47
The read-path statistics belong to the file, not to the vCPU that reads it, so with a multi-vCPU backend every worker serving the same image updates lsmt_io_cnt, lsmt_io_size and the allocated-block counter of the writable index at the same time, and their plain read-modify-writes lose updates. Make all three atomic and update them with relaxed ordering. The writable index itself is worse than a lost counter: it is a std::set that insert() mutates in place, and while pwrite() serialises the writers on m_rw_mtx, the readers never took it. On a single vCPU that was safe only because photon does not preempt and neither lookup() nor insert() yields; across vCPUs a reader walking the tree while another vCPU erases and reinserts a mapping resolves a hole or a foreign mapping, and one that catches a rebalance in progress follows nodes that are no longer on its search path. Take a photon::rwlock on every access to the index of a file: for reading in pread() and seek_data() and around the index snapshots of commit(), close_seal() and flatten(), for writing in index_insert(), and across the whole of restack(), which also reshuffles m_files. m_rw_mtx still comes first and the rwlock second, and readers never take m_rw_mtx, so the two cannot deadlock. The write lock covers the index mutation only, not the data I/O of a write, so appending does not stall the readers; a hybrid writer still rewrites the data of an existing mapping in place outside of it, so a read overlapping a write of the same LBA may return a mix of both, which is what a same-LBA read/write race is. A read-only file has an immutable index, so m_index_mutable stays false there and pread() skips the lock: that is the hot path of an image service, and a lock that is only ever shared still puts every vCPU on one cache line. For the same reason pread() delegates to do_pread(), which also lets the MAX_IO_SIZE splitting take the lock once instead of recursively -- photon's rwlock is not reentrant. Also close the writers that mutated the index with no lock at all: discard() inserted before taking m_rw_mtx, and the sparse and the warp files inserted and appended to the index log without it. Add two tests. multi_vcpu_io_counters reads a sealed layer from four vCPUs and checks the counters against the number of reads. multi_vcpu_concurrent_rw maps a whole 1MB layer, then has two vCPUs rewrite random blocks while two others read them, requiring every block to come back as one consistent version of its own pattern; with the index lock disabled it reads 5 corrupted blocks out of 40000 and fails. Signed-off-by: Lanzheng Liu <liulanzheng@gmail.com>
BaseCompressor held the per-block source and destination pointers of a batch in two member vectors, so two vCPUs compressing or decompressing through the same compressor overwrote each other's batch. Build them in std::arrays on the caller's stack and pass them down to do_compress() and do_decompress() instead, which also lets the QAT path drop the resize it needed once nbatch() started returning the real batch size. Bound the batch at MAX_BATCH (256) so those arrays cannot be overrun, and reject n == 0, which divided dst_buffer_capacity by zero. Signed-off-by: Lanzheng Liu <liulanzheng@gmail.com>
Dispatch TCMU commands across a shared Photon WorkPool and let each vCPU publish its own responses, allow the file cache in multi-vCPU mode, and add a benchmark that compares the backend against the single-vCPU implementation on the same runner. Commands beyond the ones the dispatcher starts locally go onto the pool's own task ring, so the device dispatcher never leaves its vCPU and keeps draining its mailbox: WorkPool spreads the overflow over whichever vCPUs are free, and its RingChannel pays for a wakeup only when a worker is parked. Each task just spawns the handler thread and returns, because the pool runs tasks inline on its own loop. Completions are published in place under a per-device spinlock with the uio notification coalesced by a counter, instead of being forwarded to the device's home vCPU one wakeup per command. libtcmu keeps the reader's cursor (dev->cmd_tail) separate from the response cursor (mb->cmd_tail) and the kernel matches responses by cmd_id, so completions only have to be mutually exclusive and must never run ahead of the reader; the in-flight count is released last because device teardown waits on it before freeing the device. Fan-out is load adaptive: each batch starts up to 8 of its commands on the current vCPU, limited further by that vCPU's idle handler slots, and sends only the rest through async_call for pool-wide distribution. This keeps available local capacity busy without holding overflow on a saturated vCPU. enableThread, which turned on one extra thread and refused the file cache, becomes workpoolSize: the number of vCPUs in the pool, 8 by default. main() now releases everything through DEFER, so the image service, the work pool, the tcmulib context and the main loop are also torn down on the error paths, and in the reverse order of their construction: the device loops stop, tcmulib_close() removes the devices and with them the image files and their photon threads, the workers are joined, the image service goes away, and photon::fini() runs last -- it waits for every photon thread of the main vCPU, so nothing may outlive it. The Release benchmark disables DSA and ISA-L in both builds to isolate the TCMU implementation, and is sized to the hosted runner's 4 vCPUs: the backend's overlaybd.json sets workpoolSize to 3, while fio runs a single job that takes all of its concurrency from iodepth, leaving the fourth CPU to fio and the kernel's LIO loopback path instead of oversubscribing it. It reads a range whose allocated file extents cover at least 95% of it, and gates the IOPS ratio against the single-vCPU baseline: at least 95% through a queue depth of 32, then 2x at 64, 4x at 128 and 8x at 256. The comparison table is published as a check-run annotation because job logs and artifacts require repository authentication. Use signed elapsed time in the TCMU and ublk read retry loops so a backward timestamp does not underflow into a spurious seven-day timeout. Signed-off-by: Lanzheng Liu <liulanzheng@gmail.com>
liulanzheng
force-pushed
the
codex/tcmu-multivcpu
branch
from
September 15, 2026 08:10
04b30d2 to
7712b62
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does / why we need it:
A TCMU device is pinned to a single Photon vCPU today, so one core caps its IOPS however deep the queue gets. This fans each device's commands out over a shared Photon WorkPool and lets any vCPU publish completions in place, under a per-device spinlock with the uio notification coalesced by a counter.
enableThreadis replaced byworkpoolSize(vCPUs in the shared pool, default8); compression batch state is now request-local and the file cache works in multi-vCPU mode.tcmu-performance.ymlre-runs that comparison against the single-vCPU baseline on every push (Release, DSA/ISA-L off in both builds, 4 KiB random reads overlibaio+O_DIRECT, prewarmed file cache with the network blocked, one fio job so all concurrency isiodepth, backend on 3 of the runner's 4 vCPUs) and gates the ratio at >=95% through qd32, then 2x/4x/8x. Latest run:https://github.com/containerd/overlaybd/actions/runs/34945509588
Which issue(s) this PR fixes: none
Please check the following list: