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