Представьте сервер, на котором живут четыре сайта. Корпоративная визитка на самописной CMS, интернет-магазин на популярном движке, блог с десятком плагинов и забытый клиентский лендинг, который кто-то когда-то «починил на пару часов». Все четыре работают на одной версии PHP, потому что так было проще при установке. Теперь эту версию пора менять, и тут выясняется, что у каждого сайта своё мнение на этот счёт.
Соблазн понятен: один пакет, один переключатель, одна перезагрузка сервиса. Удобно ровно до первого проекта, который после обновления выдаёт белый экран из-за функции, убранной из языка. После этого вы чините один сайт под давлением, а три других заложники его проблем. Эта статья о том, как расцепить их судьбы заранее и обновлять сайты по очереди, с репетицией на копии и понятным путём назад.
Три разных «PHP» на одном сервере
Слова «на сервере стоит PHP 8.x» скрывают сразу три отдельные сущности, и путаница между ними причиняет больше всего боли при переносе.
- Команда
phpв терминале. Это интерпретатор, который запускается по умолчанию, когда вы набираетеphp -vилиphp artisan. Его версия определяется системными настройками и переменнойPATH, а не веб-сервером. - Веб-обработчик. Процессы PHP-FPM, которым nginx или Apache передают запросы посетителей. У каждой установленной версии пакета свой набор таких процессов и свои конфигурационные файлы.
- Фоновые задачи проекта. Cron, очереди, воркеры, скрипты импорта и обмена с учётной системой. Они запускаются не через веб-обработчик, а через CLI, причём версия там может оказаться совсем не той, что у сайта в браузере.
Типичная ловушка выглядит так: сайт переключили на новую версию, страницы открываются, все довольны. А ночной скрипт выгрузки заказов по-прежнему запускается системной командой php от старой версии (или, наоборот, внезапно от новой, если системная версия сменилась при обновлении пакетов). Падает он тихо, письмо с ошибкой уходит в ящик пользователя, которого никто не читает. Через неделю выясняется, что выгрузка не работала.
Поэтому первый шаг не техническая команда, а таблица. Для каждого проекта нужно записать три значения: какая версия отвечает на веб-запросы, какая запускает cron и какая нужна фоновым воркерам. Если в какой-то строке значения расходятся, это не обязательно ошибка, но это должно быть осознанным решением.
| Проект | Веб (FPM) | Cron и CLI | Воркеры | Особенность |
|---|---|---|---|---|
| Визитка | старая ветка | старая ветка | нет | самописный код, давно не менялся |
| Магазин | средняя ветка | системная | очередь писем | CLI и веб могут расходиться |
| Блог | средняя ветка | средняя ветка | нет | зависит от плагинов |
| Лендинг | старая ветка | нет | нет | владелец неизвестен |
Названия веток здесь намеренно общие: конкретные номера зависят от вашего дистрибутива и репозитория, и их стоит сверять с тем, что реально доступно у вас, а не с чужой статьёй.
Отдельный пул на каждый сайт: как это устроено
Идея, на которой держится вся схема: PHP-FPM делится на глобальные настройки и настройки пулов, а пулы могут слушать разные сокеты и работать от разных пользователей. Это прямо описано в документации PHP по конфигурации FPM. Для нас важны два следствия.
Первое: одна установленная версия PHP-FPM может обслуживать несколько пулов, и наоборот, разные версии работают независимо, каждая со своей конфигурацией. Значит, сайт можно привязать к конкретному пулу конкретной версии, и смена версии для одного сайта сводится к смене пула, к которому веб-сервер отправляет его запросы.
Второе: у каждого пула может быть собственный пользователь. Это не вопрос красоты. Если все сайты работают от одного пользователя, то уязвимость в одном плагине даёт доступ к файлам всех остальных. Отдельный пользователь на проект не делает код безопаснее, но сужает область, которую можно задеть.
Как выглядит конфигурация пула
Ниже схематичный фрагмент для одного проекта. Пути и номер версии подставьте свои: они зависят от дистрибутива.
[shop]
user = shop
group = shop
listen = /run/php/shop-php.sock
listen.owner = shop
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 8
pm.process_idle_timeout = 30sРазберём ключевые строки по смыслу.
[shop]— имя пула. По нему удобно искать записи в журналах и отличать проекты друг от друга.userиgroup— от кого работает код сайта. Файлы проекта должны принадлежать этому пользователю или быть ему доступны на чтение, а записываемыми оставляйте только каталоги, куда сайту действительно нужно писать: загрузки, кэш, сессии.listen— путь к Unix-сокету. У каждого пула свой файл, и именно его адрес вы потом укажете в конфигурации веб-сервера.listen.owner,listen.group,listen.mode— права на сам сокет. В документации отмечено, что права Unix-сокета на Linux задаются отдельно, поэтому о них нужно подумать явно. Задача простая: подключаться к сокету должен веб-сервер и никто больше.pm.max_children— потолок числа одновременно обслуживаемых запросов для этого пула. Он же защищает соседей: один зависший проект не займёт всю память сервера, а упрётся в собственный лимит.
Если вместо Unix-сокета вы решите слушать TCP-порт, учтите тонкость из той же документации: пустое значение listen.allowed_clients разрешает подключения с любых адресов. На сервере, где PHP-FPM должен быть доступен только локальному веб-серверу, это нежелательный сюрприз, поэтому для схемы «много версий на одной машине» Unix-сокеты проще и аккуратнее: они вообще не выходят в сеть.
Права на файлы без драмы
Типичная ошибка при разделении на пользователей: вы создали пул с новым пользователем, переключили сайт, и он сразу перестал писать загрузки, потому что каталог принадлежит старому пользователю. Поэтому смену пользователя нужно репетировать так же, как смену версии. Практичное правило: код принадлежит владельцу проекта и не записывается из веб-процесса, а для каталога загрузок и кэша выделяется явное разрешение на запись. Сначала проверьте, как ведёт себя сайт на копии, и только потом повторяйте на боевом.
Лимиты пулов и общая память
Когда пулов становится несколько, их потолки pm.max_children складываются. Каждый воркер занимает память, и суммарный максимум по всем пулам должен помещаться в оперативную память сервера вместе с базой данных и самим веб-сервером. Как оценивать этот запас по реальным процессам, подробно разобрано в статье про подсчёт воркеров PHP-FPM до первых 502. Для нашей задачи достаточно одного вывода: две версии PHP работают одновременно, и это не бесплатно. Пока сайты постепенно переезжают, сервер несёт нагрузку обоих рантаймов, и запас по памяти нужен именно на период переезда.
Выбираем первого: кому идти в разведку
Порядок обновления определяет не алфавит и не громкость жалоб владельцев. Первым стоит брать сайт, у которого сочетаются три свойства: понятный код, небольшие последствия сбоя и человек, который может быстро подтвердить, что всё работает.
Худший кандидат на первый заход — самописный лендинг без владельца: никто не скажет, что считать нормой. Не лучший и главный магазин: цена ошибки максимальна. Хороший первый кандидат — блог или второстепенный проект с известным набором плагинов и активным ответственным.
Есть и обратная логика: иногда выгоднее начать с самого проблемного сайта на копии, просто чтобы узнать масштаб бедствия. Но на копии, а не на боевом. Репетиция как раз и нужна, чтобы узнать худшее без свидетелей.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Репетиция на копии одного проекта
Копия сайта нужна не для проверки «открывается ли главная». Её задача выявить зависимости, которые молчат в обычной жизни. Для этого копию нужно сделать максимально похожей на боевой проект по тому, что влияет на выполнение кода.
Что переносить в копию
- Файлы проекта целиком, включая каталоги загрузок, чтобы пути и права вели себя так же.
- База данных в той версии и с теми настройками, что на боевом сайте. Совпадение важно: многие сбои после смены PHP возникают на стыке с базой.
- Конфигурационные файлы приложения, но без боевых ключей внешних сервисов. Копия не должна отправлять настоящие письма клиентам, списывать оплаты или дёргать боевые обмены. Подставьте тестовые значения или отключите отправку.
- Задачи cron в отключённом виде. Их мы запустим вручную и осознанно.
Копию разместите на отдельном поддомене или на закрытом адресе, а свой пул под неё создайте на целевой версии PHP. Боевой сайт при этом продолжает работать на старом пуле, не замечая репетиции.
Что проверять в первую очередь
Сначала выясните, что приложение само заявляет о совместимости. У популярных движков и фреймворков в метаданных проекта обычно указаны допустимые версии PHP, а у менеджера зависимостей есть команды проверки платформы. Но декларации лишь отправная точка: плагины и самописный код могут нарушать их молча. Конкретные версии языка и пакетов, которые вы собираетесь использовать, сверяйте с документацией самого проекта и с заметками о миграции в руководстве PHP, а не с памятью и не с этой статьёй.
Затем включите вывод всех ошибок и предупреждений на копии (только на копии) и пройдитесь по сценариям, а не по страницам. Что значит сценарий: зарегистрироваться, войти, восстановить пароль, положить товар в корзину, оформить тестовый заказ, загрузить файл, отправить форму, открыть админку и сохранить запись. Многие проблемы после смены версии проявляются не на чтении, а на записи и в редко используемых разделах.
Отдельно ищите три класса проблем:
- Фатальные ошибки. Функция или расширение, которых в новой версии нет. Сайт показывает белый экран или код ответа 500. Их видно сразу.
- Предупреждения об устаревшем. Код пока работает, но использует то, что будет убрано в следующей версии. Они дают список долгов, а не повод откладывать переезд.
- Изменения поведения. Самый коварный класс: ошибки нет, но результат другой. Например, иначе обрабатывается сравнение значений или иначе ведёт себя функция работы со строками. Ловятся только сценарной проверкой с ожидаемым результатом.
Не забудьте о расширениях. Для новой версии PHP они ставятся отдельными пакетами, и набор, подключённый к старой, не переезжает автоматически. Запишите список модулей, которые использует проект, получив его командой php -m под старой версией, и сравните с тем, что загружено под новой.
Репетиция CLI-задач
Теперь та часть, про которую чаще всего забывают. Запустите вручную каждый cron-скрипт и каждый воркер проекта, явно указав путь к нужной версии интерпретатора, а не полагаясь на системную команду. Например, вместо голого php используйте полное имя исполняемого файла нужной ветки. В crontab и в юнитах systemd полезно закрепить это явно: так версия задана в самом расписании, а не зависит от настроек окружения.
Проверяйте не код возврата, а результат: появились ли записи в базе, создался ли файл выгрузки, ушло ли тестовое письмо в песочницу. Скрипт может завершиться без ошибки и ничего не сделать.
Переключение: меняем маршрут одного сайта
Когда репетиция прошла без блокирующих проблем, сама смена версии для боевого сайта оказывается удивительно скучной. В конфигурации веб-сервера у проекта есть место, где указан адрес обработчика PHP, то есть путь к сокету пула. Вы создаёте новый пул на целевой версии (по образцу репетиционного), а затем меняете в конфигурации сайта путь к сокету со старого на новый. Остальные сайты продолжают смотреть на свои сокеты и ничего не замечают.
Практически это выглядит как три отдельных шага:
- Новый пул создан и запущен, но на него пока не идёт ни одного боевого запроса.
- В конфигурации веб-сервера для этого одного сайта указан новый сокет. Конфигурация проверена встроенной командой проверки синтаксиса, затем применена мягкой перезагрузкой, без обрыва соединений остальных проектов.
- Старый пул не удаляется. Он остаётся запущенным и просто не получает трафик от этого сайта.
Третий пункт и есть быстрый возврат. Если после переключения что-то пошло не так, вы возвращаете прежний путь к сокету, снова мягко перезагружаете веб-сервер, и сайт опять обслуживается старым рантаймом. Это работает, пока файлы и база не изменены необратимо: если новая версия записала в базу данные в формате, который старый код не читает, откат маршрута уже не спасёт. Поэтому перед переключением зафиксируйте состояние базы вашим обычным способом и заранее решите, какие изменения считаются необратимыми. Гарантий автоматических копий от самого тарифа здесь рассчитывать не стоит: создавайте и проверяйте их сами.
Как понять, что переключение удалось
Опирайтесь не на ощущение, а на заранее записанные критерии. Для типичного проекта это могут быть такие пункты:
- Главная и ключевые страницы отдают нормальные ответы, а в журнале ошибок пула нет новых фатальных сообщений.
- Сценарии записи (форма, заказ, загрузка файла) проходят до конца и дают ожидаемые записи.
- Вручную запущенные CLI-задачи на новой версии выполнились и оставили правильный результат.
- Потребление памяти пулом соответствует ожиданиям и не упирается в потолок
pm.max_childrenпри обычной нагрузке.
Первые часы после переключения особенно внимательно смотрите журнал ошибок именно этого пула. Раздельные журналы по пулам и есть дополнительная выгода всей схемы: ошибка сразу подписана именем проекта, и не нужно гадать, чей скрипт её породил.
Отдельный шаг для cron и воркеров
Веб-маршрут переключается одним изменением. Фоновые задачи этим изменением не затрагиваются, их нужно перевести отдельно, и лучше сразу после подтверждения веба, а не через неделю. Заменяйте в расписании путь к интерпретатору на целевой, перезапускайте воркеры, затем дождитесь ближайшего запуска каждой задачи и убедитесь, что результат появился. Пока не переведены и не проверены оба контура, проект нельзя считать обновлённым: он наполовину в новом мире и наполовину в старом.
Старый рантайм: срок жизни, а не вечное хранение
Раз уж несколько версий PHP удобно сосуществуют, возникает искушение оставить всё как есть навсегда. Это ловушка, и вот почему.
Пул изолирует проекты друг от друга по процессам, пользователям и файлам. Он не делает неподдерживаемую версию языка безопасной. Если ветка PHP больше не получает исправлений безопасности от разработчиков, то уязвимости, найденные в ней, остаются навсегда, и отдельный пул с отдельным пользователем только ограничивает радиус ущерба, но не закрывает саму дыру. Изоляция снижает последствия компрометации одного сайта для других, но не устраняет её причину. Актуальные сроки поддержки конкретных веток смотрите на официальных страницах проекта PHP и в политике вашего дистрибутива: именно оттуда нужно брать даты, а не из этой статьи.
Разумнее относиться к старому рантайму как к временной мере с датой окончания. Для каждого проекта на устаревшей ветке стоит записать три вещи:
- Почему он там. Конкретная зависимость: библиотека, плагин, модуль, не работающий на новой версии.
- Что нужно, чтобы уйти. Обновить плагин, заменить библиотеку, переписать участок кода, договориться с владельцем.
- До какого срока. Дата, к которой вопрос должен быть решён или вынесен на явное решение: заморозить, заменить или выключить сайт.
Если сайт на устаревшей ветке остаётся доступным из интернета, дополнительно ограничьте его: уберите все неиспользуемые функции, закройте административную часть по адресу или дополнительной аутентификации веб-сервера, не храните на этом же пользователе ничего ценного. Это не превращает устаревшую среду в защищённую, а лишь уменьшает риски на то время, пока вы её убираете.
Сам сервер тоже влияет на комфорт такого переходного периода. Пока работают два и больше рантаймов, вам нужны процессор и память с запасом, место на диске для нескольких наборов пакетов и копий проектов, а также root-доступ, чтобы устанавливать пакеты нужных версий и управлять службами. На VPS MAATRIX доступен root и NVMe-диски, а тарифная линейка начинается с конфигурации Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и доходит до Djed (16 vCPU, 32 ГБ, 320 ГБ). Эти ресурсы не равны обещанию ёмкости: сколько пулов и сайтов выдержит конкретный план, показывает только измерение вашей нагрузки. Для небольшого переходного набора сайтов разумно сначала посмотреть реальное потребление памяти пулами, а при нехватке перейти на план побольше.
Есть и вариант, о котором стоит подумать при очень разных требованиях: вынести совсем проблемный проект на отдельный VPS. Это дороже в обслуживании, зато судьбы сайтов разделены не только на уровне процессов, но и на уровне сервера. Решение зависит от важности проекта и от того, насколько близок срок устранения зависимостей, и универсального ответа здесь нет.
Как выглядит порядок на сервере после первого переезда
Допустим, первым обновили блог. Его веб-пул и фоновые задачи работают на новой версии, в учётной таблице напротив него появилась новая строка, а старый пул блога остановлен после нескольких спокойных дней и записан в журнал изменений как отключённый. Магазин, визитка и лендинг продолжают работать на своих рантаймах, и посетители никаких перемен не заметили. Это и есть главный результат: один проект сменил версию, остальные не почувствовали.
Теперь можно выбирать следующего кандидата, уже имея шаблон действий. Логично взять магазин, но не сразу: сначала на его копии нужно разобраться, почему CLI у него использует системную версию, и закрепить версию в расписании. Работа над ним потребует аккуратной проверки сценариев заказа и обмена данными, зато репетиция уже отлажена на блоге. Визитку и лендинг стоит оставить на потом, но не без дела: у каждого должна быть запись о причине, сроке и владельце вопроса. Ближайшим следующим шагом разумно считать магазин, а отдельной задачей параллельно найти человека, который отвечает за лендинг, потому что без него этот проект так и останется последним в очереди.