My Journey to Optimize PHP-FPM for Kubernetes

My Journey to Optimize PHP-FPM for Kubernetes
Photo by Ben Griffiths / Unsplash

Optimizing PHP-FPM within a Kubernetes environment felt like a daunting task at first. However, through trial, error, and success, I've compiled insights and code snippets that I hope will guide you as much as they enlightened me. Let's dive into how I fine-tuned PHP settings and PHP-FPM pool directives to enhance the performance of my applications.

The Quest for Understanding Kubernetes Pod Memory Limits

Kubernetes pods come with their set of rules, among which memory limits are critical. Exceeding these limits meant my applications were at risk of being terminated unexpectedly. Here's how I started to make sense of it all:

1. Auto-Detecting Pod Memory Limits

The first step was to understand the memory environment in which my applications were operating. This was crucial for ensuring that I didn't allocate more memory to PHP than was available in the pod, which could lead to processes being killed due to exceeding limits.

get_pod_memory_limit_in_mb() {
    local memory_path="/sys/fs/cgroup/memory.max"

    if [[ ! -f "$memory_path" ]]; then
        echo 2048  # Default memory in MB if not set
        return
    fi

    local memory_limit_bytes=$(cat "$memory_path")
    if [[ "$memory_limit_bytes" == "max" ]]; then
        echo "Unlimited"
    else
        echo $((memory_limit_bytes / 1024 / 1024))  # Convert bytes to MB
    fi
}

Explanation:

  • memory_path: This variable points to the file path where the memory limit for the pod is stored.
  • File existence check: If the file doesn't exist, it assumes a default memory limit of 2048MB.
  • Memory limit reading: Reads the memory limit directly from the system file and checks if it's "max", indicating no limit, or calculates the limit in megabytes.

2. Dynamically Setting PHP memory_limit

After determining the available memory, I needed to adjust PHP's memory_limit to prevent it from exceeding the pod's limit.

update_php_settings() {
    local php_memory_limit=$(calculate_usable_memory)

    if [[ "$php_memory_limit" == "Unlimited" ]]; then
        php_memory_limit=2048  # Fallback limit if unlimited
    fi

    echo "Updating PHP settings..."
    sudo sed -i "s/memory_limit\s*=.*/memory_limit = ${php_memory_limit}M/g" /etc/php7/php.ini
}

Explanation:

  • Calculating usable memory: It first calculates the usable memory, considering a buffer to avoid hitting the limit.
  • Default limit for "Unlimited": If the memory is "Unlimited", it sets a sensible default.
  • Updating php.ini: Uses sed to replace the memory_limit value in the php.ini file.

3. Optimizing PHP-FPM Child Processes

Efficiently configuring PHP-FPM's child processes was critical for balancing performance and resource usage.

calculate_php_fpm_max_children() {
    local usable_memory_mb=$(calculate_usable_memory)

    if [[ "$usable_memory_mb" == "Unlimited" ]]; then
        usable_memory_mb=2048  # Set a default usable memory if unlimited
    fi

    local process_size_kb=40000
    local memtotal_kb=$(( usable_memory_mb * 1024 ))

    local children=$((memtotal_kb / process_size_kb))

    if [[ ! "$children" =~ ^[0-9]+$ || $children -eq 0 ]]; then
        echo 1
    else
        echo $children
    fi
}

configure_php_fpm_children() {
    local php_fpm_max_children=$(calculate_php_fpm_max_children)

    local min_spare=$((php_fpm_max_children / 4))
    local max_spare=$((php_fpm_max_children / 2))
    local start_servers=$(((min_spare + max_spare) / 2))

    # Ensure at least 1 for each setting
    (( min_spare < 1 )) && min_spare=1
    (( max_spare < 1 )) && max_spare=1
    (( start_servers < 1 )) && start_servers=1

    # Adjust start_servers within bounds
    (( start_servers < min_spare )) && start_servers=$min_spare
    (( start_servers > max_spare )) && start_servers=$max_spare

    echo "Configuring PHP-FPM max_children..."
    sudo sed -i "s/pm\.max_children\s*=\s*[0-9]*/pm.max_children = $php_fpm_max_children/g" /etc/php7/php-fpm.d/www.conf
    sudo sed -i "s/pm\.min_spare_servers\s*=\s*[0-9]*/pm.min_spare_servers = $min_spare/g" /etc/php7/php-fpm.d/www.conf
    sudo sed -i "s/pm\.max_spare_servers\s*=\s*[0-9]*/pm.max_spare_servers = $max_spare/g" /etc/php7/php-fpm.d/www.conf
    sudo sed -i "s/pm\.start_servers\s*=\s*[0-9]*/pm.start_servers = $start_servers/g" /etc/php7/php-fpm.d/www.conf

    echo "PHP-FPM max_children set to $php_fpm_max_children"
    echo "PHP-FPM min_spare_servers set to $min_spare"
    echo "PHP-FPM max_spare_servers set to $max_spare"
    echo "PHP-FPM start_servers set to $start_servers"
}

