OPcache и жизненный цикл PHP
Жизненный цикл PHP-запроса
При каждом HTTP-запросе (PHP-FPM) или запуске CLI-скрипта PHP проходит строгую последовательность этапов.
PHP-FPM worker lifecycle
========================
Master start
│
├── Load php.ini
├── MINIT (Module Init) — для КАЖДОГО расширения
│ ext/pdo: register PDO class
│ ext/json: register json_encode/decode
│ ext/opcache: allocate shared memory
│ ...
├── Fork worker processes
│
│ Worker process (handles many requests):
│ ┌─────────────────────────────────────────┐
│ │ Request arrives │
│ │ │ │
│ │ ├── RINIT (Request Init) │
│ │ │ Reset $_GET, $_POST, $_SERVER │
│ │ │ Reset error state │
│ │ │ ext/session: read session │
│ │ │ │
│ │ ├── Execute script │
│ │ │ Compile PHP → Opcodes (if not cached) │
│ │ │ Execute opcodes │
│ │ │ Generate output │
│ │ │ │
│ │ ├── RSHUTDOWN (Request Shutdown) │
│ │ │ ext/session: write session │
│ │ │ Flush output buffers │
│ │ │ Destroy request-scoped data │
│ │ │ Call registered shutdown functions │
│ │ │ Run destructors │
│ │ │ gc_collect_cycles() │
│ │ │ │
│ │ └── Ready for next request │
│ │ │
│ │ ... (repeat for pm.max_requests) │
│ └─────────────────────────────────────────┘
│
├── Worker reaches pm.max_requests → killed
│
└── Master shutdown
MSHUTDOWN (Module Shutdown) — для КАЖДОГО расширения
Free resources, close connections
MINIT / MSHUTDOWN
<?php
declare(strict_types=1);
// MINIT — Module Initialization
// Called ONCE when PHP process starts (or when extension loads)
//
// What happens:
// - Extensions register classes, functions, constants
// - OPcache allocates shared memory
// - PDO registers database drivers
// - ext/intl loads ICU data
//
// In extensions (C code):
// PHP_MINIT_FUNCTION(myext) {
// register_class("MyClass");
// return SUCCESS;
// }
// MSHUTDOWN — Module Shutdown
// Called ONCE when PHP process terminates
//
// What happens:
// - Extensions free global resources
// - Shared memory released
// - File descriptors closed
RINIT / RSHUTDOWN
<?php
declare(strict_types=1);
// RINIT — Request Initialization
// Called at the START of each request
//
// What happens:
// - $_GET, $_POST, $_COOKIE, $_SERVER populated
// - Error reporting reset
// - Output buffering initialized
// - Sessions started (if auto_start)
// - Each extension's RINIT called
// RSHUTDOWN — Request Shutdown
// Called at the END of each request
//
// What happens (in order):
// 1. Call registered shutdown functions (register_shutdown_function)
// 2. Flush output buffers
// 3. Call __destruct() on remaining objects
// 4. Run garbage collector
// 5. Free all request-scoped memory
// register_shutdown_function example
register_shutdown_function(function (): void {
echo "Shutdown function called\n";
// Last chance to do cleanup
// Errors can still be caught here
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE], true)) {
// Log fatal error
error_log("Fatal: {$error['message']}");
}
});
PHP-FPM Worker Lifecycle
<?php
declare(strict_types=1);
// PHP-FPM configuration affects worker lifecycle:
// pm = dynamic|static|ondemand
//
// pm = dynamic:
// pm.max_children = 50 — max workers at any time
// pm.start_servers = 5 — workers at startup
// pm.min_spare_servers = 2 — min idle workers
// pm.max_spare_servers = 10 — max idle workers
//
// pm = static:
// pm.max_children = 50 — fixed number of workers
//
// pm = ondemand:
// pm.max_children = 50 — max workers
// pm.process_idle_timeout = 10s — kill idle workers after N seconds
// pm.max_requests = 1000
// After 1000 requests, worker is KILLED and respawned
// Prevents memory leaks from accumulating
// Set to 0 for unlimited (not recommended)
// Memory calculation:
// Each worker: ~30-50 MB (depends on application)
// 50 workers × 40 MB = 2 GB RAM needed
// FPM status page (enable in pool config):
// pm.status_path = /fpm-status
// Then: curl http://localhost/fpm-status?full
// Key metrics:
// - active processes: currently handling requests
// - idle processes: waiting for requests
// - listen queue: requests waiting for a free worker
// - max listen queue: highest queue length seen
// - slow requests: requests exceeding request_slowlog_timeout
OPcache: как работает
OPcache — расширение PHP, которое кеширует скомпилированный байткод (opcodes) в разделяемой памяти. Без OPcache каждый запрос проходит полный цикл: лексический анализ, парсинг, компиляция в opcodes, выполнение.
БЕЗ OPcache:
┌──────┐ ┌───────┐ ┌─────┐ ┌─────────┐ ┌───────────┐
│ .php │───►│ Lexer │───►│ AST │───►│ Compile │───►│ Execute │
│ file │ │ │ │ │ │→opcodes │ │ opcodes │
└──────┘ └───────┘ └─────┘ └─────────┘ └───────────┘
↑ Каждый запрос проходит весь путь заново
С OPcache:
┌──────┐ ┌───────┐ ┌─────┐ ┌─────────┐
│ .php │───►│ Lexer │───►│ AST │───►│ Compile │──┐
│ file │ │ │ │ │ │→opcodes │ │ Первый запрос
└──────┘ └───────┘ └─────┘ └─────────┘ │
▼
┌──────────────┐
│ Shared │
│ Memory │
│ (OPcache) │
└──────┬───────┘
│ Последующие запросы
▼
┌───────────┐
│ Execute │
│ opcodes │
└───────────┘
Базовая конфигурация
; php.ini — OPcache settings
; Enable OPcache (required)
opcache.enable=1
; Enable for CLI (useful for long-running scripts, tests)
opcache.enable_cli=0
; Shared memory size in MB (for storing compiled scripts)
opcache.memory_consumption=256
; Memory for interned strings (class names, function names, etc.)
opcache.interned_strings_buffer=32
; Max number of cached scripts
opcache.max_accelerated_files=20000
; Tip: must be a prime number internally. PHP rounds up.
; Find number of files: find . -name "*.php" | wc -l
; Check file timestamps for changes?
; Production: 0 (never recheck — deploy clears cache)
; Development: 1 (check every revalidate_freq seconds)
opcache.validate_timestamps=0
; How often to check timestamps (seconds)
; Only relevant when validate_timestamps=1
opcache.revalidate_freq=2
; Save comments (needed for annotations/attributes in frameworks)
opcache.save_comments=1
; Enable file override (stat optimizations)
opcache.enable_file_override=1
; Huge pages for better TLB performance (Linux)
; opcache.huge_code_pages=1
Проверка статуса OPcache
<?php
declare(strict_types=1);
// Check if OPcache is active
if (!function_exists('opcache_get_status')) {
echo "OPcache not available\n";
exit(1);
}
$status = opcache_get_status(false); // false = без списка файлов
echo "OPcache enabled: " . ($status['opcache_enabled'] ? 'yes' : 'no') . "\n";
// Memory usage
$memory = $status['memory_usage'];
echo "\n--- Memory ---\n";
echo "Used: " . number_format($memory['used_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Free: " . number_format($memory['free_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Wasted: " . number_format($memory['wasted_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Wasted %: " . number_format($memory['current_wasted_percentage'], 2) . "%\n";
// Statistics
$stats = $status['opcache_statistics'];
echo "\n--- Statistics ---\n";
echo "Cached scripts: {$stats['num_cached_scripts']}\n";
echo "Cache hits: {$stats['hits']}\n";
echo "Cache misses: {$stats['misses']}\n";
echo "Hit rate: " . number_format($stats['opcache_hit_rate'], 2) . "%\n";
echo "Max cached keys: {$stats['max_cached_keys']}\n";
// Interned strings
$strings = $status['interned_strings_usage'];
echo "\n--- Interned Strings ---\n";
echo "Buffer size: " . number_format($strings['buffer_size'] / 1024 / 1024, 2) . " MB\n";
echo "Used: " . number_format($strings['used_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Free: " . number_format($strings['free_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Strings count: {$strings['number_of_strings']}\n";
Управление кешем
<?php
declare(strict_types=1);
// Precompile a specific file (don't wait for first request)
opcache_compile_file('/path/to/important/file.php');
// Invalidate a specific file (force recompile on next use)
opcache_invalidate('/path/to/changed/file.php', force: true);
// Reset entire cache (after deploy)
opcache_reset();
// Check if file is cached
$status = opcache_get_status(true); // true = include file list
$scripts = $status['scripts'] ?? [];
foreach ($scripts as $path => $info) {
echo sprintf(
"%s — hits: %d, size: %s, timestamp: %s\n",
$path,
$info['hits'],
number_format($info['memory_consumption']),
date('Y-m-d H:i:s', $info['timestamp']),
);
}
// Deploy script example:
// After deploying new code:
// 1. Upload new files
// 2. php -r "opcache_reset();"
// Or: curl http://localhost/opcache-reset.php (with auth!)
// Or: kill -USR2 $(cat /var/run/php-fpm.pid) — graceful reload
Preloading (PHP 7.4+)
Preloading загружает PHP-файлы в OPcache при старте PHP-FPM и разделяет их между ВСЕМИ воркерами. Загруженные классы и функции доступны сразу, без require и без проверки файловой системы.
; php.ini
opcache.preload=/var/www/app/preload.php
opcache.preload_user=www-data
<?php
// preload.php — executed ONCE at PHP-FPM startup
declare(strict_types=1);
// Strategy 1: Preload specific files
$files = [
// Core framework classes
__DIR__ . '/vendor/symfony/http-kernel/HttpKernel.php',
__DIR__ . '/vendor/symfony/routing/Router.php',
// Application services
__DIR__ . '/src/Service/UserService.php',
__DIR__ . '/src/Repository/UserRepository.php',
// Entities
__DIR__ . '/src/Entity/User.php',
__DIR__ . '/src/Entity/Order.php',
];
foreach ($files as $file) {
if (file_exists($file)) {
opcache_compile_file($file);
}
}
// Strategy 2: Preload from Composer's classmap
$classmap = __DIR__ . '/vendor/composer/autoload_classmap.php';
if (file_exists($classmap)) {
$classes = require $classmap;
foreach ($classes as $class => $file) {
// Skip test files, interfaces with conflicts, etc.
if (str_contains($file, '/tests/')
|| str_contains($file, '/Tests/')
) {
continue;
}
if (file_exists($file)) {
try {
opcache_compile_file($file);
} catch (\Throwable) {
// Some files may have dependencies not yet loaded
}
}
}
}
Symfony preload
<?php
// Symfony generates a preload file automatically:
// config/preload.php (generated by composer dump-autoload --classmap-authoritative)
// In Symfony, set in .env or php.ini:
// opcache.preload=/var/www/app/config/preload.php
// Symfony's preload file uses require_once for each class
// and handles dependency ordering automatically
// Verify what's preloaded:
$status = opcache_get_status();
echo "Preloaded scripts: " . ($status['preload_statistics']['scripts'] ?? 0) . "\n";
echo "Preloaded memory: " . number_format(
($status['preload_statistics']['memory_consumption'] ?? 0) / 1024 / 1024,
2
) . " MB\n";
Ограничения preloading:
- Изменения в preloaded файлах требуют перезапуска PHP-FPM
- Нельзя preload файлы с
declare(strict_types=1)в разных режимах- Классы с неразрешимыми зависимостями вызовут ошибку при старте
- Interfaces и traits загружаются нормально, abstract классы тоже
JIT Compiler (PHP 8.0+)
JIT (Just-In-Time) компилятор превращает PHP opcodes в машинный код процессора во время выполнения. Это третий уровень оптимизации после OPcache (кеширование opcodes) и preloading.
Уровни оптимизации:
═══════════════════════════════════════════════════
Уровень 1: OPcache — кеш opcodes
Уровень 2: Preloading — предзагрузка в shared memory
Уровень 3: JIT — компиляция opcodes → машинный код
═══════════════════════════════════════════════════
Без JIT:
PHP opcodes → Zend VM interprets → CPU
С JIT:
PHP opcodes → JIT compiles → Native x86/ARM code → CPU directly
(горячие участки кода компилируются в машинные инструкции)
Конфигурация JIT
; php.ini — JIT configuration (requires OPcache)
; Enable JIT
opcache.jit=1205
; JIT buffer size (memory for compiled machine code)
opcache.jit_buffer_size=128M
; JIT mode: 4-digit number CRTO
; C (CPU optimization): 0=none, 1=enable
; R (Register allocation): 0=none, 1=local, 2=global
; T (Trigger): 0=off, 1=first run, 2=hot, 3=function, 4=trace, 5=all
; O (Optimization level): 0=none, 1=minimal, 2=inline, 3=opt-inline, 4=max, 5=max+inline
;
; Common modes:
; 1205 — Tracing JIT (recommended for web): trace hot paths
; 1255 — Tracing JIT (maximum optimization)
; 1005 — Function JIT: compile entire functions
; 0000 — Disabled
; Tracing JIT (default, recommended):
opcache.jit=tracing
; or equivalently:
opcache.jit=1205
; Function JIT:
opcache.jit=function
; or:
opcache.jit=1205
; Disable JIT:
opcache.jit=disable
; or:
opcache.jit=0
Tracing vs Function JIT
<?php
declare(strict_types=1);
// Tracing JIT (opcache.jit=tracing / 1205):
// - Compiles HOT CODE PATHS (traces)
// - Profiles which paths are executed most
// - Only compiles hot paths → less memory, better optimization
// - Better for web (many different code paths)
//
// Function JIT (opcache.jit=function / 1005):
// - Compiles ENTIRE FUNCTIONS
// - No profiling needed
// - Compiles more code → more memory, less targeted optimization
// - Better for CLI / long-running scripts
// Check JIT status
$status = opcache_get_status();
$jit = $status['jit'] ?? [];
echo "JIT enabled: " . ($jit['enabled'] ? 'yes' : 'no') . "\n";
echo "JIT on: " . ($jit['on'] ? 'yes' : 'no') . "\n";
echo "JIT kind: " . ($jit['kind'] ?? 'unknown') . "\n";
echo "Buffer size: " . number_format(($jit['buffer_size'] ?? 0) / 1024 / 1024, 2) . " MB\n";
echo "Buffer free: " . number_format(($jit['buffer_free'] ?? 0) / 1024 / 1024, 2) . " MB\n";
Когда JIT помогает
<?php
declare(strict_types=1);
// JIT gives significant speedup for CPU-INTENSIVE code:
// ✅ BENEFITS from JIT:
// Mathematical computations
function fibonacci(int $n): int
{
if ($n <= 1) return $n;
return fibonacci($n - 1) + fibonacci($n - 2);
}
// Array processing / sorting
function processLargeArray(array $data): array
{
$result = [];
foreach ($data as $item) {
$result[] = $item * 2 + sqrt($item);
}
sort($result);
return $result;
}
// Image processing, encoding/decoding
// Scientific calculations
// Tight loops with arithmetic
// ❌ MINIMAL benefit from JIT:
// I/O bound operations (database queries, HTTP requests, file reads)
// Typical web applications (most time in I/O, not computation)
// Simple CRUD operations
// Benchmark example:
$start = hrtime(true);
$result = fibonacci(35);
$elapsed = (hrtime(true) - $start) / 1_000_000;
echo "fibonacci(35) = {$result} in {$elapsed} ms\n";
// Without JIT: ~2000ms
// With JIT: ~200ms (10x faster for CPU-bound!)
// Typical web: <5% improvement (most time in database/network)
Практическое правило: Для типичных веб-приложений JIT даёт 0-5% улучшения. Для CPU-intensive задач (математика, обработка данных, парсинг) — до 2-3x улучшения. Включайте JIT по умолчанию, но не ожидайте чудес для CRUD-приложений.
APCu — пользовательский кеш
APCu (APC User Cache) — кеш в разделяемой памяти для пользовательских данных. Данные разделяются между всеми PHP-FPM воркерами одного пула. Не путайте с OPcache — APCu кеширует произвольные данные, а не скомпилированный код.
# Install
apt install php8.4-apcu
# or
pecl install apcu
<?php
declare(strict_types=1);
// Basic operations
// Store a value
apcu_store('user:123', ['name' => 'John', 'email' => '[email protected]']);
// Store with TTL (seconds)
apcu_store('session:abc', ['token' => 'xyz'], 3600); // 1 hour
// Fetch a value
$user = apcu_fetch('user:123', $success);
if ($success) {
echo "Found: {$user['name']}\n";
} else {
echo "Not in cache\n";
}
// Check existence
if (apcu_exists('user:123')) {
echo "Key exists\n";
}
// Delete
apcu_delete('user:123');
// Increment/Decrement (atomic!)
apcu_store('counter', 0);
apcu_inc('counter'); // 1
apcu_inc('counter', 5); // 6
apcu_dec('counter', 2); // 4
$value = apcu_fetch('counter');
echo "Counter: {$value}\n"; // 4
// Atomic fetch-or-compute (PHP 7.0+)
$data = apcu_entry('expensive:key', function (): array {
// This runs ONLY if key doesn't exist
// Other processes wait for this to complete (no stampede)
sleep(1); // Simulate expensive operation
return ['computed' => true, 'time' => time()];
}, 300); // TTL = 300 seconds
// Store multiple
apcu_store([
'key1' => 'value1',
'key2' => 'value2',
'key3' => 'value3',
]);
// Fetch multiple
$values = apcu_fetch(['key1', 'key2', 'key3']);
// Delete multiple
apcu_delete(new APCUIterator('#^key#')); // Delete by regex
// Clear entire cache
apcu_clear_cache();
// Cache info
$info = apcu_cache_info();
echo "Entries: {$info['num_entries']}\n";
echo "Hits: {$info['num_hits']}\n";
echo "Misses: {$info['num_misses']}\n";
echo "Memory: " . number_format($info['mem_size'] / 1024 / 1024, 2) . " MB\n";
// SMA (Shared Memory Allocator) info
$sma = apcu_sma_info();
echo "Available: " . number_format($sma['avail_mem'] / 1024 / 1024, 2) . " MB\n";
Паттерн кеширования с APCu
<?php
declare(strict_types=1);
/**
* Application cache using APCu with fallback.
*/
final class AppCache
{
private string $prefix;
private int $defaultTtl;
public function __construct(
string $prefix = 'app:',
int $defaultTtl = 3600,
) {
$this->prefix = $prefix;
$this->defaultTtl = $defaultTtl;
}
public function get(string $key, mixed $default = null): mixed
{
$value = apcu_fetch($this->prefix . $key, $success);
return $success ? $value : $default;
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
return apcu_store(
$this->prefix . $key,
$value,
$ttl ?? $this->defaultTtl,
);
}
/**
* Get or compute: atomically fetch or create cached value.
*
* @template T
* @param string $key
* @param Closure(): T $compute
* @param int|null $ttl
* @return T
*/
public function remember(string $key, Closure $compute, ?int $ttl = null): mixed
{
return apcu_entry(
$this->prefix . $key,
$compute,
$ttl ?? $this->defaultTtl,
);
}
public function delete(string $key): bool
{
return apcu_delete($this->prefix . $key);
}
public function flush(): bool
{
// Delete only keys with our prefix
$iterator = new APCUIterator(
'#^' . preg_quote($this->prefix, '#') . '#',
);
return apcu_delete($iterator);
}
}
// Usage
$cache = new AppCache(prefix: 'myapp:', defaultTtl: 600);
// Simple get/set
$cache->set('config', ['debug' => false, 'locale' => 'ru']);
$config = $cache->get('config');
// Remember pattern (compute only if not cached)
$users = $cache->remember('top-users', function (): array {
// Expensive database query — runs only once per TTL
return []; // fetchTopUsersFromDB();
}, ttl: 300);
Конфигурация для production
; === PRODUCTION php.ini ===
; OPcache — MUST HAVE
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=30000
opcache.validate_timestamps=0 ; CRITICAL: don't check files for changes
opcache.revalidate_freq=0
opcache.save_comments=1 ; Needed for annotations
opcache.enable_file_override=1
; Preloading
opcache.preload=/var/www/app/config/preload.php
opcache.preload_user=www-data
; JIT
opcache.jit=1205
opcache.jit_buffer_size=128M
; APCu
apc.enabled=1
apc.shm_size=64M
apc.ttl=7200 ; Default TTL
apc.gc_ttl=3600
apc.entries_hint=4096
apc.slam_defense=1 ; Prevent cache slam
; PHP-FPM pool
; pm = dynamic
; pm.max_children = 50 ; Based on RAM / ~40MB per worker
; pm.start_servers = 10
; pm.min_spare_servers = 5
; pm.max_spare_servers = 20
; pm.max_requests = 1000 ; Restart worker after N requests
; request_terminate_timeout = 30s
; request_slowlog_timeout = 5s
; slowlog = /var/log/php-fpm/slow.log
; === DEVELOPMENT php.ini ===
; opcache.validate_timestamps=1 ; Check for file changes
; opcache.revalidate_freq=0 ; Check every request
; opcache.jit=disable ; Faster startup for dev
; opcache.preload= ; No preloading in dev
Скрипт проверки конфигурации
<?php
declare(strict_types=1);
echo "=== PHP Configuration Check ===\n\n";
// PHP version
echo "PHP Version: " . PHP_VERSION . "\n";
echo "SAPI: " . PHP_SAPI . "\n\n";
// OPcache
echo "--- OPcache ---\n";
$opcacheEnabled = function_exists('opcache_get_status');
echo "Available: " . ($opcacheEnabled ? 'yes' : 'NO — INSTALL ext-opcache!') . "\n";
if ($opcacheEnabled) {
$status = opcache_get_status(false);
echo "Enabled: " . ($status['opcache_enabled'] ? 'yes' : 'NO!') . "\n";
echo "Memory: " . ini_get('opcache.memory_consumption') . " MB\n";
echo "Max files: " . ini_get('opcache.max_accelerated_files') . "\n";
echo "Validate timestamps: " . ini_get('opcache.validate_timestamps') . "\n";
if (ini_get('opcache.validate_timestamps') === '1' && PHP_SAPI !== 'cli') {
echo " ⚠ WARNING: validate_timestamps=1 in production reduces performance\n";
}
// JIT
$jit = $status['jit'] ?? [];
echo "\n--- JIT ---\n";
echo "Enabled: " . (($jit['enabled'] ?? false) ? 'yes' : 'no') . "\n";
echo "Buffer: " . ini_get('opcache.jit_buffer_size') . "\n";
echo "Mode: " . ini_get('opcache.jit') . "\n";
// Preloading
echo "\n--- Preloading ---\n";
$preload = ini_get('opcache.preload');
echo "Script: " . ($preload ?: 'not configured') . "\n";
if ($preload && isset($status['preload_statistics'])) {
$ps = $status['preload_statistics'];
echo "Preloaded scripts: " . ($ps['scripts'] ?? 0) . "\n";
echo "Preloaded memory: " . number_format(
($ps['memory_consumption'] ?? 0) / 1024 / 1024,
2,
) . " MB\n";
}
}
// APCu
echo "\n--- APCu ---\n";
$apcuEnabled = function_exists('apcu_enabled') && apcu_enabled();
echo "Available: " . ($apcuEnabled ? 'yes' : 'no') . "\n";
if ($apcuEnabled) {
echo "SHM size: " . ini_get('apc.shm_size') . "\n";
$info = apcu_cache_info();
echo "Entries: {$info['num_entries']}\n";
echo "Hit rate: " . (
$info['num_hits'] + $info['num_misses'] > 0
? number_format(
$info['num_hits'] / ($info['num_hits'] + $info['num_misses']) * 100,
1,
) . '%'
: 'N/A'
) . "\n";
}
// Recommendations
echo "\n--- Recommendations ---\n";
if (!$opcacheEnabled) {
echo "✗ Install and enable OPcache (critical for performance)\n";
}
if (ini_get('opcache.jit_buffer_size') === '0'
|| ini_get('opcache.jit') === 'disable'
) {
echo "• Consider enabling JIT for CPU-intensive workloads\n";
}
if (!$apcuEnabled) {
echo "• Consider installing APCu for in-memory caching\n";
}