#!/bin/bash # PHP Analysis Engine - Analyzes PHP configurations and identifies issues # Part of Server Toolkit - Phase 2: Analysis # Dependencies: lib/php-detector.sh, lib/system-detect.sh # Source required libraries _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$_LIB_DIR/php-detector.sh" 2>/dev/null || { echo "ERROR: php-detector.sh not found"; return 1; } source "$_LIB_DIR/system-detect.sh" 2>/dev/null || { echo "ERROR: system-detect.sh not found"; return 1; } # ============================================================================ # ERROR LOG ANALYSIS # ============================================================================ # Analyze PHP error logs for memory exhausted errors # Usage: analyze_memory_exhausted_errors # Returns: count|file pairs analyze_memory_exhausted_errors() { local username="$1" local days="${2:-7}" # Default last 7 days local error_logs error_logs=$(find_php_error_logs "$username") if [ -z "$error_logs" ]; then echo "0|No logs found" return fi local total_count=0 local results="" while IFS= read -r log_file; do [ ! -f "$log_file" ] && continue # Find errors in last N days local count count=$(find "$log_file" -mtime -"$days" -exec grep -c "Allowed memory size.*exhausted" {} \; 2>/dev/null || echo "0") if [ "$count" -gt 0 ]; then total_count=$((total_count + count)) results+="$count|$log_file"$'\n' fi done <<< "$error_logs" echo -e "$total_count|TOTAL\n$results" } # Analyze PHP-FPM error logs for max_children errors # Usage: analyze_max_children_errors # Returns: count|timestamp|pool format analyze_max_children_errors() { local username="$1" local days="${2:-7}" local fpm_logs fpm_logs=$(find_fpm_error_logs "$username") if [ -z "$fpm_logs" ]; then echo "0|No FPM logs found" return fi local total_count=0 local results="" while IFS= read -r log_file; do [ ! -f "$log_file" ] && continue # Find max_children errors with timestamps local errors errors=$(find "$log_file" -mtime -"$days" -exec grep -E "server reached (pm\.)?max_children" {} \; 2>/dev/null) if [ -n "$errors" ]; then local count count=$(echo "$errors" | wc -l) total_count=$((total_count + count)) # Extract most recent occurrence local last_occurrence last_occurrence=$(echo "$errors" | tail -1 | grep -oE '\[[0-9]{2}-[A-Za-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}\]' | tr -d '[]') results+="$count|$last_occurrence|$(basename "$log_file")"$'\n' fi done <<< "$fpm_logs" echo -e "$total_count|TOTAL\n$results" } # Analyze slow request logs # Usage: analyze_slow_requests # Returns: count|script|duration format analyze_slow_requests() { local username="$1" local days="${2:-7}" local threshold="${3:-5}" # Default 5 seconds local slow_logs slow_logs=$(find_fpm_slow_logs "$username") if [ -z "$slow_logs" ]; then echo "0|No slow logs found" return fi local total_count=0 local results="" declare -A slow_scripts while IFS= read -r log_file; do [ ! -f "$log_file" ] && continue # Parse slow log format # [pool www] pid 12345 # script_filename = /path/to/script.php # [duration] 7.123456 local entries entries=$(find "$log_file" -mtime -"$days" -exec grep -A2 "^\[" {} \; 2>/dev/null) if [ -n "$entries" ]; then local script="" local duration="" while IFS= read -r line; do if [[ "$line" =~ script_filename.*=\ (.+)$ ]]; then script="${BASH_REMATCH[1]}" elif [[ "$line" =~ ^\[.*\]\ ([0-9]+\.[0-9]+)$ ]]; then duration="${BASH_REMATCH[1]}" # Check if exceeds threshold if [ -n "$script" ] && [ -n "$duration" ]; then local duration_int=${duration%.*} if [ "$duration_int" -ge "$threshold" ]; then total_count=$((total_count + 1)) # Track slowest occurrence per script local is_slower=$(awk "BEGIN {print (${slow_scripts[$script]:-0} < $duration ? 1 : 0)}" 2>/dev/null || echo 1) if [ -z "${slow_scripts[$script]}" ] || [ "$is_slower" -eq 1 ]; then slow_scripts[$script]="$duration" fi fi fi script="" duration="" fi done <<< "$entries" fi done <<< "$slow_logs" # Format results for script in "${!slow_scripts[@]}"; do results+="1|$script|${slow_scripts[$script]}s"$'\n' done echo -e "$total_count|TOTAL\n$results" } # Analyze execution timeout errors # Usage: analyze_execution_timeout_errors analyze_execution_timeout_errors() { local username="$1" local days="${2:-7}" local error_logs error_logs=$(find_php_error_logs "$username") if [ -z "$error_logs" ]; then echo "0|No logs found" return fi local total_count=0 local results="" while IFS= read -r log_file; do [ ! -f "$log_file" ] && continue local count count=$(find "$log_file" -mtime -"$days" -exec grep -c "Maximum execution time.*exceeded" {} \; 2>/dev/null || echo "0") if [ "$count" -gt 0 ]; then total_count=$((total_count + count)) results+="$count|$log_file"$'\n' fi done <<< "$error_logs" echo -e "$total_count|TOTAL\n$results" } # ============================================================================ # RESOURCE USAGE CALCULATIONS # ============================================================================ # Calculate average memory per PHP-FPM process # Usage: calculate_memory_per_process # Returns: average_kb|total_processes|total_memory_mb calculate_memory_per_process() { local username="$1" # Get average KB per process (single number) local avg_kb avg_kb=$(get_fpm_memory_usage "$username") # Get process count local process_count process_count=$(get_fpm_process_count "$username") # Check if no processes found if [ -z "$avg_kb" ] || [ "$avg_kb" -eq 0 ] || [ "$process_count" -eq 0 ]; then echo "0|0|0" return fi # Calculate total memory in MB local total_mb total_mb=$((avg_kb * process_count / 1024)) echo "$avg_kb|$process_count|$total_mb" } # Calculate optimal max_children based on available memory # Usage: calculate_optimal_max_children [reserved_mb] # Returns: recommended_max_children|reason calculate_optimal_max_children() { local username="$1" local reserved_mb="${2:-1024}" # Reserve 1GB for system by default # Get current memory usage local memory_stats memory_stats=$(calculate_memory_per_process "$username") local avg_kb process_count avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) process_count=$(echo "$memory_stats" | cut -d'|' -f2) if [ "$avg_kb" -eq 0 ]; then echo "0|No active processes to measure" return fi # Get total system memory local total_mem_mb total_mem_mb=$(free -m | awk '/^Mem:/ {print $2}') # Calculate available memory for PHP-FPM local available_mb=$((total_mem_mb - reserved_mb)) # Convert average KB to MB local avg_mb=$((avg_kb / 1024)) # Calculate max children (with 20% safety buffer) local theoretical_max=$((available_mb / avg_mb)) local recommended=$((theoretical_max * 80 / 100)) # Sanity checks if [ "$recommended" -lt 5 ]; then recommended=5 echo "$recommended|Minimum safe value (memory very limited)" elif [ "$recommended" -lt "$process_count" ]; then echo "$recommended|WARNING: Less than current process count ($process_count)" else echo "$recommended|Based on ${avg_mb}MB avg per process, ${available_mb}MB available" fi } # Calculate peak concurrent requests from access logs # Usage: calculate_peak_concurrent_requests # Returns: peak_concurrent|timestamp calculate_peak_concurrent_requests() { local username="$1" local days="${2:-1}" # Default last 24 hours # Find access logs local access_logs access_logs=$(find /home/"$username"/*/logs -name "access_log*" -o -name "access.log*" 2>/dev/null) if [ -z "$access_logs" ]; then echo "0|No access logs found" return fi # Analyze logs in 1-second windows to find peak concurrency # This is a simplified estimation based on request timestamps local peak=0 local peak_time="" while IFS= read -r log_file; do [ ! -f "$log_file" ] && continue # Extract timestamps and count requests per second local log_data if [[ "$log_file" == *.gz ]]; then log_data=$(zcat "$log_file" 2>/dev/null || continue) else log_data=$(cat "$log_file" 2>/dev/null || continue) fi # Apache/Nginx common log format timestamp extraction local per_second per_second=$(echo "$log_data" | \ grep -oE '\[[0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2}' | \ sort | uniq -c | sort -rn | head -1) if [ -n "$per_second" ]; then local count timestamp count=$(echo "$per_second" | awk '{print $1}') timestamp=$(echo "$per_second" | awk '{print $2}' | tr -d '[') if [ "$count" -gt "$peak" ]; then peak=$count peak_time=$timestamp fi fi done <<< "$access_logs" echo "$peak|$peak_time" } # Calculate requests per minute average # Usage: calculate_avg_requests_per_minute calculate_avg_requests_per_minute() { local username="$1" local hours="${2:-24}" local access_logs access_logs=$(find /home/"$username"/*/logs -name "access_log" -o -name "access.log" 2>/dev/null | head -1) if [ -z "$access_logs" ]; then echo "0|No access logs" return fi # Count total requests in last N hours local total_requests total_requests=$(find "$access_logs" -mmin -$((hours * 60)) -exec wc -l {} \; 2>/dev/null | awk '{sum+=$1} END {print sum}') if [ -z "$total_requests" ] || [ "$total_requests" -eq 0 ]; then echo "0|No recent requests" return fi # Calculate average per minute local total_minutes=$((hours * 60)) local avg_per_min=$((total_requests / total_minutes)) echo "$avg_per_min|Last $hours hours" } # ============================================================================ # OPCACHE ANALYSIS # ============================================================================ # Analyze OPcache effectiveness # Usage: analyze_opcache_effectiveness # Returns: status|hit_rate|memory_used_mb|cached_scripts|recommendation analyze_opcache_effectiveness() { local username="$1" # Check if OPcache is enabled local enabled enabled=$(check_opcache_enabled "$username") if [ "$enabled" != "1" ]; then echo "DISABLED|0|0|0|Enable OPcache for 40-70% performance boost" return fi # Get OPcache statistics local stats stats=$(get_opcache_stats "$username") if [ -z "$stats" ]; then echo "ENABLED|0|0|0|Unable to retrieve statistics" return fi # Parse statistics local memory_used hits misses cached_scripts max_cached wasted memory_used=$(echo "$stats" | grep "memory_usage_mb" | cut -d'=' -f2) hits=$(echo "$stats" | grep "^hits=" | cut -d'=' -f2) misses=$(echo "$stats" | grep "^misses=" | cut -d'=' -f2) cached_scripts=$(echo "$stats" | grep "num_cached_scripts=" | cut -d'=' -f2) max_cached=$(echo "$stats" | grep "max_cached_keys=" | cut -d'=' -f2) wasted=$(echo "$stats" | grep "wasted_memory_mb=" | cut -d'=' -f2) # Calculate hit rate local hit_rate hit_rate=$(calculate_opcache_hit_rate "$username") # Generate recommendation local recommendation="" local hit_rate_low=$(awk "BEGIN {print ($hit_rate < 90 ? 1 : 0)}" 2>/dev/null || echo 0) local wasted_high=$(awk "BEGIN {print ($wasted > 5 ? 1 : 0)}" 2>/dev/null || echo 0) local cached_high=$(awk "BEGIN {print ($cached_scripts > $max_cached * 0.8 ? 1 : 0)}" 2>/dev/null || echo 0) if [ "$hit_rate_low" -eq 1 ]; then recommendation="Hit rate below 90% - Increase opcache.memory_consumption" elif [ "$wasted_high" -eq 1 ]; then recommendation="High wasted memory (${wasted}MB) - Consider increasing opcache.max_accelerated_files" elif [ "$cached_high" -eq 1 ]; then recommendation="Cached scripts at 80% capacity - Increase opcache.max_accelerated_files" else recommendation="OPcache performing optimally" fi echo "ENABLED|$hit_rate|$memory_used|$cached_scripts|$recommendation" } # ============================================================================ # CONFIGURATION ISSUE DETECTION # ============================================================================ # Detect common PHP configuration issues # Usage: detect_php_config_issues # Returns: multiline issue_type|severity|message|recommendation detect_php_config_issues() { local username="$1" local domain="$2" local issues="" # Get all effective settings local settings settings=$(get_all_php_settings "$username") # Extract key settings local memory_limit upload_max post_max max_execution display_errors memory_limit=$(echo "$settings" | grep "^memory_limit=" | cut -d'=' -f2) upload_max=$(echo "$settings" | grep "^upload_max_filesize=" | cut -d'=' -f2) post_max=$(echo "$settings" | grep "^post_max_size=" | cut -d'=' -f2) max_execution=$(echo "$settings" | grep "^max_execution_time=" | cut -d'=' -f2) display_errors=$(echo "$settings" | grep "^display_errors=" | cut -d'=' -f2) # Convert to bytes for comparison local upload_bytes post_bytes upload_bytes=$(convert_to_bytes "$upload_max") post_bytes=$(convert_to_bytes "$post_max") # ISSUE 1: post_max_size < upload_max_filesize if [ -n "$post_bytes" ] && [ -n "$upload_bytes" ] && [ "$post_bytes" -lt "$upload_bytes" ]; then issues+="CONFIG_MISMATCH|CRITICAL|post_max_size ($post_max) < upload_max_filesize ($upload_max)|Set post_max_size >= upload_max_filesize"$'\n' fi # ISSUE 2: display_errors = On in production if [[ "$display_errors" =~ ^(On|1)$ ]]; then issues+="SECURITY|HIGH|display_errors is enabled|Set display_errors = Off in production (security risk)"$'\n' fi # ISSUE 3: memory_limit too low local memory_bytes memory_bytes=$(convert_to_bytes "$memory_limit") if [ -n "$memory_bytes" ] && [ "$memory_bytes" -lt $((128 * 1024 * 1024)) ]; then issues+="PERFORMANCE|MEDIUM|memory_limit is very low ($memory_limit)|Consider increasing to at least 128M"$'\n' fi # ISSUE 4: Check for max_children errors local max_children_errors max_children_errors=$(analyze_max_children_errors "$username" 7) local error_count error_count=$(echo "$max_children_errors" | grep "TOTAL" | cut -d'|' -f1) if [ -n "$error_count" ] && [ "$error_count" -gt 0 ]; then issues+="CAPACITY|CRITICAL|pm.max_children limit reached $error_count times in last 7 days|Increase pm.max_children setting"$'\n' fi # ISSUE 5: Check for memory exhausted errors local memory_errors memory_errors=$(analyze_memory_exhausted_errors "$username" 7) local memory_error_count memory_error_count=$(echo "$memory_errors" | grep "TOTAL" | cut -d'|' -f1) if [ "$memory_error_count" -gt 0 ]; then issues+="MEMORY|HIGH|Memory exhausted errors occurred $memory_error_count times in last 7 days|Increase memory_limit or optimize code"$'\n' fi # ISSUE 6: OPcache disabled or ineffective local opcache_status opcache_status=$(analyze_opcache_effectiveness "$username") local status hit_rate status=$(echo "$opcache_status" | cut -d'|' -f1) hit_rate=$(echo "$opcache_status" | cut -d'|' -f2) if [ "$status" = "DISABLED" ]; then issues+="PERFORMANCE|HIGH|OPcache is disabled|Enable OPcache for 40-70% performance improvement"$'\n' else local hit_rate_low=$(awk "BEGIN {print ($hit_rate < 90 ? 1 : 0)}" 2>/dev/null || echo 0) if [ "$hit_rate_low" -eq 1 ]; then issues+="PERFORMANCE|MEDIUM|OPcache hit rate is low (${hit_rate}%)|Increase opcache.memory_consumption"$'\n' fi fi # ISSUE 7: Check FPM pool configuration local pool_config pool_config=$(find_fpm_pool_config "$username") if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then local pool_settings pool_settings=$(parse_fpm_pool_config "$pool_config") local pm pm_max_requests pm=$(echo "$pool_settings" | grep "^pm=" | cut -d'=' -f2) pm_max_requests=$(echo "$pool_settings" | grep "^pm.max_requests=" | cut -d'=' -f2) # ISSUE 7a: pm.max_requests = 0 (no process recycling) if [ "$pm_max_requests" = "0" ]; then issues+="MEMORY_LEAK|MEDIUM|pm.max_requests is disabled (0)|Set to 500-1000 to prevent memory leak accumulation"$'\n' fi # ISSUE 7b: pm = static on low-traffic site if [ "$pm" = "static" ]; then local avg_rpm avg_rpm=$(calculate_avg_requests_per_minute "$username" 24 | cut -d'|' -f1) if [ "$avg_rpm" -lt 10 ]; then issues+="RESOURCE_WASTE|LOW|pm=static on low-traffic site ($avg_rpm req/min)|Consider pm=dynamic or pm=ondemand to save memory"$'\n' fi fi fi # Return all issues if [ -z "$issues" ]; then echo "NONE|INFO|No critical issues detected|Configuration appears healthy" else echo -e "$issues" fi } # ============================================================================ # COMPREHENSIVE DOMAIN ANALYSIS # ============================================================================ # Perform complete analysis for a domain # Usage: analyze_domain_php # Returns: JSON-like formatted comprehensive report analyze_domain_php() { local username="$1" local domain="$2" echo "=== PHP Analysis Report for $domain ===" echo "" # 1. PHP Version echo "PHP VERSION:" local php_version php_version=$(detect_php_version_for_domain "$username" "$domain") echo " Version: $php_version" echo "" # 2. Configuration Files echo "CONFIGURATION HIERARCHY:" local configs configs=$(find_all_php_configs "$username" "$domain") local priority=1 while IFS= read -r config; do [ -z "$config" ] && continue echo " Priority $priority: $config" priority=$((priority + 1)) done <<< "$configs" echo "" # 3. Key Settings echo "EFFECTIVE SETTINGS:" local memory_limit upload_max post_max max_exec memory_limit=$(get_effective_php_setting "$username" "memory_limit") upload_max=$(get_effective_php_setting "$username" "upload_max_filesize") post_max=$(get_effective_php_setting "$username" "post_max_size") max_exec=$(get_effective_php_setting "$username" "max_execution_time") echo " memory_limit: $memory_limit" echo " upload_max_filesize: $upload_max" echo " post_max_size: $post_max" echo " max_execution_time: $max_exec" echo "" # 4. PHP-FPM Pool echo "PHP-FPM POOL:" local pool_config pool_config=$(find_fpm_pool_config "$username") if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then local pool_settings pool_settings=$(parse_fpm_pool_config "$pool_config") echo " Config: $pool_config" while IFS= read -r setting; do [ -z "$setting" ] && continue echo " $setting" done <<< "$pool_settings" else echo " Status: No FPM pool config found (using mod_php?)" fi echo "" # 5. Process & Memory Stats echo "RESOURCE USAGE:" local memory_stats memory_stats=$(calculate_memory_per_process "$username") local avg_kb process_count total_mb avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) process_count=$(echo "$memory_stats" | cut -d'|' -f2) total_mb=$(echo "$memory_stats" | cut -d'|' -f3) echo " Current Processes: $process_count" echo " Avg Memory/Process: $((avg_kb / 1024))MB" echo " Total Memory: ${total_mb}MB" echo "" # 6. OPcache Status echo "OPCACHE STATUS:" local opcache_status opcache_status=$(analyze_opcache_effectiveness "$username") local status hit_rate memory_used cached_scripts recommendation status=$(echo "$opcache_status" | cut -d'|' -f1) hit_rate=$(echo "$opcache_status" | cut -d'|' -f2) memory_used=$(echo "$opcache_status" | cut -d'|' -f3) cached_scripts=$(echo "$opcache_status" | cut -d'|' -f4) recommendation=$(echo "$opcache_status" | cut -d'|' -f5) echo " Status: $status" if [ "$status" = "ENABLED" ]; then echo " Hit Rate: ${hit_rate}%" echo " Memory Used: ${memory_used}MB" echo " Cached Scripts: $cached_scripts" fi echo " Recommendation: $recommendation" echo "" # 7. Traffic Analysis echo "TRAFFIC ANALYSIS (Last 24h):" local avg_rpm peak_concurrent avg_rpm=$(calculate_avg_requests_per_minute "$username" 24) peak_concurrent=$(calculate_peak_concurrent_requests "$username" 1) echo " Avg Requests/Min: $(echo "$avg_rpm" | cut -d'|' -f1)" echo " Peak Concurrent: $(echo "$peak_concurrent" | cut -d'|' -f1)" echo "" # 8. Error Analysis echo "ERROR ANALYSIS (Last 7 days):" local memory_errors max_children_errors timeout_errors slow_requests memory_errors=$(analyze_memory_exhausted_errors "$username" 7 | grep "TOTAL" | cut -d'|' -f1) max_children_errors=$(analyze_max_children_errors "$username" 7 | grep "TOTAL" | cut -d'|' -f1) timeout_errors=$(analyze_execution_timeout_errors "$username" 7 | grep "TOTAL" | cut -d'|' -f1) slow_requests=$(analyze_slow_requests "$username" 7 5 | grep "TOTAL" | cut -d'|' -f1) echo " Memory Exhausted: $memory_errors" echo " Max Children Reached: $max_children_errors" echo " Execution Timeouts: $timeout_errors" echo " Slow Requests (>5s): $slow_requests" echo "" # 9. Issues & Recommendations echo "ISSUES DETECTED:" local issues issues=$(detect_php_config_issues "$username" "$domain") while IFS='|' read -r issue_type severity message recommendation; do [ -z "$issue_type" ] && continue echo " [$severity] $issue_type: $message" echo " → $recommendation" done <<< "$issues" echo "" # 10. Optimization Recommendations echo "OPTIMIZATION RECOMMENDATIONS:" # Calculate optimal max_children local optimal_max_children optimal_max_children=$(calculate_optimal_max_children "$username" 1024) local recommended reason recommended=$(echo "$optimal_max_children" | cut -d'|' -f1) reason=$(echo "$optimal_max_children" | cut -d'|' -f2) local current_max_children if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then current_max_children=$(grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') if [ "$recommended" -ne "$current_max_children" ]; then echo " 1. Adjust pm.max_children from $current_max_children to $recommended" echo " Reason: $reason" fi fi echo "" echo "=== End of Report ===" } # ============================================================================ # HELPER FUNCTIONS # ============================================================================ # Convert human-readable size to bytes # Usage: convert_to_bytes "128M" convert_to_bytes() { local size="$1" # Remove whitespace size=$(echo "$size" | tr -d ' ') # Extract number and unit local number="${size//[^0-9]/}" local unit="${size//[0-9]/}" # Default to bytes if no unit [ -z "$unit" ] && echo "$number" && return # Convert based on unit case "${unit^^}" in K|KB) echo $((number * 1024)) ;; M|MB) echo $((number * 1024 * 1024)) ;; G|GB) echo $((number * 1024 * 1024 * 1024)) ;; *) echo "$number" ;; 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 and their domains while IFS= read -r username; do [ -z "$username" ] && continue # Get all domains for this user local user_domains user_domains=$(get_user_domains "$username") while IFS= read -r domain; do [ -z "$domain" ] && continue # Find FPM pool config for this domain local pool_config pool_config=$(find_fpm_pool_config "$username" "$domain") 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 for this username local avg_kb=0 local memory_stats memory_stats=$(calculate_memory_per_process "$username") avg_kb=$(echo "$memory_stats" | cut -d'|' -f1) if [ -z "$avg_kb" ] || [ "$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+="$domain|$username|$max_children|${avg_mb}MB|${pool_max_mb}MB"$'\n' done <<< "$user_domains" 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 - first line is summary echo "$total_required_mb|$total_ram_mb|$percentage|$status|$pool_count pools|$total_max_children total max_children" # Return details as additional lines (not to stderr) echo "$details" } # 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 export -f analyze_slow_requests export -f analyze_execution_timeout_errors export -f calculate_memory_per_process export -f calculate_optimal_max_children export -f calculate_peak_concurrent_requests export -f calculate_avg_requests_per_minute 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