PHP RFC: Preloading

 Голосование по RFC о предзагрузке (PHP Preloading) файлов завершилось. Все члены PHP core-team единогласно поддержали это предложение. А значит в следующем году в релизе 7.4 нас ждет возможность предварительно загружать в опкэш любые файлы. Все функции и классы, объявленные в этих файлах, будут доступны для всех запросов, как если бы это были встроенные элементы вроде strlen() или Exception.

PHP использует кэш байт-кода (APC, Turck MMCache, Zend OpCache). Это позволяет добиться значительного повышения производительности благодаря полному устранению накладных расходов на перекомпиляцию PHP-кода. Файлы компилируются один раз (по первому запросу, который использует эти файлы), а затем кэш сохраняется в общей памяти. Все следующие HTTP-запросы используют скомпилированный код из общей памяти.

Хотя хранение скомпилированных файлов в кэше исключает издержки на компиляцию, по-прежнему сохраняется оверхед на извлечение файла из кэша в контексте конкретного запроса. PHP также должен проверить, был ли изменён исходный файл, скопировать определённые части классов и функций из кэша разделяемой памяти в память процесса и т. д. Поскольку каждый файл PHP скомпилирован и кэширован независимо от других файлов, PHP может разрешать зависимости между классами, хранящимися в разных файлах. При хранении файлов в кэше опкода нужно повторно связывать зависимости классов во время выполнения каждого запроса.

Это улучшение вдохновлено технологией «Class Data Sharing», используемой в Java HotSpot VM и предоставляет пользователям возможность повысить производительность путём использования гибкого механизма, который предоставляет стандартная PHP-модель. При запуске сервера и до запуска любого кода приложения мы можем загрузить определённый набор PHP-файлов в память и сделать их содержимое «постоянно доступным» для всех последующих запросов, которые будут обслуживаться этим сервером. Все функции и классы, определённые в этих файлах, будут доступны для запросов из коробки, точно так же, как и внутренние объекты (например, strlen() или Exception).

Таким образом, можно предварительно загружать фреймворк частично или полностью или все библиотеки классов приложения. Это также позволит ввести «встроенные» функции, которые будут написаны на PHP (аналогично Sytemlib HHVM). Будет включать в себя невозможность обновления этих файлов после запуска сервера (обновление этих файлов в файловой системе ничего не даст; для применения изменений потребуется перезагрузка сервера).

Кроме того, этот подход не будет совместим с серверами, на которых размещаются несколько приложений или несколько версий приложений, которые будут иметь разные реализации для определённых классов с одинаковым именем. Если такие классы предварительно загружены из кодовой базы одного приложения, это будет конфликтовать с загрузкой другой реализации класса из другого приложения.

Предварительная загрузка будет контролироваться одной новой директивой opcache.preload. Используя эту директиву, можно задать один PHP-файл, который будет выполнять задачу предварительной загрузки. После загрузки этот файл будет полностью выполнен и может предварительно загрузить другие файлы, включив их или используя функцию opcache_compile_file().

Например, следующий скрипт описывает вспомогательную функцию и использует её для предварительной загрузки всего Zend Framework.

<?php
function _preload($preload, string $pattern = "/\.php$/", array $ignore = []) {
  if (is_array($preload)) {
    foreach ($preload as $path) {
      _preload($path, $pattern, $ignore);
    }
  } else if (is_string($preload)) {
    $path = $preload;
    if (!in_array($path, $ignore)) {
      if (is_dir($path)) {
        if ($dh = opendir($path)) {
          while (($file = readdir($dh)) !== false) {
            if ($file !== "." && $file !== "..") {
              _preload($path . "/" . $file, $pattern, $ignore);
            }
          }
          closedir($dh);
        }
      } else if (is_file($path) && preg_match($pattern, $path)) {
        if (!opcache_compile_file($path)) {
          trigger_error("Preloading Failed", E_USER_ERROR);
        }
      }
    }
  }
}
set_include_path(get_include_path() . PATH_SEPARATOR . realpath("/var/www/ZendFramework/library"));
_preload(["/var/www/ZendFramework/library"]);

