Skip to content

Commit 25ac879

Browse files
committed
fix(placement): prevent thundering herd with optimistic resource accounting and shadow state
High concurrency placement requests were causing severe "thundering herd" issues due to stale node metrics. The orchestrator would continuously schedule multiple sandboxes on the same seemingly "empty" node before it could report updated resource usage (the "invisibility gap"). This commit introduces active load prediction and optimistic resource reservation to ensure perfectly balanced placement even during metric reporting intervals. Changes: - fix(placement): factor `InProgress` pending resources into the `BestOfK` scoring calculation to predict expected load. - fix(nodemanager): implement `OptimisticAdd` to immediately reserve resources upon successful placement, bridging the gap before async metric updates arrive. - test(placement): refactor `SimulatedNode` into a `NodeSimulator` interface to support diverse node behavior simulations. - test(placement): introduce `LaggyNode` to simulate real-world scenarios with stale/delayed node metrics. - test(placement): add `BenchmarkPlacementDistribution` to visualize load distribution and verify the elimination of the thundering herd effect under high concurrency. Signed-off-by: MorningTZH <morningtzh@yeah.net>
1 parent 1561b22 commit 25ac879

File tree

4 files changed

+422
-96
lines changed

4 files changed

+422
-96
lines changed

packages/api/internal/orchestrator/nodemanager/node.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,12 @@ func (n *Node) GetClient(ctx context.Context) (*clusters.GRPCClient, context.Con
177177
func (n *Node) IsNomadManaged() bool {
178178
return n.NomadNodeShortID != UnknownNomadNodeShortID
179179
}
180+
181+
func (n *Node) OptimisticAdd(res SandboxResources) {
182+
n.metricsMu.Lock()
183+
defer n.metricsMu.Unlock()
184+
185+
// Directly accumulate to the current metrics view
186+
n.metrics.CpuAllocated += uint32(res.CPUs)
187+
n.metrics.MemoryAllocatedBytes += uint64(res.MiBMemory) * 1024 * 1024 // Note: CpuPercent is difficult to estimate, usually just updating Allocated is sufficient for the scheduling algorithm
188+
}

packages/api/internal/orchestrator/placement/placement.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ func PlaceSandbox(ctx context.Context, algorithm Algorithm, clusterNodes []*node
7979
if err == nil {
8080
node.PlacementMetrics.Success(sbxRequest.GetSandbox().GetSandboxId())
8181

82+
// Optimistic update: assume resources are occupied after successful creation.
83+
// Manually update node.metrics with the newly allocated resources.
84+
// This will be overwritten by the next real Metrics report for auto-correction.
85+
node.OptimisticAdd(nodemanager.SandboxResources{
86+
CPUs: sbxRequest.GetSandbox().GetVcpu(),
87+
MiBMemory: sbxRequest.GetSandbox().GetRamMb(),
88+
})
89+
8290
return node, nil
8391
}
8492

0 commit comments

Comments
 (0)