Explanation:

These two functions, calculate_php_fpm_max_children() and configure_php_fpm_children(), are designed to dynamically configure PHP-FPM (FastCGI Process Manager) settings based on the available memory in a Kubernetes pod. This ensures that your PHP-FPM pool is optimized for the best balance between performance and resource usage. Let's break down what each part does:

calculate_php_fpm_max_children()

This function calculates the maximum number of child processes that PHP-FPM should spawn. It's an essential part of optimizing PHP-FPM because it directly affects how many simultaneous requests your application can handle without running out of memory.

local usable_memory_mb=$(calculate_usable_memory)
  • It first determines the usable memory (in MB) for PHP-FPM processes by calling calculate_usable_memory, which subtracts a buffer (e.g., for OS and other processes) from the total memory limit set for the pod.
if [[ "$usable_memory_mb" == "Unlimited" ]]; then
    usable_memory_mb=2048  # Default value if memory is unlimited
fi
  • If the usable memory is "Unlimited", it defaults to a sensible preset (2048 MB) to prevent overallocation.
local process_size_kb=40000
local memtotal_kb=$(( usable_memory_mb * 1024 ))
local children=$((memtotal_kb / process_size_kb))
  • It calculates the maximum number of child processes (children) by dividing the total available memory (converted to KB) by an estimated memory footprint per child process (40,000 KB in this example).
if [[ ! "$children" =~ ^[0-9]+$ || $children -eq 0 ]]; then
    echo 1
else
    echo $children
fi
  • This checks if the calculated children value is a positive integer. If not (or if it's zero), it defaults to 1, ensuring there's at least one child process. Otherwise, it outputs the calculated number of child processes.

configure_php_fpm_children()

After calculating the optimal number of child processes, this function adjusts the PHP-FPM configuration to apply these optimizations.

local php_fpm_max_children=$(calculate_php_fpm_max_children)
  • Calls the previously defined function to get the maximum number of child processes PHP-FPM should spawn.
local min_spare=$((php_fpm_max_children / 4))
local max_spare=$((php_fpm_max_children / 2))
local start_servers=$(((min_spare + max_spare) / 2))
  • Calculates the min_spare_servers, max_spare_servers, and start_servers settings based on the php_fpm_max_children. These settings control the number of idle processes PHP-FPM maintains to handle incoming requests efficiently.
# Ensure at least 1 for each setting
(( min_spare < 1 )) && min_spare=1
(( max_spare < 1 )) && max_spare=1
(( start_servers < 1 )) && start_servers=1
  • Ensures that each of these settings is at least 1, to maintain minimum operational capacity.
# Adjust start_servers within bounds
(( start_servers < min_spare )) && start_servers=$min_spare
(( start_servers > max_spare )) && start_servers=$max_spare
  • Adjusts start_servers to be within the bounds of min_spare and max_spare, ensuring a logical configuration.
sudo sed -i "s/pm\.max_children\s*=\s*[0-9]*/pm.max_children = $php_fpm_max_children/g" /etc/php7/php-fpm.d/www.conf
  • Uses sed to replace the pm.max_children setting in the PHP-FPM configuration file with the calculated optimal value.

The same sed command pattern is used to update pm.min_spare_servers, pm.max_spare_servers, and pm.start_servers settings in the configuration file.

These steps ensure that PHP-FPM is configured to use the optimal number of child processes for the available memory, reducing the risk of memory exhaustion while maximizing the application's ability to handle concurrent requests efficiently. This dynamic approach allows for automatic adjustment of settings in environments where memory limits might change, such as when moving between different Kubernetes nodes or clusters.

4. Flexible Case Handling for Server Tasks

Lastly, managing server tasks through a flexible case handler allowed me to automate common operations efficiently.

handle_case() {
    case "$1" in 
        # Different cases for server management and task execution
    esac
}

Explanation:

  • Versatile task management: This function uses a case statement to handle various server tasks based on the argument passed to the script, streamlining operations like starting the server, managing queues, and more.

Reflection

Each piece of this script was designed with a specific purpose in mind, from ensuring efficient resource use to simplifying server management. By understanding and applying these concepts, you can significantly improve the scalability and resilience of your PHP applications within a Kubernetes environment. This journey has taught me the importance of tailored optimization— adapting strategies to fit the unique constraints and opportunities of the operating environment.

Subscribe to codingwithalex

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe