Loading learning content...
After understanding the mechanics, trade-offs, and performance implications of global and local replacement, the practical question remains: which strategy should you choose for your specific situation?
The answer is rarely 'always global' or 'always local.' Modern systems often employ hybrid strategies, using global replacement as a baseline while enforcing local boundaries where isolation is required. This page provides a decision framework for navigating these choices effectively.
By the end of this page, you will have a systematic framework for choosing replacement strategies based on workload characteristics, isolation requirements, and operational constraints. You will understand when to use pure global, pure local, or hybrid approaches, and how to configure modern systems to implement your chosen strategy.
Choosing a replacement strategy requires evaluating your situation across multiple dimensions. The following framework structures this evaluation systematically.
The four key questions:
General guidance:
Global replacement is appropriate when efficiency and throughput are paramount, and the downsides of process interference are acceptable or manageable.
Ideal scenarios for global replacement:
| Scenario | Why Global Works | Considerations |
|---|---|---|
| Single-Tenant Server | All processes owned by same user; interference is self-interference | May still want priority protection for critical services |
| Batch Processing Cluster | Throughput matters most; latency predictability less important | Jobs complete faster when memory flows to active jobs |
| Development/Test Environments | Isolation less critical; resource efficiency valued | Interference may even help identify performance issues |
| Homogeneous Workloads | All processes have similar needs; interference is symmetric | Equal impact under pressure is inherently 'fair' |
| Memory Overcommit Required | Need to run more processes than fit simultaneously | Monitor for thrashing signs; may need to reduce load |
| Best-Effort Services | No strict SLAs; focus on aggregate performance | Occasional latency spikes acceptable |
Even when choosing global replacement, add protective mechanisms: use OOM killer scoring to protect critical processes, set memory.low limits for minimum guarantees, and monitor page fault rates for early warning of interference. Pure, unguarded global replacement is rarely appropriate in production.
Local replacement is appropriate when isolation, predictability, and fairness outweigh efficiency concerns, or when trust boundaries require hard separation.
Ideal scenarios for local replacement:
| Scenario | Why Local Works | Considerations |
|---|---|---|
| Multi-Tenant Cloud | Customer isolation is mandatory; cannot let one tenant affect another | Balance between isolation and efficient resource use |
| Real-Time Systems | Predictable timing required; interference would violate guarantees | May need to static-allocate with no dynamic adjustment |
| SLA-Bound Services | Latency guarantees require predictable memory behavior | Size allocations to meet committed SLAs |
| Security-Critical Workloads | Timing side-channels via memory sharing are a threat model | Consider additional isolation mechanisms beyond memory |
| Metered Billing | Customers pay for allocated resources; must be enforceable | Allocation = what they pay for, no more, no less |
| Regulated Environments | Compliance requires demonstrable resource isolation | Document and audit isolation mechanisms |
Local replacement transfers the complexity from 'managing interference' to 'sizing allocations correctly.' Undersized allocations cause process thrashing. Oversized allocations waste resources. You must understand your workloads' memory requirements—a challenge that global replacement sidesteps but local replacement demands.
Most production systems use hybrid strategies that combine elements of both global and local replacement. These hybrids capture much of global's efficiency while providing local's isolation where needed.
Common hybrid patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
# Kubernetes Pod with Hybrid Memory Strategy# # The pod gets a total memory limit (local boundary from other pods)# Containers within the pod share that limit (global-like within pod)# This is "Global Within, Local Between" pattern apiVersion: v1kind: Podmetadata: name: microservice-podspec: containers: # Container 1: Web server - gets guaranteed minimum - name: web-server image: nginx:latest resources: requests: memory: "512Mi" # Guaranteed minimum (protected) limits: memory: "1Gi" # Hard cap (local boundary) # Container 2: Cache - can burst but has lower priority - name: redis-cache image: redis:latest resources: requests: memory: "256Mi" # Lower guarantee limits: memory: "768Mi" # Can use more if available # Container 3: Log shipper - best effort - name: log-shipper image: fluent-bit:latest resources: requests: memory: "64Mi" # Minimal guarantee limits: memory: "256Mi" # Hard cap to prevent runaway # Behavior:# - Pod has hard boundary from other pods (local)# - Within pod, memory.requests provide soft protection# - memory.limits provide hard caps# - Kubernetes uses global reclaim within the pod's allocation# - If pod exceeds sum of limits, containers get OOM killed ---# Linux cgroup v2 hybrid configuration# This achieves similar effect on bare Linux # Create cgroup hierarchy# /sys/fs/cgroup/# └── production/ # Top-level boundary# ├── database/ # Protected service# │ └── memory.min: 4G # Guaranteed minimum# │ └── memory.max: 8G # Hard limit# ├── webserver/ # Front-end tier# │ └── memory.min: 2G# │ └── memory.max: 4G# └── batch/ # Best effort# └── memory.min: 0 # No guarantee# └── memory.max: 4G # Still capped # Effect:# - database always gets at least 4G (local minimum)# - webserver always gets at least 2G (local minimum) # - batch gets what's left (global from remainder)# - All capped at their max (local upper bound)# - Within each cgroup, global replacement among its processesHybrid strategies are the norm in production because they let you capture global replacement's efficiency (high utilization, adaptive flow) while enforcing local replacement's isolation at boundaries where it matters (tenants, SLA tiers, priority levels). The art is in choosing where to draw boundaries and how permeable to make them.
Translating strategy decisions into actual system configuration requires understanding the specific mechanisms available in your operating system or container platform.
Linux configuration options:
| Mechanism | Effect | Use When |
|---|---|---|
| memory.max | Hard limit; processes killed if exceeded | Need strict cap on resource usage |
| memory.high | Soft limit; throttling and reclaim above this | Want to limit bursts but allow flexibility |
| memory.min | Guaranteed reservation; protected from reclaim | Critical service needs minimum guarantee |
| memory.low | Best-effort protection; reclaimed last | Want preference but not hard guarantee |
| oom_score_adj | OOM killer priority tuning | Protect specific processes from termination |
| mlock() | Lock specific pages in memory | Ultra-critical pages must never be evicted |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
#!/bin/bash# Production Memory Configuration Examples # ============================================# Scenario 1: Multi-Tenant Container Host# Goal: Strong isolation between tenants# ============================================ create_tenant_cgroup() { local tenant=$1 local max_memory=$2 # Create isolated cgroup for tenant mkdir -p /sys/fs/cgroup/$tenant # Hard limit - tenant cannot exceed this echo $max_memory > /sys/fs/cgroup/$tenant/memory.max # No minimum guarantee - they get what they pay for echo 0 > /sys/fs/cgroup/$tenant/memory.min # High watermark - throttle before hitting max echo $(( max_memory * 90 / 100 )) > /sys/fs/cgroup/$tenant/memory.high} create_tenant_cgroup "tenant-a" "4G"create_tenant_cgroup "tenant-b" "8G"create_tenant_cgroup "tenant-c" "2G" # ============================================# Scenario 2: Critical Service Protection# Goal: Database always gets memory it needs# ============================================ configure_critical_service() { local cgroup_path=$1 local min_memory=$2 local max_memory=$3 mkdir -p /sys/fs/cgroup/$cgroup_path # Guaranteed minimum - NEVER reclaim below this echo $min_memory > /sys/fs/cgroup/$cgroup_path/memory.min # Hard limit to prevent runaway echo $max_memory > /sys/fs/cgroup/$cgroup_path/memory.max # Move database process into cgroup echo $(pgrep postgres) > /sys/fs/cgroup/$cgroup_path/cgroup.procs # Additional protection: resistant to OOM killer echo -500 > /proc/$(pgrep postgres)/oom_score_adj} configure_critical_service "production/database" "4G" "8G" # ============================================# Scenario 3: Tiered Service Priority# Goal: Gold > Silver > Bronze in memory access# ============================================ setup_tiered_services() { # Gold tier: guaranteed memory, high protection mkdir -p /sys/fs/cgroup/gold echo "4G" > /sys/fs/cgroup/gold/memory.min echo "10G" > /sys/fs/cgroup/gold/memory.max # Silver tier: some protection, flexible limit mkdir -p /sys/fs/cgroup/silver echo "1G" > /sys/fs/cgroup/silver/memory.low # soft protection echo "6G" > /sys/fs/cgroup/silver/memory.max # Bronze tier: no protection, capped mkdir -p /sys/fs/cgroup/bronze echo "0" > /sys/fs/cgroup/bronze/memory.min echo "4G" > /sys/fs/cgroup/bronze/memory.max} setup_tiered_services # Result:# - Gold tier always gets 4G, can burst to 10G# - Silver tier prefers to keep 1G, can burst to 6G# - Bronze tier shares remaining memory, capped at 4G# - Under pressure: Bronze reclaimed first, then Silver, Gold last # ============================================# Scenario 4: Kubernetes Memory QoS Classes# ============================================ # Kubernetes automatically creates QoS classes:## Guaranteed: requests == limits for all containers# -> Highest priority, least likely to be evicted## Burstable: requests < limits for some containers# -> Medium priority, can be evicted if exceeds requests## BestEffort: no requests or limits specified# -> Lowest priority, first to be evicted # Example Guaranteed pod:cat <<EOFapiVersion: v1kind: Podspec: containers: - name: db resources: requests: memory: "4Gi" # request == limit limits: memory: "4Gi" # Guaranteed classEOFAlways start with monitoring before adding limits. Understand actual memory usage patterns before setting allocations. Set limits slightly above observed needs to allow for variability. Use memory.high before memory.max where possible—throttling is gentler than OOM killing. Review configurations regularly as workloads evolve.
Even with good intentions, memory management configurations often go wrong. Learning from common mistakes helps avoid them in your own systems.
Frequent configuration mistakes:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
"""Common Memory Configuration Pitfalls - Examples and Fixes""" def pitfall_1_overcommitted_minimums(): """ PITFALL: Sum of memory.min exceeds available """ physical_memory_gb = 16 kernel_reservation_gb = 2 # For kernel, buffers, etc. available_gb = physical_memory_gb - kernel_reservation_gb # = 14GB # BAD: Guarantees exceed available bad_config = { "database": {"memory_min": "8G"}, "webserver": {"memory_min": "6G"}, "cache": {"memory_min": "4G"}, } # Total min: 18GB > 14GB available => SOMEONE WILL STARVE # GOOD: Guarantees fit within available good_config = { "database": {"memory_min": "6G"}, "webserver": {"memory_min": "4G"}, "cache": {"memory_min": "2G"}, } # Total min: 12GB < 14GB available => All guarantees honored print("PITFALL 1: Overcommitted Minimums") print(f" Available memory: {available_gb}GB") print(f" Bad config total min: 18GB (EXCEEDS AVAILABLE)") print(f" Good config total min: 12GB (fits)") def pitfall_2_equal_allocation_unequal_needs(): """ PITFALL: One-size-fits-all allocations """ print("\nPITFALL 2: Equal Allocation for Unequal Workloads") # Actual memory needs (measured via profiling) actual_needs = { "api-gateway": 0.5, # 500MB "auth-service": 0.2, # 200MB "data-processor": 4.0, # 4GB "report-generator": 2.0, # 2GB } # BAD: Everyone gets 1GB bad_allocation = 1.0 # GB each total_bad = len(actual_needs) * bad_allocation # 4GB total print(f" Bad approach: Give everyone {bad_allocation}GB") for service, need in actual_needs.items(): status = "WASTED" if need < bad_allocation else "STARVED" diff = abs(need - bad_allocation) print(f" {service}: needs {need}GB, gets {bad_allocation}GB -> {status} by {diff}GB") # GOOD: Proportional to actual needs with buffer total_need = sum(actual_needs.values()) available = 8.0 # GB available for these services print(f"\n Good approach: Proportional allocation from {available}GB") for service, need in actual_needs.items(): allocation = (need / total_need) * available status = "ADEQUATE" if allocation >= need else "INSUFFICIENT" print(f" {service}: needs {need}GB, gets {allocation:.1f}GB -> {status}") def pitfall_3_forgetting_overhead(): """ PITFALL: Not accounting for system overhead """ print("\nPITFALL 3: Forgetting OS/Kernel Overhead") total_ram = 32 # GB # BAD: Allocate everything to applications bad_app_allocation = total_ram # 32GB to apps print(f" Bad: Allocate {bad_app_allocation}GB to applications") print(f" Kernel needs: ~2GB (page tables, slab, etc.)") print(f" Buffer cache: ~1-4GB (for I/O performance)") print(f" Result: Apps fight kernel; system unstable") # GOOD: Reserve for system needs kernel_reserve = 2 buffer_reserve = 4 # For healthy I/O caching app_allocation = total_ram - kernel_reserve - buffer_reserve print(f"\n Good: Reserve {kernel_reserve}GB kernel + {buffer_reserve}GB buffers") print(f" Available for apps: {app_allocation}GB") print(f" Result: Healthy system with room for caching") # Run examplespitfall_1_overcommitted_minimums()pitfall_2_equal_allocation_unequal_needs()pitfall_3_forgetting_overhead()Use a pre-deployment checklist: (1) Sum all memory.min values and verify they fit in available RAM with 10-20% margin, (2) Profile workloads to base limits on actual usage, (3) Account for kernel, buffer cache, and shared libraries, (4) Set up monitoring and alerting before deployment.
Use this comprehensive checklist when making replacement policy decisions for a new system or re-evaluating an existing configuration.
Pre-decision information gathering:
Memory configurations should be reviewed quarterly or whenever significant workload changes occur. Workloads grow over time—limits that work today may cause problems next year. Build regular review into your operational practices.
We have synthesized the module's concepts into a practical decision framework for choosing replacement strategies.
Module Complete: Global vs Local Replacement
You have mastered the fundamental distinction between global and local replacement, understanding their mechanisms, trade-offs, interference patterns, performance implications, and practical configuration. This knowledge enables you to design and operate memory management policies that balance efficiency with isolation according to your specific requirements.
Congratulations! You have completed the Global vs Local Replacement module. You now have a comprehensive understanding of replacement scope decisions—from fundamental concepts to practical configuration. In the next module, we will explore Thrashing—what happens when systems run out of manageable memory and how to detect, prevent, and recover from this condition.