Loading content...
We've explored three fundamental allocation strategies: equal allocation (simple fairness), proportional allocation (size-aware), and priority-based allocation (importance-aware). Each addresses specific concerns but has limitations when used alone.
Production operating systems don't choose a single strategy—they combine multiple approaches into sophisticated hybrid policies. A process might receive:
These combination approaches balance fairness, efficiency, priority, and adaptability in ways that single strategies cannot achieve.
By the end of this page, you will understand how to design and analyze combination allocation policies. You'll master techniques for blending guaranteed minimums, proportional shares, and bounded maximums; understand hierarchical allocation structures; explore constraint-based approaches; and see how major operating systems implement hybrid allocation in practice.
Each single-strategy approach fails in specific scenarios that production systems must handle:
Equal Allocation Failures:
Proportional Allocation Failures:
Priority-Based Allocation Failures:
| Single Strategy | Key Limitation | Combination Solution |
|---|---|---|
| Equal | Size-blind | Proportional base + equal minimum |
| Proportional | Priority-blind | Priority-weighted proportional |
| Priority | Size-blind | Priority classes with proportional within |
| All single | Static/non-adaptive | Add dynamic adjustment layer |
| All single | No bounds | Add min/max constraints |
The Combination Principle
Effective combination approaches layer multiple mechanisms:
Each layer addresses different failure modes, creating a robust system that handles diverse workloads appropriately.
The goal of combination approaches isn't to find a "perfect" allocation formula—no such formula exists. Instead, the goal is to create a policy that:
• Prevents worst-case outcomes (starvation, thrashing) • Respects organizational priorities • Scales allocations reasonably with actual needs • Adapts to changing conditions
Combination approaches achieve this through layered constraints and multiple mechanisms working together.
The most common combination approach augments proportional allocation with minimum and maximum bounds. This three-layer structure provides both protection and limits.
Mathematical Formulation
For process i:
The bounded allocation is:
aᵢ = clamp(propᵢ, minᵢ, maxᵢ)
Or equivalently: aᵢ = max(minᵢ, min(maxᵢ, propᵢ))
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
from dataclasses import dataclassfrom typing import List, Optionalimport math @dataclassclass BoundedProcess: pid: int name: str size: int # Virtual memory size min_frames: int # Guaranteed minimum max_frames: Optional[int] # Ceiling (None = unlimited) allocation: int = 0 # Result def bounded_proportional_allocation( processes: List[BoundedProcess], total_frames: int, system_min: int = 3 # Architectural minimum) -> List[BoundedProcess]: """ Bounded proportional allocation with redistribution. Algorithm: 1. Calculate raw proportional allocations 2. Clamp to min/max bounds 3. Redistribute excess from capped processes 4. Iterate until stable """ if not processes: return processes # Ensure minimums are at least system minimum for p in processes: p.min_frames = max(p.min_frames, system_min) if p.max_frames is not None: p.max_frames = max(p.max_frames, p.min_frames) # Check if minimums are satisfiable total_min = sum(p.min_frames for p in processes) if total_min > total_frames: raise ValueError( f"Total minimum requirements ({total_min}) exceed " f"available frames ({total_frames})" ) # Iterative allocation with redistribution remaining = total_frames unsatisfied = list(processes) # Phase 1: Assign minimums for p in processes: p.allocation = p.min_frames remaining -= p.min_frames # Phase 2: Proportionally distribute remainder with bounds for iteration in range(len(processes)): # Max iterations if remaining <= 0 or not unsatisfied: break # Calculate proportional shares of remaining frames uncapped = [p for p in unsatisfied if p.max_frames is None or p.allocation < p.max_frames] if not uncapped: break total_uncapped_size = sum(p.size for p in uncapped) if total_uncapped_size == 0: break # Distribute remaining proportionally distributed = 0 for p in uncapped: share = (p.size / total_uncapped_size) * remaining additional = int(share) # Check against maximum if p.max_frames is not None: additional = min(additional, p.max_frames - p.allocation) p.allocation += additional distributed += additional remaining -= distributed # Remove satisfied (at max) processes from uncapped pool unsatisfied = [p for p in unsatisfied if p.max_frames is None or p.allocation < p.max_frames] # Distribute any leftover frames (due to rounding) while remaining > 0: for p in processes: if p.max_frames is None or p.allocation < p.max_frames: p.allocation += 1 remaining -= 1 if remaining <= 0: break return processes def demonstrate_bounded_allocation(): """Show how bounds affect proportional allocation.""" scenarios = [ { "name": "Minimum Protection", "processes": [ BoundedProcess(1, "Critical Small", 50, 100, None), BoundedProcess(2, "Large Normal", 500, 20, None), ], "frames": 300, }, { "name": "Maximum Capping", "processes": [ BoundedProcess(1, "Greedy App", 1000, 20, 150), BoundedProcess(2, "Normal App", 200, 20, None), BoundedProcess(3, "Small Util", 50, 20, None), ], "frames": 300, }, { "name": "Combined Bounds", "processes": [ BoundedProcess(1, "Database", 400, 80, 200), BoundedProcess(2, "Cache", 300, 50, 150), BoundedProcess(3, "Worker", 200, 30, 100), BoundedProcess(4, "Logger", 50, 10, 30), ], "frames": 400, }, ] for scenario in scenarios: print(f"\n=== {scenario['name']} ===") print(f"Total frames: {scenario['frames']}\n") result = bounded_proportional_allocation( scenario["processes"], scenario["frames"] ) total_size = sum(p.size for p in result) total_alloc = sum(p.allocation for p in result) for p in result: prop = (p.size / total_size) * scenario["frames"] max_str = str(p.max_frames) if p.max_frames else "∞" print(f" {p.name:15s}: size={p.size:4d}, " f"bounds=[{p.min_frames:3d}, {max_str:>3s}], " f"prop={prop:5.0f}, alloc={p.allocation:3d}") print(f" Total allocated: {total_alloc}") if __name__ == "__main__": demonstrate_bounded_allocation()Redistribution Semantics
When bounds constrain allocations, frames must be redistributed:
Ceiling Redistribution: When a process hits its maximum, the excess proportional share is redistributed to uncapped processes. This ensures total allocation equals total available frames.
Floor Redistribution: When processes require more than their proportional share (due to minimum floors), other processes receive less. This may cascade—forcing other processes below their proportional share or even to their minimums.
Conflict Resolution: When total minimums exceed available frames, the allocation is infeasible. The system must either:
Hierarchical allocation organizes processes into groups (often called cgroups, containers, or resource pools) and applies allocation at multiple levels. This structure supports multi-tenancy, organizational boundaries, and layered policy.
Two-Level Hierarchy Example:
System (1000 frames)
├── Production (600 frames, 60%)
│ ├── Database (proportional within Production)
│ ├── API Server
│ └── Cache
├── Development (300 frames, 30%)
│ ├── IDE
│ ├── Compiler
│ └── Test Runner
└── Background (100 frames, 10%)
├── Backup
└── Monitoring
Allocation Process:
Level 1 (Groups): Divide total frames among groups (fixed percentage, proportional to group size, or priority-weighted)
Level 2 (Processes): Within each group, divide the group's allocation among member processes (typically proportional to size within group)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
from dataclasses import dataclass, fieldfrom typing import List, Optional, Dictfrom enum import Enum @dataclassclass Process: pid: int name: str size: int min_frames: int = 3 allocation: int = 0 @dataclassclass ResourceGroup: name: str weight: float = 1.0 # Weight for inter-group allocation min_fraction: float = 0.0 # Minimum fraction of parent max_fraction: float = 1.0 # Maximum fraction of parent processes: List[Process] = field(default_factory=list) subgroups: List['ResourceGroup'] = field(default_factory=list) allocation: int = 0 # Frames allocated to this group class HierarchicalAllocator: """ Hierarchical frame allocator supporting nested resource groups. """ def __init__(self, total_frames: int): self.total_frames = total_frames self.root_groups: List[ResourceGroup] = [] def add_root_group(self, group: ResourceGroup): self.root_groups.append(group) def allocate(self): """Perform hierarchical allocation.""" self._allocate_level(self.root_groups, self.total_frames) def _allocate_level(self, groups: List[ResourceGroup], available: int): """Allocate frames to a level of the hierarchy.""" if not groups: return # Calculate total weight total_weight = sum(g.weight for g in groups) if total_weight == 0: total_weight = len(groups) # Equal distribution # Phase 1: Calculate proportional allocations allocations = {} for group in groups: proportion = group.weight / total_weight raw_alloc = int(available * proportion) # Apply bounds min_alloc = int(available * group.min_fraction) max_alloc = int(available * group.max_fraction) bounded_alloc = max(min_alloc, min(max_alloc, raw_alloc)) allocations[group.name] = bounded_alloc # Phase 2: Redistribute excess from capped groups total_allocated = sum(allocations.values()) excess = available - total_allocated if excess > 0: # Find uncapped groups uncapped = [g for g in groups if allocations[g.name] < int(available * g.max_fraction)] if uncapped: uncapped_weight = sum(g.weight for g in uncapped) for group in uncapped: share = int(excess * (group.weight / uncapped_weight)) max_alloc = int(available * group.max_fraction) additional = min(share, max_alloc - allocations[group.name]) allocations[group.name] += additional # Apply allocations and recurse for group in groups: group.allocation = allocations[group.name] if group.subgroups: # Recurse to subgroups self._allocate_level(group.subgroups, group.allocation) elif group.processes: # Leaf level: allocate to processes self._allocate_to_processes(group.processes, group.allocation) def _allocate_to_processes(self, processes: List[Process], available: int): """Proportionally allocate frames to processes within a group.""" if not processes: return total_size = sum(p.size for p in processes) if total_size == 0: per_process = available // len(processes) for p in processes: p.allocation = per_process return # Proportional allocation respecting minimums remaining = available # First pass: assign minimums for p in processes: p.allocation = p.min_frames remaining -= p.min_frames if remaining <= 0: return # Only minimums possible # Second pass: proportional distribution of remainder total_unfulfilled = sum(max(0, p.size - p.allocation) for p in processes) if total_unfulfilled == 0: return for p in processes: unfulfilled = max(0, p.size - p.allocation) share = (unfulfilled / total_unfulfilled) * remaining p.allocation += int(share) def print_allocation(self): """Display the allocation hierarchy.""" print(f"\n{'='*60}") print(f"Hierarchical Allocation ({self.total_frames} frames)") print(f"{'='*60}") self._print_groups(self.root_groups, 0) total = sum(self._group_total(g) for g in self.root_groups) print(f"\nTotal allocated: {total}") def _print_groups(self, groups: List[ResourceGroup], indent: int): for group in groups: prefix = " " * indent print(f"\n{prefix}┌─ {group.name} " f"(weight={group.weight}, alloc={group.allocation})") for proc in group.processes: pct = proc.allocation / group.allocation * 100 if group.allocation else 0 print(f"{prefix}│ └ {proc.name}: " f"size={proc.size}, alloc={proc.allocation} ({pct:.0f}%)") if group.subgroups: self._print_groups(group.subgroups, indent + 1) def _group_total(self, group: ResourceGroup) -> int: if group.processes: return sum(p.allocation for p in group.processes) return sum(self._group_total(sg) for sg in group.subgroups) def demonstrate_hierarchical(): allocator = HierarchicalAllocator(1000) # Production group production = ResourceGroup( name="Production", weight=60, min_fraction=0.5, # At least 50% max_fraction=0.7, # At most 70% processes=[ Process(1, "Database", 400, 50), Process(2, "API Server", 250, 30), Process(3, "Cache", 200, 20), ] ) # Development group development = ResourceGroup( name="Development", weight=30, min_fraction=0.2, max_fraction=0.4, processes=[ Process(4, "IDE", 150, 20), Process(5, "Compiler", 100, 15), Process(6, "Test Runner", 50, 10), ] ) # Background group background = ResourceGroup( name="Background", weight=10, min_fraction=0.05, max_fraction=0.15, processes=[ Process(7, "Backup", 100, 10), Process(8, "Monitoring", 50, 5), ] ) allocator.add_root_group(production) allocator.add_root_group(development) allocator.add_root_group(background) allocator.allocate() allocator.print_allocation() if __name__ == "__main__": demonstrate_hierarchical()Linux cgroups v2 implements exactly this hierarchical model. Each cgroup can have:
• memory.max: Hard maximum (ceiling) • memory.min: Hard minimum (floor, protected from reclaim) • memory.low: Soft minimum (best-effort protection) • memory.weight: Proportional weight for sharing
Nested cgroups inherit and subdivide their parent's allocation, creating true hierarchical resource management.
A widely-adopted combination pattern separates allocation into three distinct components, each serving a different purpose.
Three Components:
1. Guarantee (Reservation)
2. Share (Proportional Pool)
3. Limit (Ceiling)
| Component | Under Pressure | No Pressure | Purpose |
|---|---|---|---|
| Guarantee | Protected | Fully allocated | Ensure minimum operation |
| Share | Reclaimed first | Allocated proportionally | Elastic performance improvement |
| Limit | N/A (ceiling) | N/A (ceiling) | Prevent monopolization |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
#include <stdio.h>#include <stdlib.h>#include <stdbool.h> typedef struct { int pid; const char* name; // GSL parameters size_t guarantee; // Protected minimum size_t share_weight; // Weight for share pool size_t limit; // Hard maximum // Current state size_t guaranteed_alloc; // Frames from guarantee size_t share_alloc; // Frames from share pool size_t total_alloc; // Total frames} GSLProcess; typedef enum { PRESSURE_NONE = 0, // Plenty of memory PRESSURE_LOW = 1, // Some reclamation needed PRESSURE_HIGH = 2, // Significant reclamation PRESSURE_CRITICAL = 3, // Near OOM} MemoryPressure; typedef struct { GSLProcess* processes; int process_count; size_t total_frames; size_t free_frames; MemoryPressure pressure;} GSLAllocator; // Initialize allocator and validate guaranteesint gsl_init(GSLAllocator* alloc, GSLProcess* procs, int count, size_t total) { alloc->processes = procs; alloc->process_count = count; alloc->total_frames = total; alloc->pressure = PRESSURE_NONE; // Verify total guarantees don't exceed capacity size_t total_guarantee = 0; for (int i = 0; i < count; i++) { total_guarantee += procs[i].guarantee; // Ensure limit >= guarantee if (procs[i].limit < procs[i].guarantee) { procs[i].limit = procs[i].guarantee; } } if (total_guarantee > total) { fprintf(stderr, "ERROR: Total guarantees (%zu) exceed " "available frames (%zu)\n", total_guarantee, total); return -1; } alloc->free_frames = total - total_guarantee; return 0;} // Allocate based on current pressure levelvoid gsl_allocate(GSLAllocator* alloc) { size_t share_pool = alloc->free_frames; // Calculate total share weight size_t total_weight = 0; for (int i = 0; i < alloc->process_count; i++) { total_weight += alloc->processes[i].share_weight; } for (int i = 0; i < alloc->process_count; i++) { GSLProcess* p = &alloc->processes[i]; // Always get guarantee p->guaranteed_alloc = p->guarantee; // Share allocation depends on pressure switch (alloc->pressure) { case PRESSURE_CRITICAL: // Only guarantees p->share_alloc = 0; break; case PRESSURE_HIGH: // Minimal share (25% of proportional) if (total_weight > 0) { size_t prop_share = (share_pool * p->share_weight) / total_weight; p->share_alloc = prop_share / 4; } break; case PRESSURE_LOW: // Reduced share (50% of proportional) if (total_weight > 0) { size_t prop_share = (share_pool * p->share_weight) / total_weight; p->share_alloc = prop_share / 2; } break; case PRESSURE_NONE: // Full proportional share if (total_weight > 0) { p->share_alloc = (share_pool * p->share_weight) / total_weight; } break; } // Apply limit p->total_alloc = p->guaranteed_alloc + p->share_alloc; if (p->total_alloc > p->limit) { p->total_alloc = p->limit; p->share_alloc = p->limit - p->guaranteed_alloc; } }} // Respond to pressure changevoid gsl_pressure_change(GSLAllocator* alloc, MemoryPressure new_pressure) { MemoryPressure old_pressure = alloc->pressure; alloc->pressure = new_pressure; if (new_pressure != old_pressure) { printf("Pressure changed: %d -> %d, rebalancing...\n", old_pressure, new_pressure); gsl_allocate(alloc); }} // Print current allocationsvoid gsl_print_status(GSLAllocator* alloc) { const char* pressure_names[] = { "NONE", "LOW", "HIGH", "CRITICAL" }; printf("\n=== GSL Allocation Status ===\n"); printf("Total frames: %zu, Pressure: %s\n\n", alloc->total_frames, pressure_names[alloc->pressure]); size_t total_alloc = 0; for (int i = 0; i < alloc->process_count; i++) { GSLProcess* p = &alloc->processes[i]; printf("%-15s: guarantee=%3zu, share=%3zu, total=%3zu " "(limit=%3zu)\n", p->name, p->guaranteed_alloc, p->share_alloc, p->total_alloc, p->limit); total_alloc += p->total_alloc; } printf("\nTotal allocated: %zu\n", total_alloc);}A more flexible approach treats allocation as a constraint satisfaction problem. Rather than following a fixed formula, the allocator finds an allocation that satisfies all constraints while optimizing some objective.
Common Constraints:
Hard Constraints (must satisfy):
Soft Constraints (prefer to satisfy):
Optimization Objective
Given constraints, the allocator optimizes an objective function. Common objectives:
Minimize Thrashing Risk: Maximize the minimum (allocation / working_set) ratio across all processes. A minimax objective ensures no process is severely under-allocated.
Weighted Proportionality: Minimize Σ pᵢ × |aᵢ - targetᵢ|, where pᵢ is priority weight. High-priority processes have their targets more heavily weighted.
Fairness (Max-Min Fairness): Maximize the minimum allocation first, then the second-minimum, and so on. Ensures no process is sacrificed for others.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
from dataclasses import dataclassfrom typing import List, Tuple, Optionalimport heapq @dataclassclass ConstrainedProcess: pid: int name: str size: int # Virtual size working_set: int # Estimated working set priority: float # 0.0 to 1.0 min_frames: int # Hard minimum max_frames: int # Hard maximum target_frames: int # Desired allocation allocation: int = 0 # Result class ConstraintAllocator: """ Constraint-based allocator using progressive filling. Satisfies hard constraints while optimizing for: 1. Max-min fairness (no process severely starved) 2. Priority-weighted target satisfaction """ def __init__(self, total_frames: int): self.total_frames = total_frames self.processes: List[ConstrainedProcess] = [] def add_process(self, process: ConstrainedProcess): self.processes.append(process) def validate_constraints(self) -> Tuple[bool, str]: """Check if hard constraints are satisfiable.""" total_min = sum(p.min_frames for p in self.processes) total_max = sum(p.max_frames for p in self.processes) if total_min > self.total_frames: return False, f"Total minimums ({total_min}) > available ({self.total_frames})" if total_max < self.total_frames: return False, f"Total maximums ({total_max}) < available (waste)" return True, "Constraints satisfiable" def allocate_maxmin_fair(self): """ Max-min fair allocation with priority weighting. Algorithm: 1. Start all processes at their minimum 2. Progressively fill, prioritizing processes with lowest allocation relative to their working set 3. Respect maximums during filling """ valid, msg = self.validate_constraints() if not valid: raise ValueError(msg) # Initialize at minimums for p in self.processes: p.allocation = p.min_frames remaining = self.total_frames - sum(p.allocation for p in self.processes) # Progressive filling using priority queue # Heap entries: (score, -priority, process_index) # Lower score = more deserving of additional frames def compute_score(p: ConstrainedProcess) -> float: """Score based on allocation relative to need.""" if p.working_set == 0: return float('inf') # No need if p.allocation >= p.max_frames: return float('inf') # At maximum # Score = allocation / working_set, lower = more deserving base_score = p.allocation / p.working_set # Priority adjustment: high priority reduces score priority_factor = 1.0 - (p.priority * 0.5) # 0.5x to 1.0x return base_score * priority_factor while remaining > 0: # Find process most deserving of additional frame scores = [(compute_score(p), -p.priority, i) for i, p in enumerate(self.processes) if p.allocation < p.max_frames] if not scores: break # All at maximum _, _, best_idx = min(scores) self.processes[best_idx].allocation += 1 remaining -= 1 return self.processes def allocate_weighted_target(self): """ Allocate prioritizing closeness to targets. Algorithm: 1. Start at minimums 2. Allocate toward targets in priority-weighted order 3. Distribute excess proportionally """ for p in self.processes: p.allocation = p.min_frames remaining = self.total_frames - sum(p.allocation for p in self.processes) # Phase 1: Fill toward targets in priority order sorted_by_priority = sorted( self.processes, key=lambda p: p.priority, reverse=True ) for p in sorted_by_priority: needed = min( p.target_frames - p.allocation, p.max_frames - p.allocation, remaining ) if needed > 0: p.allocation += needed remaining -= needed # Phase 2: Distribute remaining proportionally if remaining > 0: eligible = [p for p in self.processes if p.allocation < p.max_frames] total_headroom = sum(p.max_frames - p.allocation for p in eligible) for p in eligible: if total_headroom > 0: headroom = p.max_frames - p.allocation share = int(remaining * (headroom / total_headroom)) actual = min(share, p.max_frames - p.allocation) p.allocation += actual return self.processes def print_allocation(self): print("\n" + "="*70) print("Constraint-Based Allocation Results") print("="*70) total = 0 for p in self.processes: ws_ratio = p.allocation / p.working_set if p.working_set else 0 target_diff = p.allocation - p.target_frames status = "✓" if ws_ratio >= 1.0 else "⚠" print(f"{p.name:15s}: alloc={p.allocation:3d} " f"[min={p.min_frames:3d}, max={p.max_frames:3d}] " f"WS%={ws_ratio*100:5.1f}% target_diff={target_diff:+4d} {status}") total += p.allocation print(f"\nTotal: {total}/{self.total_frames}") def demonstrate_constraint_allocation(): allocator = ConstraintAllocator(500) processes = [ ConstrainedProcess(1, "Database", 400, 200, 0.9, 50, 250, 200), ConstrainedProcess(2, "API Server", 250, 100, 0.8, 30, 150, 100), ConstrainedProcess(3, "Cache", 200, 80, 0.7, 20, 120, 80), ConstrainedProcess(4, "Worker", 150, 60, 0.5, 15, 100, 60), ConstrainedProcess(5, "Logger", 50, 20, 0.3, 5, 40, 20), ] for p in processes: allocator.add_process(p) print("\n--- Max-Min Fair Allocation ---") allocator.allocate_maxmin_fair() allocator.print_allocation() # Reset and try weighted target for p in processes: p.allocation = 0 print("\n--- Weighted Target Allocation ---") allocator.allocate_weighted_target() allocator.print_allocation() if __name__ == "__main__": demonstrate_constraint_allocation()Major operating systems and orchestration platforms implement sophisticated combination approaches. Understanding these real-world implementations connects theory to practice.
Linux Memory Management
Linux uses a multi-layered combination approach:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
#!/bin/bash# Linux cgroups v2 combination allocation configuration CGROUP_BASE="/sys/fs/cgroup" # Create hierarchical groupsmkdir -p $CGROUP_BASE/services/{critical,standard,background} # === CRITICAL SERVICES ===# Guarantee: At least 4GB protected from reclaimecho "4G" > $CGROUP_BASE/services/critical/memory.min # Soft target: Prefer 8GB, reclaim only under pressureecho "8G" > $CGROUP_BASE/services/critical/memory.low # Hard limit: Never exceed 16GBecho "16G" > $CGROUP_BASE/services/critical/memory.max # High weight for proportional sharingecho "300" > $CGROUP_BASE/services/critical/memory.weight # === STANDARD SERVICES ===# Lower guaranteeecho "1G" > $CGROUP_BASE/services/standard/memory.minecho "4G" > $CGROUP_BASE/services/standard/memory.lowecho "8G" > $CGROUP_BASE/services/standard/memory.maxecho "100" > $CGROUP_BASE/services/standard/memory.weight # === BACKGROUND SERVICES ===# Minimal guaranteeecho "256M" > $CGROUP_BASE/services/background/memory.minecho "512M" > $CGROUP_BASE/services/background/memory.lowecho "2G" > $CGROUP_BASE/services/background/memory.maxecho "25" > $CGROUP_BASE/services/background/memory.weight # Assign processesecho $DATABASE_PID > $CGROUP_BASE/services/critical/cgroup.procsecho $WEBSERVER_PID > $CGROUP_BASE/services/standard/cgroup.procsecho $LOGROTATE_PID > $CGROUP_BASE/services/background/cgroup.procs # Result under memory pressure:# 1. Background reclaimed first (lowest weight)# 2. Standard reclaimed after background exhausted# 3. Critical protected until memory.min reached# 4. Proportional sharing based on weights for unprotected portionKubernetes Memory Management
Kubernetes combines several mechanisms:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
# Kubernetes combination allocation exampleapiVersion: v1kind: Podmetadata: name: tiered-applicationspec: # Priority class affects eviction order priorityClassName: high-priority containers: # Critical container: Guaranteed QoS class # requests == limits ensures guaranteed allocation - name: database image: postgres:14 resources: requests: memory: "4Gi" # Guaranteed minimum cpu: "2" limits: memory: "4Gi" # Same as request = Guaranteed QoS cpu: "2" # Standard container: Burstable QoS class # requests < limits allows elasticity - name: application image: myapp:latest resources: requests: memory: "1Gi" # Guaranteed minimum cpu: "500m" limits: memory: "4Gi" # Can burst up to 4Gi cpu: "2" # Background container: BestEffort QoS (no resources specified) # First to be evicted under memory pressure - name: sidecar image: logging-agent:latest # No resource requests/limits = BestEffort # Will be evicted first if node runs low on memory ---# Priority class definitionapiVersion: scheduling.k8s.io/v1kind: PriorityClassmetadata: name: high-priorityvalue: 1000000 # Higher = more importantglobalDefault: falsedescription: "Priority class for production-critical workloads"| QoS Class | Definition | Eviction Order | Memory Treatment |
|---|---|---|---|
| Guaranteed | requests == limits for all containers | Last (lowest) | Protected allocation |
| Burstable | requests < limits or some containers missing | Middle | Elastic with reclaim |
| BestEffort | No requests/limits specified | First (highest) | Whatever's available |
Both Linux and Kubernetes demonstrate the same combination pattern:
This four-component pattern—floor, target, ceiling, priority—appears across most production allocation systems.
When designing a combination allocation policy for a real system, follow a systematic approach to ensure the policy meets operational requirements.
Step 1: Identify Workload Classes
Group processes by their memory behavior and importance:
Step 2: Determine Requirements for Each Class
For each class, define:
Step 3: Define Pressure Response
Specify how allocation changes under memory pressure:
No Pressure (< 80% utilized):
Low Pressure (80-90% utilized):
High Pressure (90-95% utilized):
Critical Pressure (> 95% utilized):
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
# Example combination allocation policy specification system: total_memory: 64Gi reserved_for_os: 2Gi available_for_processes: 62Gi classes: realtime: description: "Latency-critical production services" min_fraction: 0.40 # At least 40% of memory max_fraction: 0.60 # At most 60% target_fraction: 0.50 # Target 50% priority_weight: 100 # Highest priority eviction_protected: true interactive: description: "User-facing applications" min_fraction: 0.20 max_fraction: 0.40 target_fraction: 0.30 priority_weight: 50 eviction_protected: false batch: description: "Throughput-oriented jobs" min_fraction: 0.10 max_fraction: 0.30 target_fraction: 0.15 priority_weight: 20 eviction_protected: false background: description: "Maintenance and monitoring" min_fraction: 0.02 max_fraction: 0.10 target_fraction: 0.05 priority_weight: 5 eviction_protected: false pressure_response: levels: - name: "normal" threshold_pct: 0 action: "proportional to targets" - name: "moderate" threshold_pct: 80 action: "reduce background to min, batch to 75% target" - name: "high" threshold_pct: 90 action: "background at min, batch at min, reduce interactive" - name: "critical" threshold_pct: 95 action: "only minimums protected, trigger admission control" - name: "emergency" threshold_pct: 99 action: "OOM killer armed for background/batch processes" validation: # Sum of min_fractions should be ≤ 1.0 total_min: 0.72 # 0.40 + 0.20 + 0.10 + 0.02 = 0.72 ✓ # Sum of target_fractions should be ~1.0 total_target: 1.00 # 0.50 + 0.30 + 0.15 + 0.05 = 1.00 ✓ # Sum of max_fractions can exceed 1.0 (elastic) total_max: 1.40 # 0.60 + 0.40 + 0.30 + 0.10 = 1.40 ✓We have comprehensively explored combination allocation approaches—the sophisticated hybrid strategies that production systems use to balance multiple allocation concerns simultaneously. Let's consolidate the essential concepts:
Looking Ahead
Combination approaches provide static or semi-static policies that balance multiple concerns. However, memory workloads change continuously—processes go through phases, usage patterns shift, and new processes arrive.
In the final page of this module, we explore dynamic adjustment—techniques for continuously adapting allocation based on runtime behavior. Dynamic adjustment adds the final layer atop combination policies, creating truly adaptive memory management that responds to changing conditions in real-time.
You now understand how production systems combine multiple allocation strategies into sophisticated hybrid policies. You can design bounded proportional allocations, hierarchical structures, and guarantee-share-limit models. You recognize these patterns in Linux cgroups and Kubernetes, and you have a systematic approach for designing combination policies for real systems.