Как стать автором
Обновить

PHP и realpath_cache

Время на прочтение7 мин
Количество просмотров22K
Автор оригинала: Julien Pauli
От переводчика: разбираясь на днях с ошибкой, возникшей после деплоя сервиса, натолкнулся на эту замечательную статью про механизм кэширования файловых статусов в PHP. Предлагаю сообществу перевод.

Слышали ли вы про PHP-функции realpath_cache_get() и realpath_cache_size()? А может быть про параметры realpath_cache_size и realpath_cache_ttl в php.ini?

Кэш realpath — довольно важный механизм PHP, который нужно иметь в виду. Особенно, когда приходится работать с символическими ссылками, например, при деплое проекта. Настройка кэширования realpath может значительно влиять на быстродействие сервера и нагрузку на дисковую подсистемы сервера. Этот параметр был введен в версии 5.1, когда начали появляться первые PHP-фреймворки.

Далее мы разберемся, как все это работает под капотом, и как с этим жить. Под катом много ссылок на исходники.


Вспоминаем о системном вызове stat()


Вы знаете, как работает ваша система? Давайте я освежу вашу память. Когда вы работаете с путём, системное ядро и файловая система должны понимать, что вы от них хотите. Когда вы используете путь для доступа к файлу, ваша библиотека или ядро системы должны разрешить его. Разрешение пути — это получение информации о нем: это файл, директория или, может быть ссылка?

Один из способов сделать это — спросить систему о типе файла. В случае, если попалась ссылка, узнать о целевом файле. Когда вы используете относительные пути (вроде "../hey/./you/../foobar"), необходимо сначала получить абсолютный путь, а уже потом получать информацию о конечном файле.

Обычно для разрешения относительного пути используется C-функция realpath(). Она, в свою очередь, делает системный вызов stat().

Вызов stat() достаточно тяжелый. Во-первых, это системный вызов, влекущий за собой прерывание и переключение конекста. Во-вторых, работает с данными на медленном диске. В коде можно найти обращения к файловой системе inode->getattr(). Обычно ядро использует собственный кэш (buffer-cache), поэтому влияние на производительность должно быть незначительным. Однако, на нагруженном сервере кэш может не содержать необходимую информацию, что влечет за собой повышенную нагрузку на дисковую подсистему. Поэтому, в наших же интересах предупреждать такое поведение.

Что делает PHP?


Проекты, написанные на PHP, обычно хранятся во множестве файлов. Сегодня мы используем тонны классов, означающих наличие тонны файлов (поскольку используем по файлу на каждый класс). Вне зависимости от того, используем мы механизм автоматической загрузки (autoload) или нет, мы должны подключать все эти файлы, чтобы прочитать код внутри них, а для этого сделать вызов stat() для получения информации о файле. Поэтому, когда мы получаем доступ к файлу из PHP, он сначала разрешает пути и ссылки, потом получает информацию о файле через системный вызов stat(), а потом сохраняет полученный результат в свой собственный кэш, называемый realpath cache.

PHP использует данный кэш только при работе функции realpath(). Вся остальная информация о файле вроде владельца, группы, прав доступа и временных меток сохраняется в отдельный кэш — access cache. Давайте посмотрим в исходники: когда происходит обращение к файлу, вызывается функция php_resolve_path(). Эта функция делает вызов tsrm_reapath(), которая внутри выполняет virtual_file_ex() и tsrm_realpath_r().

И в этом месте происходят интересные вещи: вызываются функции вроде realpath_cache_find() для поиска сохраненных в кеше данных для запрашиваемого файла. Для хранения информации используется структура realpath_cache_bucket, которая инкапсулирует большой пакет данных:

typedef struct _realpath_cache_bucket {
    unsigned long                  key;
    char                          *path;
    int                            path_len;
    char                          *realpath;
    int                            realpath_len;
    int                            is_dir;
    time_t                         expires;
#ifdef PHP_WIN32
    unsigned char                  is_rvalid;
    unsigned char                  is_readable;
    unsigned char                  is_wvalid;
    unsigned char                  is_writable;
#endif
    struct _realpath_cache_bucket *next;
} realpath_cache_bucket;

Если данные в кэше не найдены, вызывается функция php_sys_lstat(), которая является прокси для системного вызова lastat(). Результат этого вызова сохраняется в realpath cache.

Настройки PHP


Итак, со стороны PHP нам необходимо знать несколько вещей про realpath cache. Для начала, настройки php.ini:
realpath_cache_size
realpath-cache-ttl

В документации есть ремарка про увеличение этих параметров на серверах, где исходный код меняется редко. Так же стоит учесть, что стандартный размер кэша 16КБ ничтожно маленький. Он весь исчерпается одним запросом с фреймворком вроде Symfony2. Для поддержания настройки размера кэша в актуальном состоянии стоит следить за выводом функции realpath_cache_get(). Если доступный объем быстро исчерпывается — это явный повод увеличить размер кэша вплоть до 1МБ. В случае, если кэш переполнится, PHP начнет злоупотреблять вызовами stat(), что напрямую скажется на производительности. Требуемый размер кэша сложно посчитать с достаточной точностью. Покопавшись в исходниках, можно сделать вывод, что каждая сущность в кэше занимает место, равное: `sizeof(realpath_cache_bucket) + кол-во символов разрешенного пути + 1`
Для 64-битной системы (LP64) sizeof(realpath_cache_bucket) = 56 байт.

