docs/design-docs/design_docs/20260204-channel_exclusive_mode.md
Related Issues and PRs:
In Milvus QueryCoord, the traditional load balancing strategy distributes channels and segments across QueryNodes without strict isolation. This approach can lead to several issues:
Channel Exclusive Mode aims to address these challenges by introducing a channel-centric load balancing strategy with the following goals:
Channel Exclusive Mode is implemented as an extension to Milvus QueryCoord's balancing system. The design follows these key principles:
┌─────────────────────────────────────────────────────────────┐
│ QueryCoord Services │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ReplicaManager │ │
│ │ - Creates replicas with exclusive mode enabled │ │
│ │ - Persists replica state to etcd │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ ↑ │
│ │ create/update │ read │
│ ↓ │ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Replica (Immutable + Mutable) │ │
│ │ - Stores ChannelNodeInfos mapping │ │
│ │ - Manages exclusive node assignments │ │
│ │ - Provides COW semantics for updates │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↑ │ │
│ │ monitor/update │ query │
│ │ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ReplicaObserver │ │
│ │ - Monitors replica state periodically │ │
│ │ - Initializes exclusive metadata dynamically │ │
│ │ - Cleans up RO nodes when not needed │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ChannelLevelScoreBalancer │ │
│ │ - Implements channel-aware load balancing │ │
│ │ - Generates channel/segment movement plans │ │
│ │ - Falls back to ScoreBasedBalancer when needed │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
File: pkg/proto/query_coord.proto
message ChannelNodeInfo {
repeated int64 rw_nodes = 6; // RW nodes exclusively assigned to this channel
}
message Replica {
int64 ID = 1;
int64 collectionID = 2;
repeated int64 nodes = 3; // All RW nodes in this replica
repeated int64 ro_nodes = 5; // Read-only nodes
string resource_group = 4;
map<string, ChannelNodeInfo> channel_node_infos = 6; // Channel → Node mapping
repeated int64 rw_sq_nodes = 7; // Streaming query nodes (RW)
repeated int64 ro_sq_nodes = 8; // Streaming query nodes (RO)
}
Design Rationale:
File: internal/querycoordv2/meta/replica.go
type Replica struct {
replicaPB *querypb.Replica // Protobuf representation with ChannelNodeInfos
rwNodes typeutil.UniqueSet // RW nodes (non-streaming)
roNodes typeutil.UniqueSet // RO nodes (non-streaming)
rwSQNodes typeutil.UniqueSet // RW streaming query nodes
roSQNodes typeutil.UniqueSet // RO streaming query nodes
loadPriority commonpb.LoadPriority
}
Key Methods:
GetChannelRWNodes(channelName string) []int64: Returns the exclusive RW nodes for a channelIsChannelExclusiveModeEnabled() bool: Checks if ChannelNodeInfos are initializedCopyForWrite() *mutableReplica: Creates a mutable copy for modificationsDesign Rationale:
type mutableReplica struct {
*Replica
exclusiveRWNodeToChannel map[int64]string // Reverse mapping: Node → Channel
}
Key Methods:
TryEnableChannelExclusiveMode(channelNames ...string): Initializes channel exclusive modetryBalanceNodeForChannel(): Rebalances node assignments across channelsDisableChannelExclusiveMode(): Clears ChannelNodeInfosIntoReplica() *Replica: Converts mutable copy back to immutable replicaDesign Rationale:
exclusiveRWNodeToChannel enables O(1) checks for node availabilityIntoReplica() enforces intentional conversion back to immutable stateChannel exclusive mode is activated when all of the following conditions are met:
queryCoord.balancer is set to "ChannelLevelScoreBalancer"replica.RWNodesCount() >= len(channels) * channelExclusiveNodeFactor
channelExclusiveNodeFactor defaults to 3 (at least 3 RW QueryNodes per channel)Check Logic (internal/querycoordv2/meta/replica.go):
func (replica *mutableReplica) shouldEnableChannelExclusiveMode(channelInfos map[string]*querypb.ChannelNodeInfo) bool {
balancePolicy := paramtable.Get().QueryCoordCfg.Balancer.GetValue()
channelExclusiveFactor := paramtable.Get().QueryCoordCfg.ChannelExclusiveNodeFactor.GetAsInt()
return balancePolicy == ChannelLevelScoreBalancerName &&
replica.RWNodesCount() >= len(channelInfos)*channelExclusiveFactor
}
Scenario 1: New Collection Load
User.LoadCollection(collection, replicaNum=2)
↓
QueryCoord.LoadCollection()
↓
ReplicaManager.Spawn(collection, replicaNum, channels)
├─ Check: Is ChannelLevelScoreBalancer enabled?
├─ If YES:
│ ├─ Create Replica with default nodes
│ ├─ mutableReplica.TryEnableChannelExclusiveMode(channels...)
│ │ ├─ Initialize ChannelNodeInfos for all channels (empty node lists)
│ │ └─ Call tryBalanceNodeForChannel() to assign nodes
│ └─ Persist replica with ChannelNodeInfos
└─ If NO: Create replica without ChannelNodeInfos
Scenario 2: Dynamic Enablement
ReplicaObserver.checkNodesInReplica() [node-change event or periodic timeout]
↓
For each replica:
├─ Check: Is ChannelLevelScoreBalancer enabled?
├─ Check: Is exclusive mode already enabled?
├─ If should enable but not yet enabled:
│ ├─ Load all collection channels from TargetManager
│ ├─ mutableReplica.TryEnableChannelExclusiveMode(channels...)
│ └─ Persist updated replica
└─ Continue monitoring
Location: internal/querycoordv2/meta/replica.go
Goal: Distribute nodes evenly across channels
func calculateOptimalAssignments(totalNodes, channelCount int) map[string]int {
baseNodes := totalNodes / channelCount // Base allocation per channel
extraNodes := totalNodes % channelCount // Remaining nodes to distribute
assignments := make(map[string]int)
for i, channelName := range sortedChannels {
assignments[channelName] = baseNodes
if i < extraNodes {
assignments[channelName]++ // Distribute extra nodes to first few channels
}
}
return assignments
}
Example:
[3, 2, 2] (first channel gets extra node)[2, 2, 2] (even distribution)Two-Phase Process:
Phase 1: Release Excess Nodes
func releaseExcessNodes(targetAssignments map[string]int) {
for _, channelName := range sortedChannels {
currentNodes := r.GetChannelRWNodes(channelName)
targetCount := targetAssignments[channelName]
if len(currentNodes) > targetCount {
// Keep first targetCount nodes, release the rest
nodesToKeep := currentNodes[:targetCount]
nodesToRelease := currentNodes[targetCount:]
for _, nodeID := range nodesToRelease {
delete(r.exclusiveRWNodeToChannel, nodeID)
}
r.replicaPB.ChannelNodeInfos[channelName].RwNodes = nodesToKeep
}
}
}
Phase 2: Allocate Nodes to Under-Allocated Channels
func allocateInsufficientNodes(targetAssignments map[string]int) {
availableNodes := r.getAvailableNodes() // Nodes not exclusively assigned
for _, channelName := range sortedChannels {
currentNodes := r.GetChannelRWNodes(channelName)
targetCount := targetAssignments[channelName]
if len(currentNodes) < targetCount {
neededCount := targetCount - len(currentNodes)
selectedNodes := allocateNodesFromPool(availableNodes, neededCount)
for _, nodeID := range selectedNodes {
r.replicaPB.ChannelNodeInfos[channelName].RwNodes =
append(r.replicaPB.ChannelNodeInfos[channelName].RwNodes, nodeID)
r.exclusiveRWNodeToChannel[nodeID] = channelName
}
// Remove allocated nodes from available pool
availableNodes = removeNodes(availableNodes, selectedNodes)
}
}
}
Allocation Order: Channels are sorted by current node count in descending order. Channels with more current nodes are considered first when assigning the remainder and when releasing excess nodes; newly allocated nodes come from the unassigned RW node pool.
The ChannelLevelScoreBalancer extends the traditional ScoreBasedBalancer with channel-awareness:
ScoreBasedBalancer and delegates to it when exclusivity is not feasibleFile: internal/querycoordv2/balance/channel_level_score_balancer.go
type ChannelLevelScoreBalancer struct {
*ScoreBasedBalancer // Embedded for fallback behavior
targetMgr meta.TargetManagerInterface
}
High-Level Flow (channel_level_score_balancer.go:69-160):
BalanceReplica(ctx, replica)
↓
1. Check Streaming Service
├─ If enabled: Call ScoreBasedBalancer.balanceChannels()
└─ Return early if plans generated
↓
2. Check Exclusive Mode Eligibility
├─ For each channel:
│ └─ If GetChannelRWNodes(channel) is empty → exclusive mode disabled
└─ If disabled: Delegate to ScoreBasedBalancer.BalanceReplica() → Return
↓
3. Channel-Aware Balancing (Exclusive Mode Enabled)
For each channel:
├─ Get channel's exclusive RW nodes
├─ Identify outbound nodes (nodes no longer in RW node list)
├─ If outbound nodes exist:
│ ├─ genChannelPlanForOutboundNodes()
│ └─ genSegmentPlanForOutboundNodes()
└─ If no outbound nodes:
├─ If AutoBalanceChannel enabled:
│ └─ genChannelPlan() - balance channel distribution
└─ Else:
└─ genSegmentPlan() - balance segments within channel nodes
↓
4. Return Plans
└─ (segmentPlans, channelPlans)
genChannelPlan)Goal: Distribute channels evenly across online nodes
Location: channel_level_score_balancer.go:272-312
Algorithm:
1. Calculate target: avgChannelsPerNode = totalChannels / onlineNodeCount
2. For each online node:
- If node.channelCount > avgChannelsPerNode + 1:
- Mark as sourceNode (overloaded)
- If node.channelCount < avgChannelsPerNode:
- Mark as targetNode (under-loaded)
3. For each sourceNode:
- Select channels to move (excess channels)
- Assign to least-loaded targetNode
- Create ChannelPlan(channel, sourceNode → targetNode)
Example:
[4, 1, 1] → Move 2 channels from node 0 → nodes 1 and 2[2, 2, 2]genSegmentPlan)Goal: Balance segment load within a channel's exclusive node group
Location: channel_level_score_balancer.go:200-270
Algorithm:
1. Get channel's exclusive RW nodes
2. Convert nodes to NodeItems with scores (resource-based)
3. Identify source nodes (high score) and target nodes (low score)
4. For each segment on source node:
- Skip redundant segments (already on target)
- Create SegmentPlan(segment, sourceNode → targetNode)
- Update node scores
5. Return plans
Node Score Calculation (inherited from ScoreBasedBalancer):
score = α * CPU% + β * Memory% + γ * DelegatorScore
Outbound Node: A node that was previously assigned to a channel but is no longer in the channel's RW node list (due to node removal or rebalancing)
genChannelPlanForOutboundNodes() (channel_level_score_balancer.go:162-176)
func (b *ChannelLevelScoreBalancer) genChannelPlanForOutboundNodes(
ctx context.Context,
replica *meta.Replica,
channelName string,
channelRwNodes []int64,
channelPlans []ChannelAssignPlan,
) []ChannelAssignPlan {
// Find nodes with this channel that are not in channelRwNodes
currentAssignment := b.dist.ChannelDistManager.GetByCollectionAndFilter(...)
for _, view := range currentAssignment {
if !lo.Contains(channelRwNodes, view.Node) {
// Node has channel but shouldn't → Move away
targetNode := selectLeastLoadedNode(channelRwNodes)
plan := ChannelAssignPlan{
Channel: view.Channel,
From: view.Node,
To: targetNode,
Replica: replica,
}
channelPlans = append(channelPlans, plan)
}
}
return channelPlans
}
genSegmentPlanForOutboundNodes() (channel_level_score_balancer.go:178-195)
Similar logic but for segments:
1. Find segments on outbound nodes
2. Move to nodes in channelRwNodes
3. Create SegmentPlan for each segment
File: pkg/util/paramtable/component_param.go
| Parameter | Config Key | Default | Version | Description |
|---|---|---|---|---|
Balancer | queryCoord.balancer | "ChannelLevelScoreBalancer" | 2.4+ | Balancer type selection |
ChannelExclusiveNodeFactor | queryCoord.channelExclusiveNodeFactor | "3" | 2.4.2+ | Minimum RW QueryNodes per channel to enable exclusive mode |
AutoBalanceChannel | queryCoord.autoBalanceChannel | true | 2.3.4 | Whether to auto-rebalance channels (inherited from parent) |
queryCoord:
balancer: ChannelLevelScoreBalancer
channelExclusiveNodeFactor: 3
Behavior:
queryCoord:
balancer: ChannelLevelScoreBalancer
channelExclusiveNodeFactor: 2
Behavior:
queryCoord:
balancer: ScoreBasedBalancer # Use traditional balancer
Behavior:
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: User loads collection │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User: LoadCollection( │
│ collection = "product_catalog", │
│ replicaNum = 2, │
│ channels = ["channel_0", "channel_1", "channel_2"] │
│ ) │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: ReplicaManager.Spawn creates replicas │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Config: queryCoord.balancer = "ChannelLevelScoreBalancer" │
│ Config: queryCoord.channelExclusiveNodeFactor = 3 │
│ │
│ For replica 1: │
│ ├─ Assign nodes: [1, 2, 3, ..., 9] │
│ ├─ Check: 9 nodes >= 3 channels * 3 → Eligible │
│ ├─ TryEnableChannelExclusiveMode(channels...) │
│ │ ├─ Initialize ChannelNodeInfos: │
│ │ │ "channel_0": {rw_nodes: []} │
│ │ │ "channel_1": {rw_nodes: []} │
│ │ │ "channel_2": {rw_nodes: []} │
│ │ └─ Call tryBalanceNodeForChannel() │
│ │ ├─ Calculate assignments: [3, 3, 3] │
│ │ │ (9 nodes / 3 channels = 3 base + 0 extra) │
│ │ └─ Assign nodes: │
│ │ "channel_0": {rw_nodes: [1, 2, 3]} │
│ │ "channel_1": {rw_nodes: [4, 5, 6]} │
│ │ "channel_2": {rw_nodes: [7, 8, 9]} │
│ └─ Persist replica to etcd │
│ │
│ For replica 2: (same logic with different nodes) │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: QueryCoord balancer runs (periodic task) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ChannelLevelScoreBalancer.BalanceReplica(replica1) │
│ ↓ │
│ Check: Is exclusive mode enabled? │
│ → YES (ChannelNodeInfos exist) │
│ ↓ │
│ For "channel_0": │
│ ├─ RW nodes: [1, 2, 3] │
│ ├─ Check outbound nodes: None │
│ └─ genSegmentPlan(segments, nodes=[1,2,3]) │
│ → Move segments to balance load on nodes 1, 2, 3 │
│ ↓ │
│ For "channel_1": │
│ ├─ RW nodes: [4, 5, 6] │
│ ├─ Check outbound nodes: None │
│ └─ genSegmentPlan(segments, nodes=[4,5,6]) │
│ ↓ │
│ For "channel_2": │
│ ├─ RW nodes: [7, 8, 9] │
│ ├─ Check outbound nodes: None │
│ └─ genSegmentPlan(segments, nodes=[7,8,9]) │
│ ↓ │
│ Return plans: (segmentPlans, channelPlans) │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: Scheduler executes plans │
├─────────────────────────────────────────────────────────────────┤
│ │
│ For each SegmentPlan: │
│ └─ Move segment from source node → target node │
│ │
│ For each ChannelPlan: │
│ └─ Reassign channel from source node → target node │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 5: ReplicaObserver monitors state (continuous) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ On node-change event or checkNodeInReplicaInterval timeout: │
│ ├─ Check if exclusive metadata should be initialized │
│ │ → Already initialized, no action │
│ ├─ Check RO nodes for cleanup │
│ │ → None present │
│ └─ Continue monitoring │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Initial State: │
│ channel_0: [1, 2, 3] │
│ channel_1: [4, 5, 6] │
│ channel_2: [7, 8, 9] │
│ │
│ Event: Node 2 goes offline │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: ReplicaManager detects node failure │
├─────────────────────────────────────────────────────────────────┤
│ │
│ RemoveNode(replicaID=1, nodeID=2) │
│ ├─ Remove node 2 from replica.rwNodes │
│ ├─ Trigger tryBalanceNodeForChannel() │
│ │ ├─ Check: 8 nodes < 3 channels * 3 → Not eligible │
│ │ └─ Disable channel exclusive node assignments │
│ │ channel_0: [] │
│ │ channel_1: [] │
│ │ channel_2: [] │
│ └─ Persist updated replica │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: ChannelLevelScoreBalancer.BalanceReplica │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ChannelLevelScoreBalancer.BalanceReplica(replica1) │
│ ├─ Sees at least one channel with no RW nodes │
│ └─ Delegates to ScoreBasedBalancer.BalanceReplica() │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: Scheduler executes movement plans │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Execute ScoreBasedBalancer plans │
│ │
│ Final State: │
│ ChannelNodeInfos remain present but RW node lists are empty │
│ Segment and channel movement follows ScoreBasedBalancer │
│ │
└─────────────────────────────────────────────────────────────────┘
⚠️ WARNING: Performance Impact
Switching channel exclusive mode can trigger channel and segment rebalancing. The actual impact depends on collection size, segment size, index size, QueryNode capacity, and the number of channels that need to move. Operators should expect elevated CPU, memory, disk I/O, network traffic, and query latency during the transition, but this document does not provide a fixed percentage or duration guarantee.
Recommendation: Perform during low-traffic maintenance windows, validate the impact in staging, and monitor cluster resources closely.
For Existing Clusters:
Check Prerequisites:
# Ensure sufficient nodes
# Required: totalNodes >= channels * channelExclusiveNodeFactor
Update Configuration (Runtime Configuration Change):
queryCoord:
balancer: ChannelLevelScoreBalancer
channelExclusiveNodeFactor: 3 # Default safety threshold
Wait for Automatic Enablement:
Balancer parameter is runtime-refreshable (refreshable:"true")queryCoord.checkNodeInReplicaInterval times outqueryCoord.checkNodeInReplicaInterval is 60 seconds, so metadata initialization is not guaranteed to happen within 1 secondMonitor Transition:
# Check replica state
# ReplicaObserver will enable exclusive metadata on its next node-change event
# or checkNodeInReplicaInterval timeout
# Check logs
kubectl logs -f milvus-querycoord | grep "channel exclusive mode"
How It Works:
ReplicaObserver.checkNodesInReplica() runs after QueryNode membership changes or after queryCoord.checkNodeInReplicaInterval times outqueryCoord.balancer configurationChannelLevelScoreBalancer and replica doesn't have exclusive mode:
mutableReplica.TryEnableChannelExclusiveMode()Expected Behavior:
ScoreBasedBalancerRollback Steps:
Update Configuration (Runtime Configuration Change):
queryCoord:
balancer: ScoreBasedBalancer # Switch to traditional balancer
Wait for ScoreBasedBalancer to take over:
ScoreBasedBalancer after the refreshed configuration is observedChannelNodeInfos metadata is not synchronously deleted by ReplicaObserverVerify Rollback:
# Check that queryCoord.balancer is ScoreBasedBalancer
# ChannelNodeInfos may remain in replica metadata, but ScoreBasedBalancer ignores them
How It Works:
ScoreBasedBalancer when queryCoord.balancer is set to ScoreBasedBalancerReplicaObserver does not have a synchronous disable branch that clears all ChannelNodeInfostryBalanceNodeForChannel() checks shouldEnableChannelExclusiveMode() when it is invoked by replica update paths:
false if balancer is not ChannelLevelScoreBalancerfalse if node count is insufficientDisableChannelExclusiveMode() to reset per-channel RW node listsData Consistency:
ChannelNodeInfos only records balancing metadataScoreBasedBalancer takes over balancing responsibility after the refreshed configuration is observedWhen upgrading from a version without channel exclusive mode to a version with it enabled by default, the upgrade process involves special considerations.
Upgrade Process:
Rolling Upgrade Starts:
StoppingBalancer Takes Over:
Upgrade Completes:
What Happens to Channel Exclusive Mode:
Initial State (before upgrade):
Collection A loaded, channel exclusive mode not enabled
Channels distributed across nodes without isolation
During Upgrade (node-by-node):
Step 1: Node 1 becomes RO
└─ StoppingBalancer: Move channels from node 1 → any RW node (2,3,4,5)
❌ May assign channel_0 to node 5 (not in exclusive list)
Step 2: Node 2 becomes RO
└─ StoppingBalancer: Move channels from node 2 → any RW node (3,4,5,6)
❌ Distribution further deviates from exclusive mode
... (continues for all nodes)
After Upgrade (all nodes upgraded):
Step 1: ReplicaObserver detects replicas without ChannelNodeInfos
└─ Calls TryEnableChannelExclusiveMode() for all replicas
✅ Initializes ChannelNodeInfos for each channel
Step 2: ChannelLevelScoreBalancer runs
└─ Detects channels on "outbound nodes" (nodes not in ChannelNodeInfos)
└─ Generates plans to move channels to correct exclusive nodes
└─ Gradually restores exclusive mode distribution
Timeline and Resource Impact:
| Phase | Duration | Balancer | Resource Impact | Channel Distribution |
|---|---|---|---|---|
| Pre-Upgrade | Stable | ScoreBasedBalancer | Normal | Non-exclusive, evenly distributed |
| During Upgrade | Depends on cluster size | StoppingBalancer | Potentially high | May break exclusive mode |
| Post-Upgrade (Recovery) | Depends on cluster size | ChannelLevelScoreBalancer | Potentially high | Gradually restoring exclusive mode |
| Stable | Ongoing | ChannelLevelScoreBalancer | Normal | Exclusive placement restored after balancing |
Important Notes:
⚠️ Expected Behavior During Upgrade:
- Channel exclusive mode distribution will be temporarily violated
- This is by design - StoppingBalancer prioritizes service availability over isolation
- Exclusive placement should be restored by normal balancing after upgrade completes, assuming enough RW nodes are available
- Do NOT manually intervene during the upgrade process
⚠️ Resource Planning:
- Upgrade causes two waves of rebalancing:
- During upgrade (StoppingBalancer evacuation)
- After upgrade (ChannelLevelScoreBalancer restoration)
- Total impact time depends on cluster size and the amount of data that must move
- Plan for sustained high CPU/memory/I/O during this period
This section outlines the testing strategy for Channel Exclusive Mode, covering checked-in unit tests, integration tests, system tests, and production verification.
Replica Logic Tests (internal/querycoordv2/meta/replica_test.go):
| Test Case | Coverage | Status |
|---|---|---|
TestChannelExclusiveMode | Basic channel-exclusive metadata behavior | Checked in |
TestTryBalanceNodeForChannel* | Empty, disabled, insufficient-node, balanced, unbalanced, and extra-node cases | Checked in |
TestShouldEnableChannelExclusiveMode | Activation condition checks | Checked in |
TestClearChannelNodeInfos | Resets per-channel RW node lists | Checked in |
TestGetSortedChannelsByNodeCount | Channel sort order | Checked in |
TestCalculateOptimalAssignments | Optimal assignment calculation, including remainder distribution | Checked in |
ChannelLevelScoreBalancer Tests (internal/querycoordv2/balance/channel_level_score_balancer_test.go):
| Test Case | Coverage | Status |
|---|---|---|
TestExclusiveChannelBalance_ChannelOutBound | Moves channels away from nodes outside the channel RW node list | Checked in |
TestExclusiveChannelBalance_SegmentOutbound | Moves segments away from nodes outside the channel RW node list | Checked in |
TestExclusiveChannelBalance_SegmentUnbalance | Balances segments within a channel's RW node list | Checked in |
| Existing score-based balance tests | Non-exclusive and fallback score-based behavior | Checked in |
ReplicaObserver Tests (internal/querycoordv2/observers/replica_observer_test.go):
| Test Case | Coverage | Status |
|---|---|---|
TestCheckNodesInReplica | Replica recovery, RO node cleanup, and channel-exclusive metadata registration during observer checks | Checked in |
TestCheckSQnodesInReplica | Streaming QueryNode replica cleanup | Checked in |
End-to-End Scenarios:
| Test File | Coverage | Status |
|---|---|---|
tests/integration/balance/channel_exclusive_balance_test.go | Go integration scenarios for channel-exclusive balance | Checked in but skipped until https://github.com/milvus-io/milvus/issues/42966 is fixed |
tests/python_client/resource_group/test_channel_exclusive_balance.py | Resource-group channel-exclusive balance scenarios, including QueryNode scale-up/down, k changes, single-channel collections, and search performance | Checked in |
System tests should cover the following scenarios before changing the default in a production branch:
| Test | Configuration | Metrics to Monitor | Status |
|---|---|---|---|
| Sustained Query Load | Multiple collections and channels with exclusive mode enabled | QPS, latency, CPU, memory, segment load latency | Recommended |
| Node Failure During Load | Remove QueryNodes while exclusive mode is active | Query success rate, rebalancing time, channel movement count | Recommended |
| Mode Transition Under Load | Switch between ScoreBasedBalancer and ChannelLevelScoreBalancer | QPS degradation, recovery time, task backlog | Recommended |
| Skewed Channel Load | Collections whose channels have uneven row counts or query rates | Per-node memory, per-channel latency, task movement volume | Recommended |
| Many Small Collections | Many collections with one or few channels each | Global QueryNode utilization and balance plan volume | Recommended |
Pre-Deployment:
queryCoord.balancer configuration is set correctlynodes >= channels * channelExclusiveNodeFactorScoreBasedBalancer)During Deployment:
Post-Deployment:
Rolling Upgrade Verification:
Critical Paths to Test:
Balancing Logic:
State Consistency:
Fallback Behavior:
ScoreBasedBalancer takes over after the refreshed configuration is observedConfiguration Changes:
queryCoord.checkNodeInReplicaIntervalThis design does not include checked-in benchmark results. Before enabling this as a default, collect benchmark data for:
ScoreBasedBalancer QPS and latency versus ChannelLevelScoreBalancerChannel Exclusive Mode enhances Milvus's query coordination by introducing channel-level resource isolation. Each channel is assigned to a dedicated set of QueryNodes, preventing resource contention and improving performance predictability.
Key Advantages:
Important Considerations:
totalNodes >= channels * channelExclusiveNodeFactorProduction Readiness: Channel Exclusive Mode should be enabled with adequate node resources, staging validation, and a rollback plan. The design balances isolation benefits with operational simplicity, but production readiness depends on workload shape, channel skew, and the measured cost of rebalancing in the target environment.
internal/querycoordv2/meta/replica.go - Core replica logicinternal/querycoordv2/meta/replica_manager.go - Replica lifecycle managementinternal/querycoordv2/balance/channel_level_score_balancer.go - Main balancer implementationinternal/querycoordv2/observers/replica_observer.go - State monitoring and enforcementtests/integration/balance/channel_exclusive_balance_test.go - Integration testspkg/util/paramtable/component_param.go - Configuration parametersconfigs/milvus.yaml - Default configuration filequeryCoord.checkNodeInReplicaIntervalDocument Version: 1.1 (with Test Plan and Verification) Date: 2026-02-04 Author: weiliu1031 Status: Ready for Review Last Updated: 2026-02-04 (Added comprehensive test plan and production verification checklist)