Understanding High Memory Usage in PHP-FPM Processes: Shared Memory vs RSS Explained


1 views

When examining the top output, we need to distinguish between different memory measurements:

VIRT - Virtual memory (total address space)
RES  - Resident memory (physical RAM used)
SHR  - Shared memory (shared between processes)
%MEM - Percentage of physical RAM used

The apparent memory "overcommitment" occurs because PHP-FPM processes typically share:

  • Opcode cache (APC/OPcache)
  • PHP binary itself
  • Common libraries (libc, etc.)

In your output, notice the SHR column shows ~290MB shared per process. This means only about 15MB (307MB RES - 292MB SHR) is truly unique to each process.

To get the real memory consumption:

Unique memory per process = RES - SHR
Total unique memory = (RES - SHR) * process_count
Shared memory = SHR (counted only once system-wide)

To better manage memory, consider these php-fpm.conf tweaks:

; Limits process memory usage
php_admin_value[memory_limit] = 128M

; Controls how many processes can spawn
pm.max_children = 30
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10

; Helps with shared memory efficiency
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16

Use pmap to inspect shared memory segments:

pmap -x <pid> | grep -i shared
smem -t -k -P php-fpm
  • htop (press F2 to enable shared memory column)
  • glances (shows memory breakdown)
  • ps_mem.py (accurately calculates shared memory)

Practical code-level improvements:

// Instead of loading entire datasets:
$hugeArray = getAllRecords(); 

// Use generators or pagination:
function getRecordsGenerator() {
    $offset = 0;
    $limit = 100;
    do {
        $batch = getBatch($offset, $limit);
        foreach ($batch as $record) {
            yield $record;
        }
        $offset += $limit;
    } while (!empty($batch));
}

When examining the top output showing multiple PHP-FPM processes each consuming ~30% of memory, it's crucial to understand three key columns:

VIRT  - Virtual memory (total address space allocated)
RES   - Resident memory (physical RAM actually used)
SHR   - Shared memory (portion of RES shared between processes)
%MEM  - Percentage of physical RAM used (RES/total RAM)

The apparent memory "overcommitment" occurs because:

  • Shared libraries (like PHP core) are loaded once but counted for each process
  • PHP's OPcache and common extensions create shared memory segments
  • Copy-on-write mechanisms allow memory sharing until modifications occur

In your case, notice how SHR values (292m) are nearly identical across processes - this is shared memory being reported multiple times.

To get the real memory consumption:

Actual RAM Used = (Sum of RES) - (Sum of SHR * (n-1)/n)

For two PHP processes with RES=300m and SHR=290m:

Actual = (300m + 300m) - (290m * 1/2) ≈ 455m (not 600m)

Instead of top, consider these alternatives:

# smem - shows proportional vs unique memory
smem -t -P php-fpm

# pmap - reveals shared memory mapping details
pmap -x [PID] | grep -i shared

# ps with custom output
ps -eo pid,comm,rss,pmem,shared | grep php-fpm

Configuration adjustments in php-fpm.conf:

; Reduce process count
pm.max_children = 30 → 15

; Optimize shared memory
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16

; Enable early garbage collection
php_admin_value[memory_limit] = 256M
php_admin_value[opcache.revalidate_freq] = 60

This PHP snippet helps identify memory-hungry components:

<?php
function track_memory($label) {
    static $last = 0;
    $current = memory_get_usage(true);
    printf("%s: %+d KB\n", $label, ($current - $last)/1024);
    $last = $current;
}

track_memory("Start");
// Your code segments here...
track_memory("After heavy operation");

Linux provides different memory accounting modes via /proc/meminfo:

# Check available memory accounting
grep -E 'MemTotal|MemFree|Buffers|Cached|Slab|SReclaimable' /proc/meminfo

# Compare with kernel's view of process memory
cat /proc/[PID]/smaps | grep -i pss