Optimize capacity manager to use atomic SQL updates instead of row-level locks - #14016
Draft
sb-abhish3k wants to merge 3 commits into
Draft
Optimize capacity manager to use atomic SQL updates instead of row-level locks#14016sb-abhish3k wants to merge 3 commits into
sb-abhish3k wants to merge 3 commits into
Conversation
… eliminate N+1 queries updateCapacityForHost called listDetailsKeyPairs per VM (50-100+ round-trips per host). Now batch-loads all VM details in a single WHERE vm_id IN (...) query before the loops. - Add listDetailsKeyPairs(List<Long>, List<String>) to ResourceDetailsDao/DaoBase - Extract VM_DETAIL_KEYS_FOR_CAPACITY constant, add batchGetVmDetailsForCapacityCalculation - Replace per-VM getVmDetailsForCapacityCalculation with map lookup in both loops - Add test for mixed static/dynamic offerings verifying batch path and capacity math
Author
|
@blueorangutan package |
|
@sb-abhish3k a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 4.22 #14016 +/- ##
============================================
+ Coverage 17.67% 17.82% +0.15%
- Complexity 15792 16016 +224
============================================
Files 5922 5928 +6
Lines 533167 534335 +1168
Branches 65210 65374 +164
============================================
+ Hits 94246 95258 +1012
+ Misses 428276 428268 -8
- Partials 10645 10809 +164
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19039 |
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.
Description
The
op_host_capacitytable experiences severe lock contention during concurrent VM lifecycle operations. Slow query analysis shows:errno 1205(InnoDB lock wait timeout exceeded)SELECT ... FROM op_host_capacity WHERE id = ? FOR UPDATERoot cause:
CapacityManagerImpl.releaseVmCapacity()andallocateVmCapacity()use a lock-read-compute-write pattern viaGenericDaoBase.lockRow(). Each VM operation acquires exclusive row locks on 3 capacity rows (CPU, Memory, CPU Core), then holds those locks while performing Java computation, queryingcluster_detailsfor overcommit ratios, logging, and validating — before finally committing. Any concurrent VM operation targeting the same host queues behind this entire sequence.Fix: Replace the
lockRow()+Transaction.execute()pattern with single-statement atomic SQL UPDATEs that push arithmetic to the database:Six new atomic DAO methods cover all capacity mutation patterns:
decrementUsedCapacity— VM stop/migrate awaydecrementReservedCapacity— release reserved (destroy/expunge)incrementUsedCapacity— VM start/migrate todecrementUsedIncrementReservedCapacity(id, used, reserved, overcommitRatio)— VM stop with reservation (capped at overcommitted total)decrementUsedIncrementReservedCapacity(id, used, reserved)— same, uncapped (for CPU core which has no overcommit)incrementUsedDecrementReservedCapacity— allocate from last hostLock duration reduction: From seconds (full transaction span including cluster_details reads, Java math, logging) to microseconds (single UPDATE statement). InnoDB still acquires an implicit row lock for each UPDATE, but releases it immediately on statement completion.
Behavioral changes from original code:
if (used >= amount)— leaves value unchangedCASE WHEN used >= ? THEN used - ? ELSE used END— same semanticsfromLastHostreserved decrementGREATEST(reserved - ?, 0)floor. Fixes a reserved capacity leak in the original where one insufficient resource blocked all three from being freedallocateVmCapacityupdateCapacityForHost()periodically recalibratesAtomicity guarantees:
The old code relied on explicit
SELECT ... FOR UPDATErow locks held across a multi-statement transaction to ensure correctness. The new code relies on InnoDB's implicit row-level locking within single UPDATE statements. Both are correct, but the lock hold time differs by orders of magnitude.Before — explicit transaction locking:
Lock hold time: seconds (measured avg 25s in production). Three rows locked simultaneously for the entire transaction span. Concurrent VM ops on the same host queue behind this.
After — single-statement atomic updates:
Lock hold time per row: microseconds (single UPDATE statement). Each row locked independently for the minimum possible duration.
How InnoDB guarantees correctness for concurrent updates:
When two sessions concurrently issue
UPDATE op_host_capacity SET used_capacity = used_capacity + ? WHERE id = ?on the same row:used_capacity(post-Session-A), then applies its own incrementThis is guaranteed by InnoDB's locking protocol: an UPDATE always reads the latest committed version of the row, not an MVCC snapshot. Both increments are correctly applied with no lost updates. This behavior is identical in MySQL (5.6+) and MariaDB (10.x+), which both use InnoDB as the default storage engine. All SQL constructs used (
GREATEST,CASE WHEN,CAST ... AS SIGNED,NOW()) are supported since MySQL 4.0+.What changes and what is acceptable:
Single-row correctness (no change): Each UPDATE is atomic and serialized by InnoDB's row lock.
used_capacity = used_capacity + ?cannot lose updates, andCASE WHEN used_capacity >= ? THEN used_capacity - ? ELSE used_capacity ENDprevents negative values. Equivalent to the old Java guards (if (usedCpu >= vmCPU)).Cross-row atomicity (relaxed, acceptable): The old code updated CPU, Memory, and CPU Core in a single transaction — all-or-nothing. The new code uses three independent autocommit statements. If a DB connection dies between UPDATE 1 and UPDATE 2, capacity state is temporarily inconsistent for that host. This is an extremely unlikely failure mode (requires connection loss between two statements milliseconds apart), and
updateCapacityForHost()periodic recalibration self-heals any inconsistency by recomputing capacity from actual VM state.Capacity validation window (slightly wider, acceptable): In
allocateVmCapacity, the capacity check (checkIfHostHasCapacity) now runs before the atomic UPDATE rather than inside the locked transaction. Two VMs could both pass validation before either writes. However, this is the same semantic as the original code — the original also validated against pre-update DB state (thecheckIfHostHasCapacitycall performs its ownfindByHostIdTypeSELECT, which reads uncommitted-to-disk values since the_capacityDao.update()hasn't been called yet within the transaction). The validation window is slightly wider without locks, but capacity accounting is inherently approximate andupdateCapacityForHost()recalibration is the safety net.fromLastHostreserved decrement (improved): The old code had a cross-row invariant (reservedCpu >= cpu && reservedMem >= ram) gating all three decrements — if memory didn't have enough reserved, CPU reserved wasn't freed either, leaking reserved capacity until the next recalibration. The new per-rowGREATEST(reserved - ?, 0)frees each resource independently. This is strictly more correct.Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
Bug Severity
Screenshots (if appropriate):
How Has This Been Tested?
How did you try to break this feature and the system with this change?
9 new unit tests added to
CapacityManagerImplTest(15 total, all pass):testReleaseVmCapacityNullHostReturnsTrue— null host guard clausetestReleaseVmCapacityNullCapacityReturnsFalse— null capacity entry guard clausetestReleaseVmCapacityDecrementUsed— verifiesdecrementUsedCapacitycalled for CPU/Memory/CPU Core with correct amounts whenmoveFromReserved=false, moveToReserved=falsetestReleaseVmCapacityDecrementUsedIncrementReserved— verifies capped variant called with overcommit ratio for CPU/Memory, uncapped variant for CPU Core whenmoveToReserved=truetestReleaseVmCapacityDecrementReserved— verifiesdecrementReservedCapacitycalled whenmoveFromReserved=truetestAllocateVmCapacityNewHost— verifiesincrementUsedCapacitycalled for all three capacity typestestAllocateVmCapacityFromLastHost— verifiesincrementUsedDecrementReservedCapacitycalled for all three capacity typestestAllocateVmCapacityInsufficientThrows— verifiesCloudRuntimeExceptionthrown when host lacks capacitytestAllocateVmCapacityNullCapacityReturnsEarly— verifies no DAO mutation when capacity entries are nullAll tests verify
lockRowis never called, confirming the lock-free atomic path.