4d745f203e
Implement data-driven optimization using actual server metrics instead of thresholds: NEW FEATURES: - lib/php-analytics.sh: Analytics engine for domain profiling • analyze_memory_errors_from_logs: Parse error logs for memory exhaustion • analyze_process_memory_usage: Measure actual PHP process memory via ps • get_peak_concurrent_detailed: Extract peak concurrent requests from access logs • detect_memory_leak_pattern: Identify domains with memory leak issues • build_domain_profile: Complete profile with all real usage data • Intelligent recommendations based on ACTUAL peak memory, traffic, and leak patterns - modules/performance/php-domain-analyzer.sh: Pre-analysis script • Scans all domains and builds comprehensive profiles • Stores profiles in /tmp/php-domain-profiles/ for use by optimizer • Shows summary with top memory users, traffic patterns, and potential leaks • Displays analysis in real-time with progress indicators - php-optimizer.sh: Profile-based optimization levels • Option 0: Run pre-analysis to collect real usage data • Levels 1-5: Now use profile-based recommendations (fallback to traffic-based if no profiles) • Shows real usage data from profiles when optimizations applied • Memory recommendations: peak_memory_seen + 20% buffer • Max children: peak_concurrent_requests + 30% safety margin • Max requests: 250 for leak-prone domains, 500 for normal domains ARCHITECTURE: - Profile format (pipe-delimited): domain|username|peak_concurrent|avg_concurrent| total_hits|min_mem|max_mem|avg_mem|proc_count|mem_exhausted|peak_mem_seen| leak_type|current_memory_limit|current_max_children - Profiles cached in /tmp/php-domain-profiles/ (24 hour TTL) - All 5 optimization levels now profile-aware - Seamless fallback to traffic-based method if no profiles exist CONVERSION COMPLETED: - Level 1: Optimizes pm.max_children only (profile-aware) - Level 2: pm.max_children + memory_limit (profile-aware) - Level 3: All of above + pm.max_requests for leak prevention (profile-aware) - Level 4: OPcache optimization (unchanged) - Level 5: Complete optimization with all settings (NOW PROFILE-AWARE - FIXED) All levels now enumeraate users/domains directly and use profile recommendations when available, with intelligent fallback to the original traffic-based method. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
391 lines
13 KiB
Bash
Executable File
391 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# PHP Analytics Library
|
|
# Analyzes real usage data to make intelligent optimization decisions
|
|
# Parses logs, process memory, and builds accurate domain profiles
|
|
|
|
# ============================================================================
|
|
# ERROR LOG ANALYSIS - Find memory-related issues
|
|
# ============================================================================
|
|
|
|
# Parse PHP-FPM error logs for memory exhaustion errors
|
|
analyze_memory_errors_from_logs() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
local days="${3:-7}"
|
|
|
|
local log_files
|
|
log_files=$(find_php_error_logs "$username" "$domain")
|
|
|
|
local memory_exhausted_count=0
|
|
local memory_limit_errors=0
|
|
local peak_memory_seen=0
|
|
|
|
# Look for memory exhaustion patterns
|
|
while IFS= read -r log_file; do
|
|
[ -z "$log_file" ] && continue
|
|
[ ! -f "$log_file" ] && continue
|
|
|
|
# Count "Allowed memory size exhausted" errors
|
|
local exhausted_in_file
|
|
exhausted_in_file=$(\grep -c "Allowed memory size of" "$log_file" 2>/dev/null || echo 0)
|
|
exhausted_in_file=${exhausted_in_file##[[:space:]]}
|
|
exhausted_in_file=${exhausted_in_file%%[[:space:]]}
|
|
memory_exhausted_count=$((memory_exhausted_count + exhausted_in_file))
|
|
|
|
# Count memory limit exceeded
|
|
local limit_errors_in_file
|
|
limit_errors_in_file=$(\grep -c "memory_limit" "$log_file" 2>/dev/null || echo 0)
|
|
limit_errors_in_file=${limit_errors_in_file##[[:space:]]}
|
|
limit_errors_in_file=${limit_errors_in_file%%[[:space:]]}
|
|
memory_limit_errors=$((memory_limit_errors + limit_errors_in_file))
|
|
|
|
# Extract peak memory from logs (format: "Allowed memory size of 134217728 bytes exhausted")
|
|
local mem_values
|
|
mem_values=$(\grep -o "Allowed memory size of [0-9]* bytes" "$log_file" 2>/dev/null | \grep -o "[0-9]*" | sort -rn | head -1)
|
|
|
|
if [ -n "$mem_values" ]; then
|
|
# Convert bytes to MB
|
|
local mem_mb=$((mem_values / 1048576))
|
|
if [ "$mem_mb" -gt "$peak_memory_seen" ]; then
|
|
peak_memory_seen=$mem_mb
|
|
fi
|
|
fi
|
|
done <<< "$log_files"
|
|
|
|
# Return: exhausted_count|limit_errors|peak_memory_mb
|
|
echo "$memory_exhausted_count|$memory_limit_errors|$peak_memory_seen"
|
|
}
|
|
|
|
# Find PHP error log files for a domain
|
|
find_php_error_logs() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
# cPanel locations
|
|
if [ -d "/home/$username" ]; then
|
|
find "/home/$username" -name "error_log" 2>/dev/null | head -5
|
|
fi
|
|
|
|
# PHP-FPM error logs
|
|
if [ -d "/var/log/php-fpm" ]; then
|
|
find "/var/log/php-fpm" -name "*error*" 2>/dev/null | head -5
|
|
fi
|
|
|
|
# Common log locations
|
|
[ -f "/var/log/php.log" ] && echo "/var/log/php.log"
|
|
[ -f "/var/log/php-errors.log" ] && echo "/var/log/php-errors.log"
|
|
}
|
|
|
|
# ============================================================================
|
|
# PROCESS MEMORY ANALYSIS - Measure actual memory usage
|
|
# ============================================================================
|
|
|
|
# Analyze PHP process memory for a domain
|
|
analyze_process_memory_usage() {
|
|
local username="$1"
|
|
|
|
# Get current running PHP processes for this user
|
|
local processes
|
|
processes=$(ps aux | \grep -E "php-fpm.*$username|_www.*php" | \grep -v grep)
|
|
|
|
if [ -z "$processes" ]; then
|
|
echo "0|0|0|0" # min|max|avg|count
|
|
return
|
|
fi
|
|
|
|
local mem_values=()
|
|
local min_mem=999999
|
|
local max_mem=0
|
|
local total_mem=0
|
|
local count=0
|
|
|
|
# Extract memory (RSS) from ps output
|
|
while IFS= read -r line; do
|
|
local rss=$(echo "$line" | awk '{print $6}')
|
|
if [ -n "$rss" ] && [[ "$rss" =~ ^[0-9]+$ ]]; then
|
|
mem_values+=("$rss")
|
|
total_mem=$((total_mem + rss))
|
|
count=$((count + 1))
|
|
|
|
if [ "$rss" -lt "$min_mem" ]; then
|
|
min_mem=$rss
|
|
fi
|
|
if [ "$rss" -gt "$max_mem" ]; then
|
|
max_mem=$rss
|
|
fi
|
|
fi
|
|
done <<< "$processes"
|
|
|
|
if [ "$count" -eq 0 ]; then
|
|
echo "0|0|0|0"
|
|
return
|
|
fi
|
|
|
|
local avg_mem=$((total_mem / count))
|
|
|
|
# Convert to MB
|
|
min_mem=$((min_mem / 1024))
|
|
max_mem=$((max_mem / 1024))
|
|
avg_mem=$((avg_mem / 1024))
|
|
|
|
# Return: min_mb|max_mb|avg_mb|count
|
|
echo "$min_mem|$max_mem|$avg_mem|$count"
|
|
}
|
|
|
|
# ============================================================================
|
|
# TRAFFIC PATTERN ANALYSIS - Understand domain load
|
|
# ============================================================================
|
|
|
|
# Get peak concurrent requests from access logs
|
|
get_peak_concurrent_detailed() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
local log_file
|
|
log_file=$(find_domain_access_log "$domain" "$username")
|
|
|
|
if [ -z "$log_file" ] || [ ! -f "$log_file" ]; then
|
|
echo "0|0|0" # peak|avg|stddev
|
|
return
|
|
fi
|
|
|
|
# Analyze timestamps to find peak concurrency
|
|
local timestamps
|
|
timestamps=$(awk '{print $4}' "$log_file" 2>/dev/null | sed 's/\[//;s/\/.*//' | sort | uniq -c | sort -rn | head -1)
|
|
|
|
local peak_concurrent=$(echo "$timestamps" | awk '{print $1}')
|
|
peak_concurrent=${peak_concurrent:-0}
|
|
|
|
# Calculate average concurrent
|
|
local total_hits=$(wc -l < "$log_file")
|
|
local unique_seconds=$(awk '{print $4}' "$log_file" 2>/dev/null | sed 's/\[//;s/\/.*//' | sort -u | wc -l)
|
|
local avg_concurrent=0
|
|
|
|
if [ "$unique_seconds" -gt 0 ]; then
|
|
avg_concurrent=$((total_hits / unique_seconds))
|
|
fi
|
|
|
|
# Return: peak|avg|total_hits
|
|
echo "$peak_concurrent|$avg_concurrent|$total_hits"
|
|
}
|
|
|
|
# ============================================================================
|
|
# MEMORY GROWTH DETECTION - Find memory leaks
|
|
# ============================================================================
|
|
|
|
# Detect if domain has memory leak pattern
|
|
detect_memory_leak_pattern() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
# Check error logs for progressive memory growth
|
|
local error_analysis
|
|
error_analysis=$(analyze_memory_errors_from_logs "$username" "$domain")
|
|
|
|
local memory_exhausted_count=$(echo "$error_analysis" | cut -d'|' -f1)
|
|
local peak_memory=$(echo "$error_analysis" | cut -d'|' -f3)
|
|
|
|
# If many memory exhausted errors with growing peak memory, likely a leak
|
|
if [ "$memory_exhausted_count" -gt 5 ] && [ "$peak_memory" -gt 200 ]; then
|
|
echo "LIKELY_LEAK|High memory exhaustion errors ($memory_exhausted_count) detected"
|
|
return 0
|
|
fi
|
|
|
|
# Check if max_requests is 0 (process never recycled)
|
|
local pool_config
|
|
pool_config=$(find_fpm_pool_config "$username")
|
|
|
|
if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then
|
|
local max_requests
|
|
max_requests=$(\grep "^pm.max_requests" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ')
|
|
|
|
if [ "$max_requests" = "0" ]; then
|
|
echo "NEEDS_RECYCLING|pm.max_requests is disabled (0) - processes never recycled"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
echo "NO_LEAK|Normal memory patterns"
|
|
return 1
|
|
}
|
|
|
|
# ============================================================================
|
|
# DOMAIN PROFILE BUILDER - Comprehensive analysis
|
|
# ============================================================================
|
|
|
|
# Build complete profile for a domain
|
|
build_domain_profile() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
# Get memory errors
|
|
local memory_errors
|
|
memory_errors=$(analyze_memory_errors_from_logs "$username" "$domain")
|
|
local mem_exhausted=$(echo "$memory_errors" | cut -d'|' -f1)
|
|
local mem_limit_errors=$(echo "$memory_errors" | cut -d'|' -f2)
|
|
local peak_mem_seen=$(echo "$memory_errors" | cut -d'|' -f3)
|
|
|
|
# Get current process memory
|
|
local process_mem
|
|
process_mem=$(analyze_process_memory_usage "$username")
|
|
local min_mem=$(echo "$process_mem" | cut -d'|' -f1)
|
|
local max_mem=$(echo "$process_mem" | cut -d'|' -f2)
|
|
local avg_mem=$(echo "$process_mem" | cut -d'|' -f3)
|
|
local proc_count=$(echo "$process_mem" | cut -d'|' -f4)
|
|
|
|
# Get traffic patterns
|
|
local traffic
|
|
traffic=$(get_peak_concurrent_detailed "$username" "$domain")
|
|
local peak_concurrent=$(echo "$traffic" | cut -d'|' -f1)
|
|
local avg_concurrent=$(echo "$traffic" | cut -d'|' -f2)
|
|
local total_hits=$(echo "$traffic" | cut -d'|' -f3)
|
|
|
|
# Detect memory leaks
|
|
local leak_status
|
|
leak_status=$(detect_memory_leak_pattern "$username" "$domain")
|
|
local leak_type=$(echo "$leak_status" | cut -d'|' -f1)
|
|
local leak_note=$(echo "$leak_status" | cut -d'|' -f2)
|
|
|
|
# Get current settings
|
|
local current_memory_limit
|
|
current_memory_limit=$(get_effective_php_setting "$username" "memory_limit")
|
|
local pool_config
|
|
pool_config=$(find_fpm_pool_config "$username")
|
|
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 ' ')
|
|
fi
|
|
|
|
# Format: domain|username|peak_concurrent|avg_concurrent|total_hits|min_mem|max_mem|avg_mem|proc_count|mem_exhausted|peak_mem_seen|leak_type|current_memory_limit|current_max_children
|
|
echo "$domain|$username|$peak_concurrent|$avg_concurrent|$total_hits|$min_mem|$max_mem|$avg_mem|$proc_count|$mem_exhausted|$peak_mem_seen|$leak_type|$current_memory_limit|$current_max_children"
|
|
}
|
|
|
|
# ============================================================================
|
|
# INTELLIGENT RECOMMENDATIONS - Based on real data
|
|
# ============================================================================
|
|
|
|
# Calculate memory_limit based on ACTUAL usage, not thresholds
|
|
calculate_memory_limit_from_actual_usage() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
# Get real data
|
|
local memory_errors
|
|
memory_errors=$(analyze_memory_errors_from_logs "$username" "$domain")
|
|
local peak_mem_seen=$(echo "$memory_errors" | cut -d'|' -f3)
|
|
|
|
local process_mem
|
|
process_mem=$(analyze_process_memory_usage "$username")
|
|
local max_mem=$(echo "$process_mem" | cut -d'|' -f2)
|
|
|
|
# Determine optimal memory_limit
|
|
local recommended_memory=128
|
|
|
|
# If we've seen memory exhaustion, use observed peak + 20% buffer
|
|
if [ "$peak_mem_seen" -gt 0 ]; then
|
|
recommended_memory=$((peak_mem_seen + (peak_mem_seen / 5)))
|
|
elif [ "$max_mem" -gt 0 ]; then
|
|
# Use max observed process memory + 30% buffer for growth
|
|
recommended_memory=$((max_mem + (max_mem / 3)))
|
|
fi
|
|
|
|
# Ensure minimum of 64M and maximum of 1024M
|
|
[ "$recommended_memory" -lt 64 ] && recommended_memory=64
|
|
[ "$recommended_memory" -gt 1024 ] && recommended_memory=1024
|
|
|
|
echo "${recommended_memory}M"
|
|
}
|
|
|
|
# Calculate max_children based on ACTUAL peak concurrent
|
|
calculate_max_children_from_actual_usage() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
# Get real peak concurrent from logs
|
|
local traffic
|
|
traffic=$(get_peak_concurrent_detailed "$username" "$domain")
|
|
local peak_concurrent=$(echo "$traffic" | cut -d'|' -f1)
|
|
|
|
# Add 30% safety margin for traffic spikes
|
|
local recommended_max_children=$((peak_concurrent + (peak_concurrent / 3)))
|
|
|
|
# Minimum of 5, maximum of 100
|
|
[ "$recommended_max_children" -lt 5 ] && recommended_max_children=5
|
|
[ "$recommended_max_children" -gt 100 ] && recommended_max_children=100
|
|
|
|
echo "$recommended_max_children"
|
|
}
|
|
|
|
# Calculate max_requests based on memory leak patterns
|
|
calculate_max_requests_from_actual_usage() {
|
|
local username="$1"
|
|
local domain="$2"
|
|
|
|
# Default: recycle every 500 requests
|
|
local recommended_requests=500
|
|
|
|
# Check if memory leak detected
|
|
local leak_status
|
|
leak_status=$(detect_memory_leak_pattern "$username" "$domain")
|
|
local leak_type=$(echo "$leak_status" | cut -d'|' -f1)
|
|
|
|
# If leak detected, recycle more frequently
|
|
if [ "$leak_type" = "LIKELY_LEAK" ]; then
|
|
recommended_requests=250 # Recycle more often
|
|
fi
|
|
|
|
echo "$recommended_requests"
|
|
}
|
|
|
|
# ============================================================================
|
|
# PROFILE STORAGE AND RETRIEVAL
|
|
# ============================================================================
|
|
|
|
# Store domain profile to file
|
|
store_domain_profile() {
|
|
local profile="$1"
|
|
local profile_dir="/tmp/php-domain-profiles"
|
|
|
|
mkdir -p "$profile_dir" 2>/dev/null
|
|
|
|
local domain=$(echo "$profile" | cut -d'|' -f1)
|
|
echo "$profile" > "$profile_dir/$domain.profile"
|
|
}
|
|
|
|
# Retrieve stored profile
|
|
get_stored_profile() {
|
|
local domain="$1"
|
|
local profile_dir="/tmp/php-domain-profiles"
|
|
|
|
[ -f "$profile_dir/$domain.profile" ] && cat "$profile_dir/$domain.profile"
|
|
}
|
|
|
|
# Get all stored profiles
|
|
get_all_stored_profiles() {
|
|
local profile_dir="/tmp/php-domain-profiles"
|
|
|
|
[ -d "$profile_dir" ] && cat "$profile_dir"/*.profile 2>/dev/null
|
|
}
|
|
|
|
# Clear old profiles (older than 24 hours)
|
|
cleanup_old_profiles() {
|
|
local profile_dir="/tmp/php-domain-profiles"
|
|
|
|
[ ! -d "$profile_dir" ] && return
|
|
|
|
find "$profile_dir" -name "*.profile" -mtime +0 -delete 2>/dev/null
|
|
}
|
|
|
|
export -f analyze_memory_errors_from_logs
|
|
export -f analyze_process_memory_usage
|
|
export -f get_peak_concurrent_detailed
|
|
export -f detect_memory_leak_pattern
|
|
export -f build_domain_profile
|
|
export -f calculate_memory_limit_from_actual_usage
|
|
export -f calculate_max_children_from_actual_usage
|
|
export -f calculate_max_requests_from_actual_usage
|
|
export -f store_domain_profile
|
|
export -f get_stored_profile
|
|
export -f get_all_stored_profiles
|
|
export -f cleanup_old_profiles
|