Есть еще другая особенность. PHP разрешает каждый путь, с которым сталкивается во время работы, разбивая его на части. Если вы запросите файл /home/julien/www/fooproject/app/web/entry.php, PHP разобъет его на максимальное кол-во доступных путей, начиная от корня. Таким образом, он сначала сохранит в кэш /home, потом /home/julien, потом /home/julien/www и т.д.

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

Итого, первый вывод из предыдущего параграфа: кэш — это хорошо!

Второй — «дернуть» несколько страница сайта после выкладки — необходимая задача перед открытием публичного доступа к сайту. Это не только сбросит OPcode cache, но так же актуализирует realpath cache и page cache ядра системы.

Как очистить кэш realpath? Функция, выполняющая эту задачу, спрятана от посторонних глаз. realpath_cache_clear()? Нет, такой функции не существует :(. Зато, в лучших традициях PHP, есть clearstatcache(true). Параметр true очень важный и зовется он $clear_realpath_cache. Очевидно, что он как раз и служит поставленным целям.

Пример


Возьмем с потолка простой пример^

<?php
$f = @file_get_contents('/tmp/bar.php');
echo "hello";
var_dump(realpath_cache_get());

Вот, что он нам выведет
hello
array(5) {
  ["/home/julien.pauli/www/realpath_example.php"]=>
  array(4) {
    ["key"]=>
    float(1.7251638834424E+19)
    ["is_dir"]=>
    bool(false)
    ["realpath"]=>
    string(43) "/home/julien.pauli/www/realpath_example.php"
    ["expires"]=>
    int(1404137986)
  }
  ["/home"]=>
  array(4) {
    ["key"]=>
    int(4353355791257440477)
    ["is_dir"]=>
    bool(true)
    ["realpath"]=>
    string(5) "/home"
    ["expires"]=>
    int(1404137986)
  }
  ["/home/julien.pauli"]=>
  array(4) {
    ["key"]=>
    int(159282770203332178)
    ["is_dir"]=>
    bool(true)
    ["realpath"]=>
    string(18) "/home/julien.pauli"
    ["expires"]=>
    int(1404137986)
  }
  ["/tmp"]=>
  array(4) {
    ["key"]=>
    float(1.6709564980243E+19)
    ["is_dir"]=>
    bool(true)
    ["realpath"]=>
    string(4) "/tmp"
    ["expires"]=>
    int(1404137986)
  }
  ["/home/julien.pauli/www"]=>
  array(4) {
    ["key"]=>
    int(5178407966190555102)
    ["is_dir"]=>
    bool(true)
    ["realpath"]=>
    string(22) "/home/julien.pauli/www"
    ["expires"]=>
    int(1404137986)


Что мы видим? Полный путь до скрипта разрешается по частям, с самого начала. Так как файл /tmp/bar.php не существует, записи о нем нет в кэше. Однако, путь до /tmp разрешен, поэтому каждый следующий запрос во вложенные файлы будет немного быстрее, чем в первый раз.

В возвращаемом функцией realpath_cache_get() массиве можно посмотреть такую важную информацию, как время устаревания записи. Это значение посчитано на основе времени доступа к файлу и настройки realpath_cache_ttl.

Поле key — хэш разрешенного пути. Используется вариант алгоритма FNV. Это внутренняя информация, которая вряд ли понадобится в практическом смысле. Хэш может быть как int, так и float, в зависимости от размера INT_MAX.

Если сейчас вызвать clearstatcache(true), этот массив обнулится и PHP будет снова делать системный вызов stat() на каждый запрашиваемый файл, который раньше уже был закэширован.

Поговорим про кэш OPcode


Готовы к очередному подводному камню?

Кэш realpath привязан к конкретному процессу и не сохраняется в разедляемую память (shared memory).

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

Что обычно происходит во время выкладки проекта? Чаще всего мы просто заменяем символическую ссылку с одной версии на другую, например, с /www/deploy-a на /www/deploy-b. И тут все обычно забывают, что кэш OPcode (по крайней мере OPCache и APC) полагаются на внутрений кэш realpath. Поэтому, механизмы кэширования OPcode не видят изменений символических ссылок и обновляют кэш только по мере его устаревания. Ну а дальше вы и так все знаете :)

Лучшим найденным решением для предотвращения этого побочного эффекта стало подготовка отдельного пула воркеров PHP и переключения балансировщика на него, позволяя старым воркерам нормально завершить работу. Это позволяет изолировать две версии друг от друга, тем самым, предотвратив использование неактуального кэша. Все окружение, включая кэш realpath и кэш OPCode, будет новым. Этот прием доступен как минимум при использовании Lighttpd и Nginx. И он успешно работает в продакшне.

Конец


Меня попросили написать несколько строк о кэше realpath. Скорее всего из-за проблем, возникающих при выкладке кода. Ну, теперь вы знаете, как это работает и как этим управлять.

P.S. от переводчика:
Из древних мейл-листов php-internals:
Just a thought, should clearstatcache() force the reset of the cache? I cant think of many situations where you would re-build directory tree's on the fly, but you never know what to expect from PHP users :)
Теги:
Хабы:
+24
Комментарии20

Публикации

Истории

Работа

PHP программист
157 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн