17 вопросов
zval - внутренняя структура Zend Engine для хранения любого значения PHP. Содержит: тип данных, само значение, счетчик ссылок (refcount) и флаги.
// Структура (упрощенно):
// typedef struct {
// zend_value value; // union: long, double, string, array...
// uint32_t type_info; // тип + флаги
// } zval;С PHP 7 zval стал 16 байт (вместо 48 в PHP 5). Скаляры хранятся прямо в zval, сложные типы (string, array, object) - через указатель с refcount. Это дало прирост производительности 2-3x.
Этапы выполнения PHP-скрипта:
Opcodes - низкоуровневые инструкции VM: ASSIGN, ADD, CONCAT, FETCH_R, SEND_VAL, DO_FCALL и т.д.
// Посмотреть opcodes:
// php -d opcache.opt_debug_level=0x10000 script.php
// Или расширение VLD: php -d vld.active=1 script.phpOpcache кеширует opcodes в shared memory, пропуская шаги 1-3 при повторных запросах.
OPcache - расширение, кеширующее скомпилированные opcodes в shared memory. Устраняет повторную компиляцию файлов.
; php.ini
opcache.enable=1
opcache.memory_consumption=256 ; MB для opcodes
opcache.interned_strings_buffer=16 ; MB для интернированных строк
opcache.max_accelerated_files=10000 ; макс. число файлов
opcache.validate_timestamps=0 ; 0 на проде (не проверять mtime)
opcache.revalidate_freq=0
opcache.save_comments=0 ; не хранить docblocks
opcache.enable_file_override=1На проде validate_timestamps=0 - файлы не перечитываются с диска. При деплое: opcache_reset() или перезапуск FPM. Прирост производительности: 50-100%.
JIT (Just-In-Time) компилирует горячие opcodes в нативный машинный код процессора. Использует DynASM.
; php.ini
opcache.jit=1255 ; tracing JIT
opcache.jit_buffer_size=128M ; буфер для машинного кодаГде помогает: математика, обработка данных, CPU-intensive задачи. Где НЕ помогает: типичные веб-приложения (I/O bound - БД, сеть, файлы). Для большинства Laravel/Symfony проектов прирост от JIT минимален (1-5%).
В PHP 8.4 JIT перешел на IR framework - стал стабильнее и быстрее.
Preloading (PHP 7.4+) - загрузка классов и функций в shared memory при старте PHP-FPM, до обработки запросов.
; php.ini
opcache.preload=/app/preload.php
opcache.preload_user=www-data// preload.php
require_once __DIR__ . '/vendor/autoload.php';
// Загрузить часто используемые классы
$files = glob(__DIR__ . '/app/Models/*.php');
foreach ($files as $file) { require_once $file; }Преимущества: классы доступны мгновенно, нет overhead автозагрузки, opcache не нужно искать файлы. Недостаток: при изменении кода нужен перезапуск FPM.
PHP-FPM (FastCGI Process Manager) - менеджер процессов PHP для обработки HTTP-запросов через FastCGI.
Архитектура: master-процесс управляет пулом worker-процессов. Каждый worker обрабатывает один запрос за раз.
; pool.d/www.conf
[www]
user = www-data
listen = /run/php/php-fpm.sock
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500 ; перезапуск воркера после N запросовNginx передает запрос PHP-FPM через FastCGI, FPM возвращает ответ.
; Расчет max_children:
; (Доступная RAM - RAM для ОС/БД) / RAM на воркер
; Пример: (4GB - 1GB) / 40MB = 75 воркеровФормула: max_children = (Total RAM - System RAM) / Average Worker Memory
# Узнать потребление памяти одного воркера:
ps aux | grep php-fpm | awk '{sum+=$6; n++} END {print sum/n/1024 " MB"}'
# Пример расчета:
# Сервер: 8 GB RAM
# ОС + БД + Nginx: ~2 GB
# Средний воркер: 50 MB
# max_children = (8192 - 2048) / 50 = 122Мониторьте через pm.status_path: количество idle/active воркеров, длину очереди. Если listen.backlog полон - нужно больше воркеров.
PHP GC использует два механизма:
// Циклическая ссылка - refcount никогда не станет 0
$a = new stdClass();
$b = new stdClass();
$a->ref = $b;
$b->ref = $a;
unset($a, $b); // refcount = 1, но объекты недоступны
// Cycle Collector найдет и освободитУправление: gc_enable(), gc_disable(), gc_collect_cycles(). На долгоживущих процессах (workers, daemons) GC важен для предотвращения утечек.
ob_start(); // начать буферизацию
echo "Hello ";
echo "World";
$content = ob_get_clean(); // получить содержимое и очистить
// Вложенная буферизация
ob_start();
echo "outer ";
ob_start();
echo "inner";
$inner = ob_get_clean(); // "inner"
echo $inner;
$outer = ob_get_clean(); // "outer inner"
// Полезно для:
// 1. Захвата вывода шаблонов
// 2. Модификации ответа перед отправкой
// 3. Gzip-сжатия: ob_start('ob_gzhandler')RoadRunner - PHP application server на Go. Воркеры PHP остаются в памяти между запросами (не пересоздаются). Прирост 5-10x vs PHP-FPM.
FrankenPHP - PHP application server, встроенный в Caddy (тоже на Go). Поддерживает HTTP/3, Early Hints, worker mode.
Преимущества: нет overhead на инициализацию PHP, нативные WebSocket и SSE, Go-middleware. Недостатки: нужно следить за утечками памяти (PHP не очищает все автоматически), не все расширения совместимы.
Асинхронные фреймворки для PHP:
Все три позволяют обрабатывать тысячи concurrent connections в одном процессе (event-driven). Подходят для WebSocket, real-time, high-throughput API.
# Xdebug profiling
php -d xdebug.mode=profile -d xdebug.output_dir=/tmp script.php
# SPX
SPX_ENABLED=1 SPX_KEY=dev php script.phpНа проде: Blackfire или Tideways (низкий overhead). На разработке: Xdebug или SPX.
// Быстрая отладка
var_dump($variable); // тип + значение
print_r($array); // человекочитаемый массив
dd($var); // dump and die (Laravel)
dump($var); // dump без die (Symfony)
// Xdebug step debugging
// 1. Настроить xdebug.mode=debug в php.ini
// 2. Настроить IDE (PhpStorm/VSCode)
// 3. Поставить breakpoint
// 4. Запустить с XDEBUG_SESSION cookie/query param
// Логирование
error_log("Debug: " . json_encode($data));
// Профилирование узких мест
$start = microtime(true);
// ... код ...
$elapsed = microtime(true) - $start;
error_log("Elapsed: {$elapsed}s");PDO (PHP Data Objects) - универсальный интерфейс для работы с базами данных. Поддерживает: MySQL, PostgreSQL, SQLite, MSSQL и др.
$pdo = new PDO('pgsql:host=localhost;dbname=app', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// Prepared statement (защита от SQL injection)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => 42]);
$user = $stmt->fetch();
// Транзакция
$pdo->beginTransaction();
try {
$pdo->exec('...');
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}Persistent connections - соединения с БД, которые не закрываются в конце запроса, а переиспользуются следующими запросами того же воркера.
// PDO persistent connection
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_PERSISTENT => true,
]);Преимущество: нет overhead на установку TCP + TLS + auth при каждом запросе. Недостатки: может исчерпать лимит соединений БД (max_children * число серверов), незавершенные транзакции могут "протечь" между запросами.
Лучше: использовать connection pooler (PgBouncer для PostgreSQL) вместо persistent connections.
N+1 - при загрузке N записей для каждой выполняется дополнительный запрос к связанной таблице:
// N+1: 1 запрос на посты + N запросов на авторов
$posts = Post::all(); // SELECT * FROM posts
foreach ($posts as $post) {
echo $post->author->name; // SELECT * FROM users WHERE id = ? (x N раз!)
}
// Решение: eager loading
$posts = Post::with('author')->get();
// SELECT * FROM posts
// SELECT * FROM users WHERE id IN (1, 2, 3, ...) (1 запрос!)
// Другие решения:
// JOIN
// Подзапрос
// DataLoader pattern (batch loading)
// Laravel: withCount(), loadMissing()Инструменты обнаружения: Laravel Debugbar, Clockwork, query log.