Add server-wide memory capacity check (Option 9) - Critical OOM prevention

NEW FEATURES:
- Menu Option 9: Check Server Memory Capacity (OOM Risk)
- Calculates total memory if ALL PHP-FPM pools hit max_children
- Identifies servers at risk of Out-Of-Memory (OOM) kills
- Provides balanced memory allocation recommendations

TWO NEW ANALYZER FUNCTIONS:

1. calculate_server_memory_capacity()
   - Iterates through all users/PHP-FPM pools
   - Calculates: max_children × avg_memory_per_process
   - Sums total across all pools
   - Compares to total RAM
   - Returns: total_required|total_ram|percentage|status

   Status Levels:
   - HEALTHY:  <60% RAM (safe)
   - CAUTION:  60-75% RAM (watch)
   - WARNING:  75-90% RAM (risky)
   - CRITICAL: >90% RAM (OOM likely!)

2. calculate_balanced_memory_allocation()
   - Analyzes traffic for each user (requests/minute)
   - Calculates proportional memory allocation
   - Reserves 20% of RAM for system (min 2GB)
   - Distributes remaining RAM based on traffic
   - Returns recommendations: REDUCE / INCREASE / OPTIMAL

   Example output:
   USER     CURRENT_MAX  AVG_MB  TRAFFIC_RPM  RECOMMENDED_MAX  REASON
   user1    50          45MB     120          75              INCREASE (traffic demands)
   user2    100         60MB     10           15              REDUCE (prevent OOM)

MENU OPTION 9 FEATURES:
- Shows total RAM vs required memory
- Displays percentage and color-coded status
- Optional per-user breakdown table
- Optional balanced recommendations
- Interactive: ask user what details to show

USE CASE:
Server has 16GB RAM. 10 users each with max_children=50, avg 50MB/process.
Total required: 10 × 50 × 50MB = 25GB
Percentage: 156% of RAM → CRITICAL!
Result: Server WILL run out of memory and kill processes!

This feature addresses user's request:
"calculating max children and memory allocation and then combining all the
 accounts to see if the memory will hit over the memory cap if at capacity"