Предварительно загруженные файлы сохраняются навсегда в памяти opcache. Модификация соответствующих исходных файлов не будет иметь никакого эффекта без перезапуска сервера. Все функции и большинство классов, определённых в этих файлах, будут постоянно загружаться в PHP и таблицы классов и становятся постоянно доступными в контексте любого запроса. Во время предварительной загрузки PHP также разрешает зависимости классов и ссылки с родителями, интерфейсами и трейтами. Также будут пропущены ненужные include и выполнены некоторые другие оптимизации.

opcache_reset() не будет перезагружать предварительно загруженные файлы. Это невозможно сделать используя текущую реализацию opcache, потому что во время перезаписи кэша эти файлы могут использоваться каким-то процессом, и любые изменения могут привести к сбою.

opcache_get_status расширен для предоставления информации о preload-функциях, классах и сценариях в индексе preload_statistics.

Статические элементы и статические переменные

Во избежание недоразумений нужно пояснить, что предварительная загрузка (preloading) не меняет поведения статических членов класса и статических переменных. Их значения не сохраняются за пределами одного запроса.

Ограничение предварительной загрузки

Только классы без неразрешённых родителей, интерфейсов, трейтов и константных значений могут быть предварительно загружены. Если класс не удовлетворяет этому условию, он хранится в opcache SHM как часть соответствующего PHP-скрипта так же, как без предварительной загрузки. Кроме того, могут быть предварительно загружены только объекты верхнего уровня, которые не вложены в структуры управления (например, if () …).

В Windows также невозможно предустановить классы, унаследованные от внутренних. Windows ASLR и отсутствие fork() не позволяют гарантировать одинаковые адреса внутренних классов в разных процессах.

Детали реализации

Предварительная загрузка реализована как часть opcache поверх другого (уже совершённого) патча, который вводит «неизменные» (immutable) классы и функции. Они предполагают, что неизменяемая часть хранится в разделяемой памяти один раз (для всех процессов) и никогда не копируется для обработки памяти, но переменная часть специфична для каждого процесса. Патч ввёл структуру данных указателя MAP_PTR, которая позволяет указателям из SHM обрабатывать память.

Изменения без обратной совместимости

Предварительная загрузка не влияет на функциональность, если она явно не используется. Однако, если она используется, она может нарушить поведение приложения, поскольку предварительно загруженные классы и функции всегда доступны, а проверки function_exists() или class_exists() возвращают TRUE, что приводит к неожиданному поведению кода приложения. Как упоминалось выше, неправильное использование предварительной загрузки на сервере с несколькими приложениями также может привести к сбоям. Поскольку разные приложения (или разные версии одного и того же приложения) могут иметь одинаковые имена классов / функций в разных файлах, если одна версия класса предварительно загружена, то это предотвратит загрузку любой другой версии этого класса, определённой в другом файле.

Производительность

Используя предварительную загрузку без какой-либо модификации кода, тесты получили ~ 30% ускорения на ZF1_HelloWorld (3620 req/sec против 2650 req/sec) и ~ 50% на ZF2Test (1300 req/sec vs 670 req/sec) эталонных приложений. Тем не менее, реальный прирост будет зависеть от соотношения между начальной загрузкой кода и временем выполнения кода и, вероятно, будет ниже. Это, скорее всего, обеспечит наиболее заметный выигрыш в запросах с короткими сроками выполнения, такими как микросервисы.

Планы на будущее

Предварительная загрузка может использоваться в качестве системной библиотеки в HHVM для определения «стандартных» функций и классов в PHP.

Возможно предварительно скомпилировать сценарий предварительной загрузки и использовать бинарный формат (возможно, даже родную .so или .dll), чтобы ускорить запуск сервера.

В сочетании с ext / FFI (опасное расширение) можно разрешить функциональность FFI только в предварительно загруженных PHP-файлах, но не в обычных.

Можно выполнять более агрессивные оптимизации и генерировать более эффективный JIT-код для предварительно загруженных функций и классов (аналогично режиму HHVM Repo Authoritative в HHVM).

Было бы здорово расширить предварительную загрузку с помощью какого-либо механизма развёртывания, чтобы обновить предварительно загруженные пакеты без перезагрузки сервера.