eda451093f
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!
940 lines
31 KiB
Bash
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
|