RE:NODE

Веб-хостинг13 мин чтения

Настройки php.ini, которые важны, и умолчания, которые стоит менять

memory_limit, upload_max_filesize, max_execution_time, max_input_vars и OPcache: чего стоит каждое умолчание и какой файл PHP читает на самом деле.

0 прочтений

В PHP несколько сотен директив, и примерно восемь из них решают, будет ли ваш сайт работать. memory_limit в 128 МБ, upload_max_filesize в 2 МБ, post_max_size в 8 МБ, max_execution_time в 30 секунд и max_input_vars в 1000 - это умолчания, стоящие за большинством ошибок, с которыми люди приходят в тикет хостинга, и у каждого есть свой характерный симптом.

Вторая половина проблемы в том, что изменить значение и не увидеть никакого эффекта - обычное дело. PHP читает файл конфигурации, расположение которого зависит от способа запуска, игнорирует директивы .htaccess, если не работает как модуль Apache, позволяет менять на лету только часть настроек, и может быть переопределён пулом PHP-FPM, который побеждает всё остальное. Поэтому начнём с поиска файла.

Какой файл PHP читает на самом деле#

На одну установку PHP приходится один php.ini, а установок на одной машине часто несколько: файлы веб-сервера и командной строки обычно разные, поэтому скрипт, работающий по SSH, падает в браузере. Спросите у самого PHP, а не гадайте:

bash
$ php --iniConfiguration File (php.ini) Path: /etc/php/8.2/cliLoaded Configuration File:         /etc/php/8.2/cli/php.iniScan for additional .ini files in: /etc/php/8.2/cli/conf.d

Это файл командной строки. Чтобы узнать значения, с которыми работает ваш сайт, положите в корень сайта один файл с phpinfo();, откройте его в браузере, прочитайте строку Loaded Configuration File вверху и сразу же удалите файл - он публикует ваши пути, расширения и окружение любому, кто найдёт URL.

Значение могут задать четыре механизма, и применяются они в таком порядке:

МеханизмК чему применяетсяПримечания
php.iniКо всему в этой установкеОснова
conf.d/*.iniКо всему, загружается позжеСюда пакеты кладут настройки расширений
Конфигурация пула PHP-FPMК одному пулуphp_admin_value потом не переопределить
.user.iniК одному дереву каталоговТолько FPM и CGI, кэшируется на 300 секунд
ini_set() в кодеК одному запросуТолько настройки, помеченные как изменяемые на лету

Из этого два следствия, на которых спотыкаются. Во-первых, строки php_value в .htaccess работают, только когда PHP запущен как модуль Apache; под PHP-FPM, которым сейчас пользуется почти всё, они в лучшем случае игнорируются, а в худшем вызывают ошибку 500. Эквивалент - файл .user.ini в том же каталоге. Во-вторых, изменения в .user.ini кэшируются - по умолчанию user_ini.cache_ttl равен 300 секундам, - поэтому, подождав пять минут, прежде чем решить, что правка ничего не сделала, вы избежите большой путаницы.

Учтите и то, что .user.ini может задавать только директивы классов PHP_INI_PERDIR и PHP_INI_USER. upload_max_filesize - одна из них. Многие другие - нет, и для них нужен настоящий php.ini или конфигурация пула, а на управляемом хостинге это значит - просить того, кто им управляет.

memory_limit#

memory_limit ограничивает, сколько памяти один процесс PHP может выделить для одного запроса. Значение по умолчанию - 128 МБ. Когда запрос его превышает, PHP останавливается с фатальной ошибкой, в которой точно названо это число:

code
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted(tried to allocate 20480 bytes) in /var/www/wp-includes/wp-db.php on line 2056

Для небольшого сайта на WordPress 128 МБ хватает. Для сайта с конструктором страниц, магазином и тридцатью плагинами реалистична цифра 256 МБ, а 512 МБ нужны задачам обработки изображений и импорта. Больше этого - подозревайте код: плагин, загружающий все записи в массив, нуждается не в памяти, а в исправлении.

php.ini
memory_limit = 256M

Три вещи, которые стоит знать:

  • Лимит действует на запрос, а не на сайт. Десять одновременных запросов по 256 МБ могут потребовать 2,5 ГБ. Лимит - это потолок для одного процесса, а умножает его число процессов - см. раздел про PHP-FPM ниже.
  • Ставить `-1` на веб-сервере - плохая идея. Без ограничения один взбесившийся запрос может довести весь контейнер до его лимита памяти, и тогда умирает всё, что на нём есть, а не один запрос чисто завершается ошибкой. Многие дистрибутивы поставляют -1 для интерпретатора командной строки, и именно поэтому долгие импорты работают из консоли и падают в браузере.
  • Поверх этого у WordPress есть собственный лимит. WP_MEMORY_LIMIT по умолчанию 40 МБ для внешней части сайта, а WP_MAX_MEMORY_LIMIT - 256 МБ для страниц админки. WordPress может поднять свой лимит только в пределах того, что разрешает PHP, так что верными должны быть оба. Где лежат эти константы, разобрано в статье установка WordPress вручную.

На контейнерном хостинге есть и третий потолок: собственная память контейнера. Тариф с 1 ГБ и memory_limit в 512 МБ выдержит два тяжёлых запроса одновременно, прежде чем ядро остановит контейнер. На RE:NODE достижение лимита контейнера останавливает его и запускает чистым, а не даёт уйти в своп, так что сайт возвращается быстро, но запросы в процессе выполнения теряются. Подбирайте memory_limit и число воркеров вместе, исходя из тарифа, - арифметика есть в статье сколько RAM нужно WordPress.

Размер загрузки: три лимита, а не один#

Неудавшаяся загрузка - самый частый вопрос про php.ini, и вину обычно возлагают на одну настройку, хотя задействованы три или четыре.

НастройкаПо умолчаниюЧто ограничивает
upload_max_filesize2MСамый большой отдельный загружаемый файл
post_max_size8MВсё тело запроса, все файлы плюс поля
max_file_uploads20Число файлов в одном запросе
memory_limit128MДолжен превышать post_max_size для больших запросов
max_execution_time30Достаточно времени, чтобы принять и обработать файл

Они должны быть согласованы, и порядок важен: post_max_size обязан быть больше upload_max_filesize, потому что тело запроса содержит файл, плюс поля формы, плюс служебные данные multipart. Разумный набор для сайта, принимающего загрузки до 64 МБ:

php.ini
upload_max_filesize = 64Mpost_max_size = 72Mmemory_limit = 256Mmax_execution_time = 120

Если загрузка всё равно падает с ошибкой 413, а не с сообщением PHP, дело не в PHP. У обратного прокси или веб-сервера впереди есть собственное ограничение на размер тела - в nginx это client_max_body_size, по умолчанию 1 МБ, - и он отклоняет запрос ещё до того, как его увидит PHP. На управляемом хостинге напрямую вы этим не управляете, и это веское основание импортировать большие дампы базы данных в виде сжатого файла, а не «сырого». Какие лимиты где живут, объясняет статья что делает обратный прокси.

max_execution_time и окружающие его таймауты#

max_execution_time по умолчанию равен 30 секундам и, в сборках для Unix, считает только время, когда выполняется сам PHP, - а не время ожидания запроса к базе данных или удалённого API. Интерпретатор командной строки ставит его в 0, то есть без ограничения, - это ещё одна причина, по которой скрипты в консоли ведут себя иначе.

Повышайте его под конкретную задачу, а не глобально. Глобальный лимит в 300 секунд означает, что застрявший запрос занимает воркер на пять минут, а двадцать таких заполняют пул. Долгую работу нужно отдавать туда, где ей место: в плановую задачу, очередь или командную строку.

Вокруг него есть ещё три таймаута, которые оборвут запрос независимо от того, что думает PHP:

  • `max_input_time` ограничивает разбор тела запроса, а это важно при медленных загрузках. Встроенное значение -1 означает, что берётся max_execution_time, тогда как поставляемый продакшен-файл ставит 60 секунд.
  • `request_terminate_timeout` в PHP-FPM просто убивает воркер. Он важнее max_execution_time и даёт 502, а не ошибку PHP, так что запрос, умерший без записи в журнале ошибок PHP, обычно умер здесь.
  • Собственный таймаут чтения у прокси, обычно 60 секунд, который возвращает посетителю 504, пока PHP незаметно продолжает работать.

Импортёр плагина, который «каждый раз останавливается на 60 процентах», почти всегда упирается в один из двух последних, а не в первый.

max_input_vars, тихая настройка#

max_input_vars ограничивает, сколько переменных может отправить один запрос, и по умолчанию равен 1000. Это самая коварная настройка в списке, потому что превышение не вызывает вообще никакой ошибки: PHP обрезает ввод и отдаёт скрипту неполный массив.

Симптомы такие, что никто не связывает их с файлом конфигурации. Меню WordPress со 120 пунктами сохраняет первые 90 и теряет остальные. Страница настроек темы откатывает половину полей. Товар WooCommerce с множеством вариаций теряет их при сохранении. Массовое редактирование применяется лишь к части строк.

php.ini
max_input_vars = 5000

Каждый вложенный элемент массива считается переменной, поэтому большое меню или вариативный товар упираются в 1000 гораздо раньше, чем следует из числа пунктов. Разумная настройка - от 3000 до 5000; гораздо больше - это способ спрятать форму, которую следовало бы разбить на страницы.

OPcache и кэш realpath#

Без OPcache PHP разбирает и компилирует каждый файл при каждом запросе. С ним скомпилированные опкоды лежат в разделяемой памяти и используются повторно, а это значительная доля времени PHP в любом приложении, состоящем из тысяч мелких файлов.

php.ini
opcache.enable = 1opcache.memory_consumption = 192opcache.interned_strings_buffer = 16opcache.max_accelerated_files = 20000opcache.validate_timestamps = 1opcache.revalidate_freq = 2realpath_cache_size = 4096Krealpath_cache_ttl = 600

Что делают эти значения и почему поставляемые часто слишком малы:

  • `opcache.memory_consumption` по умолчанию 128 МБ. Когда память заполняется, OPcache молча перестаёт кэшировать новые файлы, и сайт замедляется без всякой ошибки. Смотрите в opcache_get_status() значение used_memory и число кэшированных ключей относительно максимума.
  • `opcache.max_accelerated_files` по умолчанию 10 000, и большое приложение с множеством плагинов действительно может это число превысить. Внутри значение округляется вверх до числа на основе простого, так что 20 000 - нормально.
  • `opcache.validate_timestamps` со значением 1 означает, что PHP проверяет время изменения файлов каждые revalidate_freq секунд. Значение 0 быстрее и правильно для развёрнутого приложения, где PHP перезапускают после каждого релиза. Оно неверно для всего, что обновляет свои файлы из веб-интерфейса, как WordPress, потому что обновления как будто не вступают в силу, пока PHP не перезапустят.
  • Кэш realpath хранит разрешённые пути к файлам. Умолчание в 4 МБ мало для фреймворка с глубокими путями автозагрузки; увеличение его и продление TTL убирает на удивление много вызовов stat.
  • JIT по умолчанию фактически выключен, потому что opcache.jit_buffer_size равен 0, пока вы его не зададите. Для веб-приложений так и оставьте: выигрыш даёт числовая и долгая работа, а не запросный код, проводящий время в базе данных. В статье скорость и кэширование WordPress OPcache поставлен в ряд того, что действительно сдвигает время до первого байта.

Ошибки, логи и то, что посетители не должны видеть#

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

php.ini
display_errors = Offdisplay_startup_errors = Offlog_errors = Onerror_log = /var/log/php/error.logerror_reporting = E_ALL & ~E_DEPRECATEDexpose_php = Off

display_errors выключен, а log_errors включён - вот и всё правило: ничего в браузер, всё в файл, который вы можете прочитать. Задайте error_log как путь, в который PHP может писать, либо оставьте его незаданным, и ошибки попадут в лог веб-сервера, который панель показывает в консоли. expose_php в Off убирает заголовок X-Powered-By с точной версией вашего PHP - мелочь, но бесплатная.

В копии для разработки включите display_errors и поставьте error_reporting в E_ALL, включая deprecation. Уведомления об устаревании - это способ заранее узнать, какие плагины сломаются на следующей версии PHP, и это гораздо лучше, чем узнавать в день обновления.

Настройки, связанные с безопасностью#

Ряд директив стоит выставить осознанно, а пара популярных стоят меньше, чем их репутация.

  • `allow_url_fopen` включена по умолчанию и позволяет file_get_contents() забирать URL. Её отключение блокирует класс серверных подделок запросов (SSRF) и ломает плагины, использующие её вместо cURL. Проверьте, прежде чем фиксировать. Её опасная родственница allow_url_include объявлена устаревшей в PHP 7.4 и удалена в 8.0, так что это уже не ваша забота.
  • `disable_functions` может убрать exec, shell_exec, passthru, system и proc_open. Это действительно ограничивает возможности загруженного веб-шелла. Оно же ломает плагины бэкапов, вызывающие mysqldump, и инструменты для изображений, обращающиеся к бинарникам imagick, так что сперва проверьте, что использует ваш стек.
  • Cookie сессий. session.cookie_httponly = 1, session.cookie_secure = 1 на сайте с HTTPS, session.use_strict_mode = 1 и session.cookie_samesite = Lax. Фреймворки обычно ставят свои cookie сессий и это игнорируют, но всё, что использует «сырые» сессии PHP, выигрывает.
  • `open_basedir` ограничивает, к каким каталогам PHP может прикасаться. Полезно на машине, где несколько сайтов работают под одним процессом PHP; в основном избыточно, когда каждый сайт - это свой контейнер, а именно такова модель RE:NODE, где каждый сервер работает в собственном контейнере с тем CPU и памятью, которые купил тариф.
  • `date.timezone` к безопасности не относится, но поставьте UTC и храните всё в UTC. Плановые задачи и сопоставление логов от этого становятся проще, а переход на летнее время перестаёт быть ежегодным происшествием.

PHP-FPM: настройки, которых нет в php.ini#

Конфигурация пула определяет, сколько запросов может выполняться одновременно, и это отдельный файл - обычно /etc/php/8.2/fpm/pool.d/www.conf.

www.conf
pm = dynamicpm.max_children = 12pm.start_servers = 3pm.min_spare_servers = 2pm.max_spare_servers = 6pm.max_requests = 500request_terminate_timeout = 120php_admin_value[memory_limit] = 256M

Главный параметр - pm.max_children, и это расчёт памяти, а не вопрос предпочтений:

code
pm.max_children = (memory available to PHP) / (average process size)

Воркер WordPress обычно занимает от 40 до 80 МБ в резидентной памяти. На тарифе с 1 ГБ, после веб-сервера и всего прочего, это примерно от восьми до двенадцати процессов. Если поставить 50, мощности это не создаст; вы получите машину, умирающую под нагрузкой, которую она должна была выдерживать. pm = ondemand подходит небольшому сайту с редким трафиком, потому что простаивающие воркеры завершаются и возвращают память.

pm.max_requests перезапускает каждый воркер после определённого числа запросов, что скрывает медленные утечки памяти в сторонних расширениях. Нормально 500-1000, а цена ничтожна.

Обратите внимание на php_admin_value[memory_limit]: заданные так значения нельзя переопределить через ini_set() в коде, и именно так хостинг навязывает лимит. Если вы поднимаете memory_limit в wp-config.php и ничего не меняется, обычно причина в этом. На хостинге с панелью ищите ту же идею, выраженную переменной окружения или полем запуска, а не файлом, который вы правите, - и если нужная вам настройка такая, что PHP не позволит её менять файлу уровня каталога, самое время открыть тикет, а не продолжать править. С точки зрения фреймворка те же настройки пула разобраны в статье развёртывание Laravel в продакшене.

FAQ#

Я изменил php.ini, и ничего не произошло. Почему?

Четыре обычные причины: вы правили файл командной строки, а не веб-файл; вы не перезагрузили PHP-FPM после этого; ваше значение переопределяет файл в conf.d или php_admin_value пула; либо вы использовали .htaccess на сервере с PHP-FPM, где эти директивы игнорируются. Проверьте через phpinfo() и прочитайте строку Loaded Configuration File.

Какой memory_limit нужен WordPress?

256 МБ хватает почти любому настоящему сайту, включая WooCommerce. 128 МБ достаточно для небольшого блога с малым числом плагинов. Если для загрузки страницы нужно больше 512 МБ, что-то загружает слишком много данных, и повышение лимита лишь откладывает сбой.

Почему загрузка обрывается ровно на 2 МБ или 8 МБ?

Это умолчания upload_max_filesize и post_max_size. Увеличьте оба, оставив post_max_size больше, и убедитесь, что memory_limit превышает новый post_max_size. Если запрос падает с 413, а не с сообщением PHP, его первым отклоняет прокси впереди.

Безопасно ли ставить max_execution_time в 0?

В командной строке да, там это и так значение по умолчанию. На веб-сервере нет: застрявший запрос занимает воркер навсегда, и достаточное их число исчерпывает пул. Задайте ограниченное значение для задачи, которой оно нужно, а по-настоящему долгую работу перенесите в плановую задачу.

Какую версию PHP запускать?

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

Нужно ли что-то настраивать, если PHP обслуживает хостинг?

Обычно только memory_limit, размеры загрузки и max_input_vars, и они нередко вынесены в настройки, а не в файл. OPcache и размер пула хостинг обычно уже выставил разумно, и менять их без измерений - это способ превратить работающий сайт в медленный.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000