Files
Linux-Server-Management-Too…/lib/php-analyzer.sh
T
cschantz 257e846685 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!
2025-12-02 20:39:20 -05:00

940 lines
31 KiB
Bash

#!/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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/php-detector.sh" 2>/dev/null || { echo "ERROR: php-detector.sh not found"; exit 1; }
source "$SCRIPT_DIR/system-detect.sh" 2>/dev/null || { echo "ERROR: system-detect.sh not found"; exit 1; }
# ============================================================================
# ERROR LOG ANALYSIS
# ============================================================================
# Analyze PHP error logs for memory exhausted errors
# Usage: analyze_memory_exhausted_errors <username> <days>
# 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 <username> <days>
# 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 <username> <days> <threshold_seconds>
# 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
if [ -z "${slow_scripts[$script]}" ] || (( $(echo "${slow_scripts[$script]} < $duration" | bc -l) )); 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 <username> <days>
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 <username>
# Returns: average_kb|total_processes|total_memory_mb
calculate_memory_per_process() {
local username="$1"
local memory_stats
memory_stats=$(get_fpm_memory_usage "$username")
if [ -z "$memory_stats" ] || [[ "$memory_stats" == "0|0" ]]; then
echo "0|0|0"
return
fi
local avg_kb total_mb
avg_kb=$(echo "$memory_stats" | cut -d'|' -f1)
total_mb=$(echo "$memory_stats" | cut -d'|' -f2)
local process_count
process_count=$(get_fpm_process_count "$username")
echo "$avg_kb|$process_count|$total_mb"
}
# Calculate optimal max_children based on available memory
# Usage: calculate_optimal_max_children <username> [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 <username> <days>
# 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 <username> <hours>
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 <username>
# 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=""
if (( $(echo "$hit_rate < 90" | bc -l) )); then
recommendation="Hit rate below 90% - Increase opcache.memory_consumption"
elif (( $(echo "$wasted > 5" | bc -l) )); then
recommendation="High wasted memory (${wasted}MB) - Consider increasing opcache.max_accelerated_files"
elif (( $(echo "$cached_scripts > $max_cached * 0.8" | bc -l) )); 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 <username> <domain>
# 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 [ "$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 [ "$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 [ "$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)
error_count=$(echo "$memory_errors" | grep "TOTAL" | cut -d'|' -f1)
if [ "$error_count" -gt 0 ]; then
issues+="MEMORY|HIGH|Memory exhausted errors occurred $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'
elif (( $(echo "$hit_rate < 90" | bc -l) )); then
issues+="PERFORMANCE|MEDIUM|OPcache hit rate is low (${hit_rate}%)|Increase opcache.memory_consumption"$'\n'
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 <username> <domain>
# 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 "$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'|' -f2)
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
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
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