CRITICAL for preventing OOM kills on shared hosting servers!
This commit is contained in:
cschantz
2025-12-02 20:39:20 -05:00
parent ffc82cc7b7
commit eda451093f
2 changed files with 354 additions and 0 deletions
+211
View File
@@ -713,6 +713,215 @@ convert_to_bytes() {
esac
}
# ============================================================================
# SERVER-WIDE MEMORY CAPACITY ANALYSIS
# ============================================================================
# Calculate total memory required if all PHP-FPM pools hit max capacity
# Usage: calculate_server_memory_capacity
# Returns: total_required_mb|total_ram_mb|percentage|status|details
calculate_server_memory_capacity() {
echo "Analyzing server-wide PHP-FPM memory capacity..." >&2
# Get total system memory
local total_ram_mb
total_ram_mb=$(free -m | awk '/^Mem:/ {print $2}')
# Get all users
local users
users=$(list_all_users)
if [ -z "$users" ]; then
echo "0|$total_ram_mb|0|ERROR|No users found"
return 1
fi
# Track totals
local total_required_mb=0
local total_max_children=0
local pool_count=0
local details=""
# Iterate through all users
while IFS= read -r username; do
[ -z "$username" ] && continue
# Find FPM pool config
local pool_config
pool_config=$(find_fpm_pool_config "$username")
if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then
continue
fi
pool_count=$((pool_count + 1))
# Get max_children from pool config
local max_children
max_children=$(grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ')
if [ -z "$max_children" ] || [ "$max_children" -eq 0 ]; then
max_children=5 # Safe default if not set
fi
# Get average memory per process
local memory_stats
memory_stats=$(calculate_memory_per_process "$username")
local avg_kb
avg_kb=$(echo "$memory_stats" | cut -d'|' -f1)
if [ "$avg_kb" -eq 0 ]; then
# No active processes, estimate 50MB per process (conservative)
avg_kb=$((50 * 1024))
fi
local avg_mb=$((avg_kb / 1024))
# Calculate max memory for this pool
local pool_max_mb=$((max_children * avg_mb))
total_required_mb=$((total_required_mb + pool_max_mb))
total_max_children=$((total_max_children + max_children))
# Add to details
details+="$username|$max_children|${avg_mb}MB|${pool_max_mb}MB"$'\n'
done <<< "$users"
# Calculate percentage
local percentage=$((total_required_mb * 100 / total_ram_mb))
# Determine status
local status
if [ "$percentage" -gt 90 ]; then
status="CRITICAL"
elif [ "$percentage" -gt 75 ]; then
status="WARNING"
elif [ "$percentage" -gt 60 ]; then
status="CAUTION"
else
status="HEALTHY"
fi
# Return formatted result
echo "$total_required_mb|$total_ram_mb|$percentage|$status|$pool_count pools|$total_max_children total max_children"
# Return details for further processing (to stderr so it doesn't mix with main output)
echo "$details" >&2
}
# Calculate optimal memory allocation per user
# Usage: calculate_balanced_memory_allocation
# Returns: recommendations for each user to fit within system limits
calculate_balanced_memory_allocation() {
echo "Calculating balanced memory allocation..." >&2
# Get total system memory
local total_ram_mb
total_ram_mb=$(free -m | awk '/^Mem:/ {print $2}')
# Reserve memory for system (minimum 2GB or 20% of RAM, whichever is higher)
local reserved_mb
reserved_mb=$((total_ram_mb * 20 / 100))
[ "$reserved_mb" -lt 2048 ] && reserved_mb=2048
local available_mb=$((total_ram_mb - reserved_mb))
# Get all users with FPM pools
local users
users=$(list_all_users)
if [ -z "$users" ]; then
echo "ERROR|No users found"
return 1
fi
# Collect pool data
declare -A pool_traffic # Avg requests per minute
declare -A pool_memory # Avg MB per process
declare -A pool_max # Current max_children
declare -A pool_config_file
local total_traffic=0
local pool_count=0
while IFS= read -r username; do
[ -z "$username" ] && continue
# Find pool config
local pool_config
pool_config=$(find_fpm_pool_config "$username")
[ -z "$pool_config" ] || [ ! -f "$pool_config" ] && continue
pool_count=$((pool_count + 1))
pool_config_file[$username]="$pool_config"
# Get current max_children
local max_children
max_children=$(grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ')
pool_max[$username]=$max_children
# Get average memory per process
local memory_stats
memory_stats=$(calculate_memory_per_process "$username")
local avg_kb
avg_kb=$(echo "$memory_stats" | cut -d'|' -f1)
if [ "$avg_kb" -eq 0 ]; then
avg_kb=$((50 * 1024)) # Default 50MB
fi
pool_memory[$username]=$((avg_kb / 1024))
# Get traffic stats
local traffic
traffic=$(calculate_avg_requests_per_minute "$username" 24 2>/dev/null | cut -d'|' -f1 || echo "1")
[ "$traffic" -eq 0 ] && traffic=1 # Minimum 1 req/min
pool_traffic[$username]=$traffic
total_traffic=$((total_traffic + traffic))
done <<< "$users"
if [ "$pool_count" -eq 0 ]; then
echo "ERROR|No PHP-FPM pools found"
return 1
fi
# Calculate proportional allocation based on traffic
echo "USER|CURRENT_MAX|AVG_MB|TRAFFIC_RPM|RECOMMENDED_MAX|ALLOCATED_MB|REASON"
for username in "${!pool_traffic[@]}"; do
local traffic=${pool_traffic[$username]}
local avg_mb=${pool_memory[$username]}
local current_max=${pool_max[$username]}
# Calculate proportional share of available memory based on traffic
local traffic_percentage=$((traffic * 100 / total_traffic))
local allocated_mb=$((available_mb * traffic_percentage / 100))
# Calculate recommended max_children for this allocation
local recommended_max=$((allocated_mb / avg_mb))
# Apply limits
[ "$recommended_max" -lt 5 ] && recommended_max=5 # Minimum 5
[ "$recommended_max" -gt 200 ] && recommended_max=200 # Maximum 200
# Determine reason
local reason
if [ "$recommended_max" -lt "$current_max" ]; then
reason="REDUCE (prevent OOM)"
elif [ "$recommended_max" -gt "$current_max" ]; then
reason="INCREASE (traffic demands)"
else
reason="OPTIMAL"
fi
echo "$username|$current_max|${avg_mb}MB|$traffic|$recommended_max|${allocated_mb}MB|$reason"
done
}
# Export all functions
export -f analyze_memory_exhausted_errors
export -f analyze_max_children_errors
@@ -726,3 +935,5 @@ export -f analyze_opcache_effectiveness
export -f detect_php_config_issues
export -f analyze_domain_php
export -f convert_to_bytes
export -f calculate_server_memory_capacity
export -f calculate_balanced_memory_allocation