Что на самом деле происходит, когда вы нагружаете PHP-приложение миллионами запросов, и как далеко оно действительно может зайти?
Я пишу на PHP уже восемь лет. Достаточно долго, чтобы застать деплой кода на PHP 5.6, где в легаси-файлах все еще таился mysql_query(), и достаточно долго, чтобы сейчас писать на PHP 8.5. Поэтому я хочу ответить на вопрос, который мне до сих пор часто задают:
Может ли PHP масштабироваться на самом деле? Способен ли он обрабатывать миллионы запросов? Тысячи одновременно работающих пользователей, делающих запросы к базе данных, без падения сервера?
Честный ответ — да, но это требует усилий. Поэтому в этой статье мы погрузимся глубоко: откуда взялся PHP, что на самом деле происходит на сервере, когда приходит PHP-запрос, и что действительно нужно, чтобы масштабировать PHP-приложение под серьезные нагрузки.
Краткая история PHP
PHP зародился в 1994 году как личный набор CGI-скриптов, которые Расмус Лердорф написал для отслеживания посещений своей страницы с резюме. Он назвал их «Personal Home Page Tools». Это были все амбиции, с которыми он создавался.
- PHP/FI (1995) добавил интерпретатор форм и интеграцию с базами данных; это был первый намек на то, что проект превращается в настоящий веб-инструмент, а не просто хак.
- PHP 3 (1998) стал первой версией, которая действительно выглядела как язык, с полноценным парсером, переписанным Энди Гутмансом и Зеевом Сураски.
- PHP 4 (2000) представил Zend Engine: первую настоящую виртуальную машину под капотом языка, и момент, когда PHP перестал быть просто скриптовой диковинкой и стал инфраструктурой. Именно на этой версии был построен WordPress и огромная часть веба начала 2000-х.
- PHP 5 (2004) принес полноценное объектно-ориентированное программирование: интерфейсы, абстрактные классы, исключения. Именно эта версия сделала словосочетание «PHP-фреймворк» осмысленным, а Symfony (2005) и позже Laravel построили на его основе целые экосистемы.
- PHP 7 (2015) — это версия, которая сама по себе должна была похоронить миф о том, что «PHP медленный». Переписанный Zend Engine примерно удвоил производительность и сократил потребление памяти почти вдвое по сравнению с PHP 5.6 — бесплатно, без необходимости вносить изменения в код.
- PHP 8 (2020) добавил JIT-компилятор, union types, именованные аргументы и выражения match. Последующие релизы 8.1–8.5 принесли enums, readonly-свойства, fibers (настоящие, первоклассные корутины: кооперативная многозадачность без расширений) и property hooks. Это совсем не тот PHP, который представляют себе критики, когда пытаются его задеть.
И экосистема вокруг языка развивалась не меньше, чем сам язык. Composer (2012) дал PHP настоящий менеджер зависимостей и практически за одну ночь покончил с эпохой копирования библиотек вручную. Стандарты PSR дали экосистеме общие интерфейсы вместо того, чтобы каждый фреймворк заново изобретал HTTP-сообщения и автозагрузку. Laravel превратил developer experience в доминирующую философию PHP-фреймворков, подобно тому, как Rails когда-то сделал это для Ruby. И совсем недавно такие инструменты, как Laravel Octane и FrankenPHP (современный сервер приложений PHP, написанный на Go, построенный на базе веб-сервера Caddy, с первоклассной интеграцией worker-mode для Laravel и Symfony), начали атаковать то самое архитектурное допущение, которое изначально делало PHP «медленным»: необходимость загружать фреймворк с нуля при каждом запросе. Подробнее об этом чуть позже.
На сегодняшний день PHP по-прежнему обеспечивает работу примерно 7 из 10 веб-сайтов в публичном вебе, где известен серверный язык программирования, согласно регулярным опросам W3Techs, и большинство этих сайтов сейчас работают на PHP 8. Фразу «PHP мертв» повторяют каждый год примерно с 2008 года. При этом он с большим отрывом остается самым развертываемым серверным языком в интернете. То, что оба этих утверждения верны одновременно, и есть настоящая, гораздо более интересная история.
Что на самом деле происходит под капотом
Вот откуда на самом деле берется репутация «не масштабируется», и она уходит корнями в реальные факты, а не просто в старые предрассудки.
Доминирующим способом деплоя PHP на протяжении большей части его жизни был PHP-FPM (FastCGI Process Manager), работающий за Nginx или Apache. Когда приходит запрос:
- Веб-сервер принимает TCP-соединение и передает запрос свободному воркеру FPM по протоколу FastCGI.
- Этот воркер запускает приложение (или восстанавливает его из OPcache, кэша байт-кода PHP, если он прогрет), обрабатывает запрос и возвращает ответ.
- После этого практически все состояние, созданное в рамках запроса, уничтожается. Воркер не помнит предыдущий запрос. Он не держит соединение с базой данных открытым. Он не сохраняет в памяти ничего, кроме того, что OPcache уже закэшировал на уровне байт-кода.
- Воркер возвращается в пул и ожидает следующего запроса, который может принадлежать совершенно другому пользователю и который заново проделает все вышеописанное с чистого листа.
Это называется архитектурой без разделения ресурсов (shared-nothing architecture), и это осознанная философия проектирования, а не случайность. У нее есть огромные плюсы: ни один запрос не может «отравить» состояние другого, один некорректный запрос не может нарушить работу долгоживущего процесса, а для исправления последствий неудачного деплоя достаточно просто перезапустить воркеры. Именно поэтому классический PHP-хостинг был таким неприхотливым и дешевым на протяжении двух десятилетий: сценарии сбоев были изолированы по дизайну.
Минус — это как раз то, что порождает репутацию «не масштабируется»: поскольку между запросами ничего не сохраняется, каждый воркер, обращающийся к базе данных, открывает новое соединение на каждый запрос и закрывает его по окончании работы. Умножьте это на количество воркеров на сервере, а затем еще раз на количество серверов за балансировщиком нагрузки во время пика трафика, и вы получите количество соединений, которое может превысить лимиты вашей базы данных задолго до того, как она упрется в CPU или диск. Это и есть реальная причина большинства инцидентов вида «PHP упал» на практике. Это проблема архитектуры, лежащая на уровень ниже самого языка, и она решаема, о чем и пойдет речь в следующем разделе.
Итак, может ли он масштабироваться? Да, но не бесплатно
Я хочу быть предельно честным: масштабирование PHP до миллионов пользователей — это не история про «включить одну галочку в конфиге». Любой, кто говорит вам, что это не требует усилий, пытается вам что-то продать. Правда в том, что необходимые шаги хорошо изучены, проверены огромными продакшн-системами и не требуют отказа от языка. В целом все сводится к нескольким рычагам, применяемым осознанно:
Сделайте сам путь обработки запроса быстрым. OPcache (кэширование байт-кода) и, начиная с PHP 8, JIT-компилятор устраняют большую часть накладных расходов вида «PHP должен заново парсить и интерпретировать все при каждом запросе», которые изначально создавали языку репутацию медленного. Кэширование конфигурации, роутов и скомпилированных шаблонов на уровне фреймворка убирает накладные расходы на рефлексию и файловую систему, которые фреймворки иначе несут при каждом запуске.
Перестаньте пересобирать мир при каждом запросе. Именно здесь серверы с поддержкой worker-mode, такие как Laravel Octane и FrankenPHP, меняют правила игры: вместо модели shared-nothing с запуском с нуля при каждом запросе, приложение запускается один раз и остается в памяти для обработки множества запросов, приближаясь к тому, как работают долгоживущие сервисы на Node или Go. Вы жертвуете частью безопасности shared-nothing ради огромного снижения накладных расходов на запрос. Примечательно, что одно из самых перспективных решений здесь, FrankenPHP, само написано на Go и встраивает рантайм PHP, а не заменяет его. Это не противостояние PHP и Go; это Go и PHP, решающие проблему вместе.
Перестаньте открывать соединение с базой данных на каждый запрос, воркер и сервер. Это самая частая реальная причина инцидентов «PHP не масштабируется», и она решается так же, как и в любом другом shared-nothing стеке с высокой потребностью в соединениях: установите слой пула соединений — специализированный прокси вроде ProxySQL или другое готовое решение — между пулом приложений и базой данных, чтобы количество реальных соединений оставалось ограниченным, независимо от того, сколько воркеров или серверов приложений вы добавляете.
Выносите работу за пределы цикла запрос/ответ. Очереди (первоклассный, отлично поддерживаемый паттерн в любом серьезном PHP-фреймворке) позволяют принять запрос, подтвердить его и выполнить ресурсоемкую часть асинхронно. Одно это снимает огромную часть нагрузки с синхронного пути, на котором обычно фокусируются при обсуждении масштабирования.
Масштабируйте слой данных по его собственным правилам. Реплики для чтения при высокой нагрузке на чтение, слои кэширования (Redis/Memcached) перед «горячими» запросами и шардирование для тяжелых нагрузок на запись, которые иначе упирались бы в одну основную базу. В этом нет ничего специфичного для PHP; это тот же набор правил, который используется в любом языке, когда узким местом становится база данных, а не рантайм приложения.
Переносите статическую и полустатическую работу на edge-уровень. CDN, полностраничное кэширование и логика на edge-серверах позволяют предотвратить попадание огромной части запросов на воркеры PHP. Часто это самый эффективный рычаг, потому что самый дешевый запрос — это тот, который ваше приложение так и не увидело.
Ни один из этих подходов не является экзотическим. Каждый из них — это задокументированный, проверенный в продакшене паттерн, и ни один из них не требует от PHP перестать быть PHP. Требуется лишь относиться к масштабированию как к архитектурной задаче (которой оно и является в любом языке), а не считать рантайм непреодолимым потолком.
Что дальше
Это теория, и я считаю ее убедительной. Но теория стоит дешево, и я предпочитаю доказать это на практике, а не просто спорить. Поэтому в следующей статье я спроектирую реалистичное, приближенное к продакшену PHP-приложение и протестирую его под нагрузками, для достижения которых, как обычно считают, необходим «современный» стек (Go, Node, Rust). Я хочу показать на примере реальной архитектуры и реальных аргументов, что хорошо спроектированная PHP-система может на равных конкурировать со стеками, с которыми ее постоянно невыгодно сравнивают.
Я восемь лет наблюдал, как этот язык недооценивают. Давайте соберем доказательства.
Комментарии (0)
Пока нет комментариев — будьте первым.