feat(eloq): key module registry by type and add a configurable visit order - #27
Open
liangjchen wants to merge 2 commits into
Open
feat(eloq): key module registry by type and add a configurable visit order#27liangjchen wants to merge 2 commits into
liangjchen wants to merge 2 commits into
Conversation
…order register_module() assigned the first free slot and unregister_module() compacted the array, so a module's index depended on registration order and shifted whenever another module unregistered. EloqStore restarts once during normal startup, which renumbered TxService underneath it: the registry settled at ring/txservice/eloqstore only after that shuffle. Indices were therefore unusable for addressing a module. EloqModule now declares a ModuleType, and the enumerator is the slot: kRing = 0, kTxService = 1, kEloqStore = 2 Registration writes its own slot and unregistration clears it in place, so a slot always denotes the same kind of module -- across a module being absent (RingModule without io_uring) and across a module restarting. Slots may therefore be sparse, which is what makes them stable. On top of that, --module_visit_order takes module names giving the visit order of one ProcessModulesTask() pass, e.g. --module_visit_order=ring,eloqstore,txservice,eloqstore A name may repeat, which drives that module more than once per pass. The order is resolved once in TaskGroup::init() into an inline fixed array, because ProcessModulesTask() runs millions of times a second and must not pay a guard check or a heap indirection per pass. An unknown name aborts at startup rather than silently visiting the wrong module. Empty keeps the previous behavior: every registered module once, in slot order. Sparse slots also mean a module can no longer be located by counting, so ExtThdStart() for modules new to a worker moves into CheckAndUpdateModules(), which already diffs the module set and runs only when that set changes. The old loop indexed registered_modules_ with a population count and would skip -- or dereference past -- an empty slot; EloqStore binds its per-thread shard in ExtThdStart(), so skipping it left the thread-local shard null and io_uring initialization aborted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
| enum class ModuleType : size_t { | ||
| kRing = 0, | ||
| kTxService = 1, | ||
| kEloqStore = 2, |
Collaborator
There was a problem hiding this comment.
Also need a kMongo module
…ice, EloqStore Benchmarking the previous default (every module once, in slot order) against driving EloqStore twice per pass showed the latter consistently ahead on read-heavy load -- separated from run-to-run spread, with the margin growing as connection count rises -- and no worse on a mixed read/write load, where only the far tail moves. A shard accumulates completed IO faster than one visit per pass can drain, so the second visit shortens the interval between an IO completing and the shard draining it. Make that the built-in default rather than something each deployment has to discover and opt into. --module_visit_order still overrides it, and "ring,txservice,eloqstore" restores one visit each. Naming a module that is not registered stays harmless: slots are keyed by module type, so an absent module leaves its slot empty rather than shifting the others, and the visit loop skips it. A deployment on another storage backend therefore visits Ring and TxService and pays a null check for the two EloqStore entries; the same holds for Ring without io_uring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Background
This change comes out of an extended benchmarking effort on EloqKV: driving read-only and 1:1 read/write workload over 10M keys against a memory-limited node, measuring where throughput and tail latency actually go, and turning what we learned into targeted optimizations. Over that effort throughput improved several-fold and the tail latency that motivated the work fell from seconds to milliseconds. This PR is the brpc half of that work; companion PRs land in eloqstore and tx_service.
The optimization
With
ELOQ_MODULE_ENABLED, a brpc worker does not just run bthreads — it also drives the ring listener, the tx service, and EloqStore as modules, cooperatively multiplexed on its own stack inProcessModulesTask(). Whatever that worker is currently doing, the other roles wait. How often each module is visited per pass is therefore a scheduling decision, and until now it was fixed at "each module once, in whatever order they happened to register".That order is not optimal. Under read-heavy load the shard accumulates completed IO faster than one visit per pass can drain, so completions wait for the next full rotation. Visiting EloqStore twice per pass — once before and once after the tx service — shortens the gap between an IO completing and the shard draining it.
--module_visit_ordermakes the order configurable, so this is tunable per deployment rather than baked in. Measured on our internal benchmark rig with--module_visit_order=ring,eloqstore,txservice,eloqstoreagainst the default, alternating between the two configurations and driving load from a separate client host, the ordering is a consistent win on read-heavy traffic:Change details
Module identity.
register_module()previously assigned the first free slot andunregister_module()compacted the array, so a module's index depended on registration order and shifted whenever another module unregistered. EloqStore restarts once during normal startup, which renumbered the tx service underneath it — the registry only settled at ring/txservice/eloqstore after that shuffle. Indices were unusable for addressing a module.EloqModulenow declares aModuleType—Ring,TxService,EloqStore—and that type is the module's slot, so every module has a reserved place in the registry. Registration writes its own slot; unregistration clears it in place. A slot therefore always denotes the same kind of module — across a module being absent (RingModulewithout io_uring) and across a module restarting. Slots may be sparse as a result, which is exactly what makes them stable.The flag.
--module_visit_ordertakes module names, e.g.ring,eloqstore,txservice,eloqstore. A name may repeat, which drives that module more than once per pass; naming a module that is not registered is harmless. Empty preserves today's behavior exactly: every registered module once, in slot order. The order is resolved once inTaskGroup::init()into an inlinefixed-size array —
ProcessModulesTask()runs millions of times a second, so reading it must not cost a thread-safe-static guard check or a heap indirection per pass. An unknown name aborts at startup rather than silently visiting the wrong module.One consequence. Sparse slots mean a module can no longer be located by counting.
ExtThdStart()for modules new to a worker moves intoCheckAndUpdateModules(), which already diffs the module set and runs only when that set changes — so this adds nothing to the per-pass path. The old loop indexedregistered_modules_with a population count and would skip, or read past, an empty slot. This is not theoretical:EloqStoreModule::ExtThdStart()binds the worker's shard pointer, so skipping it left that thread-local null and io_uring initialization aborted at startup.