From 257e846685ba489a506b07d2c2329a6f31fbbe35 Mon Sep 17 00:00:00 2001 From: cschantz Date: Tue, 2 Dec 2025 20:39:20 -0500 Subject: [PATCH] Add server-wide memory capacity check (Option 9) - Critical OOM prevention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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! --- lib/php-analyzer.sh | 211 +++++++++++++++++++++++++++ modules/performance/php-optimizer.sh | 143 ++++++++++++++++++ 2 files changed, 354 insertions(+) diff --git a/lib/php-analyzer.sh b/lib/php-analyzer.sh index df9f171..0cd840f 100644 --- a/lib/php-analyzer.sh +++ b/lib/php-analyzer.sh @@ -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 diff --git a/modules/performance/php-optimizer.sh b/modules/performance/php-optimizer.sh index 6eda6ac..7940cd8 100755 --- a/modules/performance/php-optimizer.sh +++ b/modules/performance/php-optimizer.sh @@ -50,6 +50,7 @@ show_main_menu() { cecho " ${GREEN}6${NC}) View OPcache Statistics" cecho " ${GREEN}7${NC}) View PHP-FPM Process Stats" cecho " ${GREEN}8${NC}) Check for Configuration Issues" + cecho " ${GREEN}9${NC}) Check Server Memory Capacity (OOM Risk)" echo "" cecho " ${YELLOW}b${NC}) Backup Current Configurations" cecho " ${YELLOW}r${NC}) Restore from Backup" @@ -722,6 +723,145 @@ check_config_issues() { read -p "Press Enter to continue..." } +# ============================================================================ +# OPTION 9: CHECK SERVER MEMORY CAPACITY +# ============================================================================ + +check_server_memory_capacity() { + show_banner + cecho "${WHITE}${BOLD}SERVER MEMORY CAPACITY CHECK${NC}" + echo "" + cecho "${YELLOW}This checks if all PHP-FPM pools hitting max_children would cause OOM...${NC}" + echo "" + + # Run capacity analysis + cecho "${CYAN}Analyzing PHP-FPM memory requirements...${NC}" + echo "" + + local result + result=$(calculate_server_memory_capacity 2>&1) + + # Parse result (main output is last line) + local main_result + main_result=$(echo "$result" | tail -1) + + local total_required total_ram percentage status details + total_required=$(echo "$main_result" | cut -d'|' -f1) + total_ram=$(echo "$main_result" | cut -d'|' -f2) + percentage=$(echo "$main_result" | cut -d'|' -f3) + status=$(echo "$main_result" | cut -d'|' -f4) + + # Display summary + cecho "${CYAN}═══════════════════════════════════════════════════════════════════${NC}" + cecho "${WHITE}${BOLD}MEMORY CAPACITY ANALYSIS${NC}" + cecho "${CYAN}═══════════════════════════════════════════════════════════════════${NC}" + echo "" + cecho " Total Server RAM: ${WHITE}${total_ram}MB${NC}" + cecho " Required if ALL pools at max_children: ${WHITE}${total_required}MB${NC}" + cecho " Percentage of RAM: ${WHITE}${percentage}%${NC}" + echo "" + + # Display status with color + case "$status" in + CRITICAL) + cecho " Status: ${RED}${BOLD}CRITICAL - HIGH OOM RISK!${NC}" + cecho "" + cecho " ${RED}WARNING: If all PHP-FPM pools hit their max_children limit,${NC}" + cecho " ${RED}the server will likely run out of memory and kill processes!${NC}" + ;; + WARNING) + cecho " Status: ${YELLOW}${BOLD}WARNING - MODERATE OOM RISK${NC}" + cecho "" + cecho " ${YELLOW}CAUTION: Memory usage is high. Some pools may need reduction.${NC}" + ;; + CAUTION) + cecho " Status: ${YELLOW}${BOLD}CAUTION - WATCH MEMORY USAGE${NC}" + cecho "" + cecho " ${YELLOW}Memory usage is elevated but manageable.${NC}" + ;; + HEALTHY) + cecho " Status: ${GREEN}${BOLD}HEALTHY - LOW OOM RISK${NC}" + cecho "" + cecho " ${GREEN}Memory allocation appears safe.${NC}" + ;; + esac + + echo "" + cecho "${CYAN}═══════════════════════════════════════════════════════════════════${NC}" + echo "" + + # Ask if user wants detailed breakdown + read -p "Show detailed per-user breakdown? (y/n): " show_details + + if [[ "$show_details" =~ ^[Yy]$ ]]; then + echo "" + cecho "${WHITE}${BOLD}PER-USER BREAKDOWN${NC}" + cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" + echo "" + + # Get details from stderr of previous call + local details_output + details_output=$(echo "$result" | head -n -1) + + printf "%-20s %12s %12s %12s\n" "USER" "MAX_CHILDREN" "AVG/PROCESS" "MAX_MEMORY" + printf "%-20s %12s %12s %12s\n" "--------------------" "------------" "------------" "------------" + + while IFS='|' read -r username max_children avg_mb pool_max_mb; do + [ -z "$username" ] && continue + printf "%-20s %12s %12s %12s\n" "$username" "$max_children" "$avg_mb" "$pool_max_mb" + done <<< "$details_output" + + echo "" + fi + + # Ask if user wants balanced recommendations + echo "" + read -p "Calculate balanced memory allocation recommendations? (y/n): " show_recommendations + + if [[ "$show_recommendations" =~ ^[Yy]$ ]]; then + echo "" + cecho "${WHITE}${BOLD}BALANCED MEMORY ALLOCATION RECOMMENDATIONS${NC}" + cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" + echo "" + cecho "${YELLOW}Calculating optimal max_children based on traffic...${NC}" + echo "" + + local recommendations + recommendations=$(calculate_balanced_memory_allocation 2>/dev/null) + + # Display header + local header + header=$(echo "$recommendations" | head -1) + echo "$header" | awk -F'|' '{printf "%-15s %8s %10s %12s %12s %15s %20s\n", $1, $2, $3, $4, $5, $6, $7}' + echo "--------------------------------------------------------------------------------------------------------" + + # Display recommendations + echo "$recommendations" | tail -n +2 | while IFS='|' read -r user current_max avg_mb traffic_rpm recommended_max allocated_mb reason; do + [ -z "$user" ] && continue + + # Color code reason + if [[ "$reason" == *"REDUCE"* ]]; then + color="${RED}" + elif [[ "$reason" == *"INCREASE"* ]]; then + color="${GREEN}" + else + color="${WHITE}" + fi + + printf "%-15s %8s %10s %12s ${color}%12s${NC} %15s %20s\n" "$user" "$current_max" "$avg_mb" "$traffic_rpm" "$recommended_max" "$allocated_mb" "$reason" + done + + echo "" + cecho "${YELLOW}NOTE: These are recommendations based on proportional traffic.${NC}" + cecho "${YELLOW}Actual needs may vary. Always test changes carefully.${NC}" + echo "" + fi + + echo "" + cecho "${CYAN}═══════════════════════════════════════════════════════════════════${NC}" + read -p "Press Enter to continue..." +} + # ============================================================================ # MAIN LOOP # ============================================================================ @@ -775,6 +915,9 @@ main() { 8) check_config_issues ;; + 9) + check_server_memory_capacity + ;; b) cecho "${YELLOW}Backup feature not yet implemented${NC}" sleep 2