ColocateGroups
Move Type: Group Complexity: O(n × |S| × |G|^k) - can be very large Primary Use: Colocating related groups of objects
Move related groups of objects from hot container to the same scope item. Ensures groups that need to be together are placed in the same region/rack/cluster.
Overview
ColocateGroups (also known as COLOCATE_GROUPS) evaluates moving a related set of objects from different groups to every possible combination of containers in different scope items, ensuring all related groups end up colocated in the same scope item (e.g., same region).
Use when:
- Objects have affinity requirements (must be in same region/rack)
- Moving related groups together (e.g., primary + replicas)
- Need to ensure colocation of object groups
- Have partitions defining related object groups
- Know which groups must be colocated
Avoid when:
- Objects can move independently (use Single)
- Don't have partition/group structure
- Colocation not required
- Problem too large (complexity can explode)
Quick Example
- Python
- C++
# Colocate primary and replicas in same region
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
ColocateGroupsMoveTypeSpec(
partitionName="replica_group",
relatedGroupsList=[
ColocateGroupsMoveTypeRelatedGroupsInfo(
relatedGroups={"primary", "replica1", "replica2"},
),
],
colocationScopeName="region",
),
]
)
)
)
void quickExample() {
// Colocate primary and replicas in same region
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("shard");
solver.setContainerName("server");
// Setup problem with replicas scattered across regions
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"server1", {"shard1_p", "shard2_r2"}},
{"server2", {"shard1_r2"}},
{"server3", {"shard1_r1", "shard2_p"}},
{"server4", {"shard2_r1"}},
});
solver.addScope(
"region",
std::map<std::string, std::string>{
{"server1", "us-east"},
{"server2", "us-east"},
{"server3", "us-west"},
{"server4", "us-west"},
});
solver.addObjectDimension(
"size",
std::map<std::string, double>{
{"shard1_p", 1.0},
{"shard1_r1", 1.0},
{"shard1_r2", 1.0},
{"shard2_p", 1.0},
{"shard2_r1", 1.0},
{"shard2_r2", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-size";
balanceSpec.scope() = "server";
balanceSpec.dimension() = "size";
solver.addGoal(std::move(balanceSpec), 1.0);
// Define partition with replica groups
const std::unordered_map<std::string, std::vector<std::string>>
replicaGroups = {
{"primary", {"shard1_p", "shard2_p"}},
{"replica1", {"shard1_r1", "shard2_r1"}},
{"replica2", {"shard1_r2", "shard2_r2"}},
};
solver.addPartition("replica_group", replicaGroups);
LocalSearchSolverSpec solverSpec;
ColocateGroupsMoveTypeRelatedGroupsInfo relatedInfo;
relatedInfo.relatedGroups() = {"primary", "replica1", "replica2"};
ColocateGroupsMoveTypeSpec colocateSpec;
colocateSpec.partitionName() = "replica_group";
colocateSpec.relatedGroupsList() = {std::move(relatedInfo)};
colocateSpec.colocationScopeName() = "region";
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().colocateGroupsMoveTypeSpec() = colocateSpec;
solver.addSolver(solverSpec);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << std::fixed << std::setprecision(4)
<< *solution.initialObjective()->value() << "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
partitionName | string | Yes | null | Partition defining object groups |
relatedGroupsList | list<RelatedGroupsInfo> | Yes | null | Sets of related groups (must be disjoint) |
colocationScopeName | string | Yes | null | Scope for colocation (e.g., "region") |
colocationScopeItemToGroupToContainers | map | No | null | Valid containers per (group, scope item) |
defaultSampleSize | int | No | null | Sample size to limit move sets |
Parameter Details
partitionName:
- Name of partition defining object groups
- Each object belongs to at most one group in the partition
- Example: "replica_group" partition
relatedGroupsList:
- List of
ColocateGroupsMoveTypeRelatedGroupsInfostructs - Each struct contains:
relatedGroups: Set of group names that must be colocateddestinationScopeItems: Optional specific scope items for this set
- Sets must be disjoint (no group appears in multiple sets)
- Example:
[{relatedGroups: ["primary", "replica1", "replica2"]}]
colocationScopeName:
- Scope in which related groups must be colocated
- Example values: "region", "rack", "cluster"
- Each scope item becomes a potential destination
colocationScopeItemToGroupToContainers:
- Optional map:
scope_item → group → containers - Restricts valid destination containers per group per scope item
- If omitted, all containers in scope item are considered
- Example:
{"region1": {"primary": {"server1", "server2"}}}
defaultSampleSize:
- Limits containers considered per (group, scope item)
- Critical for controlling complexity
- If omitted, all valid containers considered
How It Works
Given a hot container (most broken):
- Select hot object: Pick object from hot container
- Identify hot group: Determine which group the hot object belongs to
- Find related groups: Identify all groups related to the hot group
- Select related objects: Pick one object from each related group in the same scope item
- Choose destination scope: Pick a different scope item
- Select destination containers: Pick valid containers for each object in the new scope
- Evaluate move set: Test moving all objects together to the new containers
- Repeat: Try all hot objects, all destination scopes, all container combinations
- Apply best: Apply the move set that improves objective most
Visual Example
Before colocation move: After colocation to region2:
┌──────────────┐ ┌──────────────┐
│ Region1 │ │ Region1 │
│ Server1 │ │ Server1 │
│ • primary1 ┼─┐ Hot object │ (empty) │
│ Server2 │ │ │ Server2 │
│ • replica1 ┼─┼─┐ Related │ (empty) │
│ Server3 │ │ │ │ Server3 │
│ • replica2 ┼─┼─┼─┐ Related │ (empty) │
└──────────────┘ │ │ │ └──────────────┘
│ │ │
┌──────────────┐ │ │ │ ┌──────────────┐
│ Region2 │ │ │ │ │ Region2 │
│ Server4 │ │ │ │ │ Server4 │
│ (empty) <──┼─┘ │ │ │ • primary1 ┼← Colocated!
│ Server5 │ │ │ │ Server5 │
│ (empty) <──┼───┘ │ │ • replica1 ┼← Colocated!
│ Server6 │ │ │ Server6 │
│ (empty) <──┼─────┘ │ • replica2 ┼← Colocated!
└──────────────┘ └──────────────┘
All related groups move together to the same scope item
Complexity
Moves evaluated: O(n × |S| × |G|^k)
Where:
- n = number of objects in hot container
- |S| = number of colocation scope items
- |G| = number of related groups
- k = valid containers per group per scope item
⚠️ Warning: This complexity can become very large quickly!
Example - Replica colocation:
- Hot container: 100 objects
- Colocation scope items (regions): 3
- Related groups: 3 (primary + 2 replicas)
- Valid containers per group: 10
- Without sampling: 100 × 3 × 10³ = 300,000 move sets
- With defaultSampleSize=5: 100 × 3 × 5³ = 37,500 move sets
Usage Patterns
Basic Replica Colocation
Colocate primary and replicas in same region:
- Python
- C++
# Ensure all replicas of a shard are in the same region
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
ColocateGroupsMoveTypeSpec(
partitionName="replica_type",
relatedGroupsList=[
ColocateGroupsMoveTypeRelatedGroupsInfo(
relatedGroups={"primary", "replica_1", "replica_2"},
),
],
colocationScopeName="region",
),
]
)
)
)
void replicaColocation() {
// Ensure all replicas of a shard are in the same region
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("shard");
solver.setContainerName("server");
// Define replica groups
std::unordered_map<std::string, std::vector<std::string>> replicaTypes = {
{"primary", {"shard1_primary", "shard2_primary"}},
{"replica_1", {"shard1_replica1", "shard2_replica1"}},
{"replica_2", {"shard1_replica2", "shard2_replica2"}},
};
solver.addPartition("replica_type", std::move(replicaTypes));
LocalSearchSolverSpec solverSpec;
ColocateGroupsMoveTypeRelatedGroupsInfo relatedInfo;
relatedInfo.relatedGroups() = {"primary", "replica_1", "replica_2"};
ColocateGroupsMoveTypeSpec colocateSpec;
colocateSpec.partitionName() = "replica_type";
colocateSpec.relatedGroupsList() = {std::move(relatedInfo)};
colocateSpec.colocationScopeName() = "region";
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().colocateGroupsMoveTypeSpec() = colocateSpec;
solver.addSolver(solverSpec);
// Setup with misaligned replicas
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"server1", {"shard1_primary"}},
{"server2", {"shard2_replica1"}},
{"server3", {"shard1_replica1", "shard2_primary"}},
{"server4", {"shard1_replica2", "shard2_replica2"}},
});
solver.addScope(
"region",
std::map<std::string, std::string>{
{"server1", "region_a"},
{"server2", "region_a"},
{"server3", "region_b"},
{"server4", "region_b"},
});
solver.addObjectDimension(
"memory",
std::map<std::string, double>{
{"shard1_primary", 2.0},
{"shard1_replica1", 2.0},
{"shard1_replica2", 2.0},
{"shard2_primary", 2.0},
{"shard2_replica1", 2.0},
{"shard2_replica2", 2.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-memory";
balanceSpec.scope() = "server";
balanceSpec.dimension() = "memory";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << std::fixed << std::setprecision(4)
<< *solution.initialObjective()->value() << "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
With Sampling
Limit move sets with sampling:
- Python
- C++
# Use sampling to limit complexity
from rebalancer.interface.thrift.v2.SolverSpecs.thrift_types import (
ColocateGroupsMoveTypeRelatedGroupsInfo,
)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
ColocateGroupsMoveTypeSpec(
partitionName="replica_group",
relatedGroupsList=[
ColocateGroupsMoveTypeRelatedGroupsInfo(
relatedGroups=["primary", "replica1", "replica2"]
)
],
colocationScopeName="region",
defaultSampleSize=5, # Sample 5 containers per group
),
]
)
)
)
void sampling() {
// Use sampling to limit complexity
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
std::unordered_map<std::string, std::vector<std::string>> replicaGroups = {
{"primary", {"obj1"}},
{"replica1", {"obj2"}},
{"replica2", {"obj3"}},
};
solver.addPartition("replica_group", std::move(replicaGroups));
LocalSearchSolverSpec solverSpec;
ColocateGroupsMoveTypeRelatedGroupsInfo relatedInfo;
relatedInfo.relatedGroups() = {"primary", "replica1", "replica2"};
ColocateGroupsMoveTypeSpec colocateSpec;
colocateSpec.partitionName() = "replica_group";
colocateSpec.relatedGroupsList() = {std::move(relatedInfo)};
colocateSpec.colocationScopeName() = "region";
colocateSpec.defaultSampleSize() = 5; // Sample 5 containers per group
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().colocateGroupsMoveTypeSpec() = colocateSpec;
solver.addSolver(solverSpec);
}
Restricted Destinations
Restrict valid containers per group:
- Python
- C++
# Restrict which containers each group can use
from rebalancer.interface.thrift.v2.SolverSpecs.thrift_types import (
ColocateGroupsMoveTypeRelatedGroupsInfo,
)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
ColocateGroupsMoveTypeSpec(
partitionName="replica_group",
relatedGroupsList=[
ColocateGroupsMoveTypeRelatedGroupsInfo(
relatedGroups=["primary", "replica"]
)
],
colocationScopeName="region",
colocationScopeItemToGroupToContainers={
"region1": {
"primary": {"server1", "server2"},
"replica": {"server3", "server4"},
},
"region2": {
"primary": {"server5", "server6"},
"replica": {"server7", "server8"},
},
},
),
]
)
)
)
void restricted() {
// Restrict which containers each group can use
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
std::unordered_map<std::string, std::vector<std::string>> replicaGroups = {
{"primary", {"obj1"}},
{"replica", {"obj2"}},
};
solver.addPartition("replica_group", std::move(replicaGroups));
LocalSearchSolverSpec solverSpec;
ColocateGroupsMoveTypeRelatedGroupsInfo relatedInfo;
relatedInfo.relatedGroups() = {"primary", "replica"};
ColocateGroupsMoveTypeSpec colocateSpec;
colocateSpec.partitionName() = "replica_group";
colocateSpec.relatedGroupsList() = {std::move(relatedInfo)};
colocateSpec.colocationScopeName() = "region";
// Restrict valid containers per group per region
StringToStringSetMap region1Map;
region1Map["primary"] = {"server1", "server2"};
region1Map["replica"] = {"server3", "server4"};
StringToStringSetMap region2Map;
region2Map["primary"] = {"server5", "server6"};
region2Map["replica"] = {"server7", "server8"};
colocateSpec.colocationScopeItemToGroupToContainers() = {
{"region1", region1Map},
{"region2", region2Map},
};
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().colocateGroupsMoveTypeSpec() = colocateSpec;
solver.addSolver(solverSpec);
}
Multiple Related Sets
Multiple independent sets of related groups:
- Python
- C++
# Multiple independent related group sets
from rebalancer.interface.thrift.v2.SolverSpecs.thrift_types import (
ColocateGroupsMoveTypeRelatedGroupsInfo,
)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
ColocateGroupsMoveTypeSpec(
partitionName="service_type",
relatedGroupsList=[
ColocateGroupsMoveTypeRelatedGroupsInfo(
relatedGroups=["web", "web_cache"] # Web tier together
),
ColocateGroupsMoveTypeRelatedGroupsInfo(
relatedGroups=["db", "db_replica"] # DB tier together
),
],
colocationScopeName="datacenter",
),
]
)
)
)
void multipleSets() {
// Multiple independent related group sets
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
std::unordered_map<std::string, std::vector<std::string>> serviceTypes = {
{"web", {"web1"}},
{"web_cache", {"cache1"}},
{"db", {"db1"}},
{"db_replica", {"db2"}},
};
solver.addPartition("service_type", std::move(serviceTypes));
LocalSearchSolverSpec solverSpec;
ColocateGroupsMoveTypeRelatedGroupsInfo webTier;
webTier.relatedGroups() = {"web", "web_cache"}; // Web tier together
ColocateGroupsMoveTypeRelatedGroupsInfo dbTier;
dbTier.relatedGroups() = {"db", "db_replica"}; // DB tier together
ColocateGroupsMoveTypeSpec colocateSpec;
colocateSpec.partitionName() = "service_type";
colocateSpec.relatedGroupsList() = {std::move(webTier), std::move(dbTier)};
colocateSpec.colocationScopeName() = "datacenter";
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().colocateGroupsMoveTypeSpec() = colocateSpec;
solver.addSolver(solverSpec);
}
Performance Characteristics
Complexity Analysis
| Related Groups | Containers/Group | Scopes | Objects | Move Sets |
|---|---|---|---|---|
| 2 | 10 | 3 | 100 | 30K |
| 3 | 10 | 3 | 100 | 300K |
| 3 | 5 (sampled) | 3 | 100 | 37.5K |
| 4 | 10 | 5 | 100 | 5M |
Critical observations:
- Exponential in number of related groups
- Linear in number of objects
- Linear in number of scopes
- Sampling is essential for reasonable performance
When Does It Help?
ColocateGroups helps when:
- Colocation requirements: Objects must be in same scope item
- Group affinity: Primary + replicas must be nearby
- Region/rack constraints: Network locality requirements
- Disaster recovery: Groups spread across failure domains
ColocateGroups does NOT help when:
- No colocation needs: Objects can be anywhere
- Too many groups: Complexity explodes
- Independent objects: No group relationships
Comparison with Alternatives
| Move Type | Colocation | Complexity | Use Case |
|---|---|---|---|
| Single | No | O(N × C) | Independent objects |
| GroupRouting | Partial | Varies | Group-aware routing |
| ColocateGroups | Yes (strict) | O(n×S×G^k) | Related groups together |
Troubleshooting
Problem: Too slow / too many move sets
Diagnosis: Complexity explosion from many groups or containers
Solutions:
- Critical: Set
defaultSampleSize(start with 5-10) - Reduce number of related groups if possible
- Restrict valid containers with
colocationScopeItemToGroupToContainers - Use fewer destination scope items
- Consider if all groups really need colocation
Problem: No improving moves found
Diagnosis: Cannot find beneficial colocation
Solutions:
- Check partition and group definitions are correct
- Verify
relatedGroupsListspecifies correct groups - Check capacity constraints on destination containers
- May already be optimally colocated
- Review objective function
Problem: Groups not moving together
Diagnosis: Related groups configuration issue
Solutions:
- Verify
relatedGroupsListincludes all groups that should move together - Check partition assigns objects to correct groups
- Ensure related group sets are disjoint
- Review scope item configuration
Problem: Memory issues
Diagnosis: Too many move sets generated
Solutions:
- Set aggressive
defaultSampleSize(e.g., 2-3) - Reduce related group sets
- Limit destination scope items
- May need to break into smaller problems
When to Use ColocateGroups
DO use when:
- Objects have strict colocation requirements
- Moving related groups together (primary + replicas)
- Need to ensure groups in same region/rack
- Have well-defined partition structure
- Willing to use sampling to control complexity
DO NOT use when:
- Objects can move independently
- No partition/group structure
- Colocation not required
- Problem scale is too large (complexity explosion)
Related Move Types
Group-based alternatives:
- GroupRouting - Group-aware routing
- GroupMoveWithHintStrategies - Group moves with hints
- GreedyGroupToScopeItem - Greedy group placement
General alternatives:
- Single - For independent objects
- SingleChain - For 2-object chains
Source Code
- Thrift definition:
interface/thrift/SolverSpecs.thrift:721 - Implementation:
solver/moves/ColocateGroupsMoveType.h - Tests:
solver/moves/tests/
Next Steps
- Learn about GroupMoveWithHintStrategies for hint-based group moves
- Try GroupRouting for group-aware routing
- Review Move Types Overview for choosing move types
- See core concepts on Groups for partition setup