В последнее время всё чаще поднимается вопрос о быстродействии различных CMS и фреймворков. Особенно в плане роста популярности различных "облачных" хостингов, виртуальные машины которых по определению имеют меньшую производительность, чем реальный жёсткий диск и оперативная память аналогичного "настоящего" выделенного сервера. Чтобы не отвечать на одни и те же вопросы много раз, я решил свести воедино всю картину оптимизации быстродействия сайтов в этом посте, раскрывая эту тему простыми словами без сложных технических терминов.
Итак, вспомним как мы все начинали делать сайты. Структура взаимоотношений "человек-сайт" выглядела примерно так:
Запрос -> Интернет -> Сервер хостинга -> Apache -> PHP -> MySQL -> Ответ
Молодой разработчик, воодушевлённый умными статьями про ЧПУ, практически сразу вдохновляется идеей использовать htaccess для манипуляции запросами. Цепочка увеличивается на один шаг:
Запрос -> Интернет -> Сервер хостинга -> Apache -> htaccess -> PHP -> MySQL -> Ответ
Рассмотрим что здесь происходит внутри: первый запрос к сайту порождает множество действий. Сначала дёргается Apache, при этом он отнимает много процессорных ресурсов и памяти. Он проверяет, существует ли файл htaccess, и считывает его – тем самым провоцируя избыточную дисковую активность. Затем он поднимает PHP, который в свою очередь "ест" ресурсы, отсылает запросы к БД и генерирует ответ. Этот ответ – всего лишь исходный html-код страницы. Запрашивающий клиент анализирует этот код и посылает ещё массу запросов для получения картинок, стилей, яваскриптов.
На каждый из этих вторичных запросов снова дёргается Apache, который считывает htaccess, при необходимости аналогично поднимает PHP и так далее. Таким образом, всего на один безобидный запрос к сайту уже генерируется чрезмерная активность в памяти и дисковой подсистеме.
Наш молодой разработчик становится умнее и подключает Nginx. Смотрим на нашу цепочку:
Запрос -> Интернет -> Сервер хостинга -> Nginx -> Apache -> htaccess -> PHP -> MySQL -> Ответ
Сервер Nginx крайне нетребователен к ресурсам и способен отдавать ответы на миллионы запросов в сутки даже на слабеньком сервере. Он просто отдаёт статические файлы (те самые картинки, стили, яваскрипты) не пропуская запросы дальше и не дёргая Apache, а значит давая существенную экономию ресурсов. Уже выйгрыш, но рано радоваться.
Во-первых, Nginx-у пофиг на ваши правила в htaccess. Безусловно, внутри него есть свой аналог, но он не совместим с форматом htaccess Апача, и правила для него надо хранить/вести/обновлять независимо, причём в большинстве случаев для этого потребуются права администратора сервера. Поэтому все ваши хитрости – закрытые директории, автогенерация "превьюшек", счётчики на скачивание файлов, блокировка спамеров по IP и прочее – могут кануть в небытие. Если, конечно же, вы не предпримете дополнительных мер.
Во-вторых, вспомним, что для генерации html-кода страниц нам по-прежнему требуется PHP. Как он работает? Он считывает исходный код php-файлов, компилирует его в реальном времени в исполняемый байт-код, и исполняет. Каждый раз при запросе он всё это делает снова и снова. Умные люди догадались сократить эту операцию, и стали применять так называемые акселераторы. Их суть проста: они один раз компилируют php-файлы, и сразу подсовывают готовый байт-код. Это помогает.
Но прогресс не стоит на месте, наш молодой разработчик осознаёт что не всегда хорошо писать php и html-код вперемешку, и натыкается на модную технологию XSLT. Это же так клёво – использовать одни и те же шаблоны на разных сайтах, унифицируя "выдачу" системы в XML-формате и просто применяя к ней XSL-трансформацию. Но спустя некоторое время он понимает (а скорее – не понимает), что сел обеими половинками своей задницы на два подводных камня: избыточность выдаваемых данных и ресурсоёмкость XSL-преобразований. Рассмотрим их:
Любая работа с XML-данными – ресурсоёмкая по определению. XML-файл – это тупой огромный кусок текста, составленный определённым образом, чтобы обозначать для человека древовидную структуру данных. Собрать его – крайне ресурсоёмкое дело. Прочитать его, а тем более "пройти" по дереву в поисках элемента, подходящего под требуемое условие – ещё более "тяжёлая" задача. Всё, что связано с обработкой строк, по определению медленно. Поэтому когда дело касается обработки XML-данных, их преобразований по особым правилам, формирования новых XML-файлов – всегда съедается огромное количество ресурсов. А вспомните, что в описанной схеме это происходит при каждом запросе, а запросов бывает – десятки в секунду.
Второй подводный камень – избыточность выдаваемых данных. Когда разработчик не знает, какие данные у него хотят получить, он для упрощения собирает и отдаёт все. Спросишь у него одну страницу – он тебе отдаст и её, и её потомков, и её предков (я имею в виду конечно не лично человека-разработчика, а систему, которая эти данные выдаёт в ответ на запрос). Хочешь построить меню? Вот тебе все страницы-потомки. Хочешь построить навигацию по "хлебным крошкам"? Вот тебе всё дерево предков до текущей страницы. И по каждой, подчёркиваю, по каждой странице система лезет в БД и достаёт оттуда массу избыточной информации – разумеется, в целях универсальности. Мало ли, мне потребуется узнать какие права доступа указаны у над-над-страницы на вот этот зелёненький блок с баннером? И система отдаёт всё сразу. А это – сотни и тысячи запросов к БД. Снова и снова, каждую секунду.
Избыточность – зло, прикрытое универсальностью. Осознав это, наш уже немного повзрослевший разработчик пытается закэшировать объекты системы, чтобы быстрее получать их из БД. В качестве самого простого решения он пишет их в файлы. И спустя некоторое время и здесь он жёстко разбивается о твёрдые камни реальности: объектов так много, что файловая система не справляется с их объёмом, и ресурсоёмкость "доставания" объекта из файлового кэша становится равной или превышает таковую при "доставании" из БД.
Лирическое отступление: а как же оптимизация БД, спросите вы? – Она за рамками данного поста. Здесь мы говорим обо всём, что не касается базы данных, потому что её оптимизация – это отдельная работа, и это просто очевидная необходимость, которой посвящена масса соответствующей литературы.
Наш разработчик тщательно гуглит и вытаскивает на свет божий так называемые key-value хранилища, среди которых популярны memcache и redis. Кратковременный экстаз от офигенной скорости их работы (а она действительно достойна уважения "на малых оборотах") быстро сменяется осознанием реальности: объектов опять так много, что весомого прироста скорости нет. Это всё равно что представить себе 20-этажный дом с 5-ю подъездами: легко найти квартиру, когда на каждом подъезде написаны номера. Но представьте себе 100-этажный дом с одним подъездом: вроде бы количество квартир то же самое, но легко ли вам теперь найти нужную?
На помощь первому разработчику, который уже опустил руки, приходит второй. И говорит: а нафига ты возишься со своими объектами? Давай кэшировать конечный результат: html-код страницы, который получается на выходе из системы. А давай! И вот разгневанные пользователи пишут: почему не работает сортировка? почему в разных категориях каталога одни и те же товары? почему я кладу товар в корзину, а в углу по-прежнему написано "у вас ноль товаров"? почему результаты голосования не меняются? почему счётчик комментариев не растёт?
Потому что кэшировать надо с умом. И разработчики осознают, что за казалось бы простой задачей "положить страницу в кэш" встаёт гораздо большая задача – верстать сайты так, чтобы динамические элементы подгружались аяксом. Аяксовая корзина, аяксовая сортировка, аяксовое голосование и так далее. А это – огого сколько перевёрстывать надо.
К тому же такое кэширование не всегда спасает. Для аудитории, которая преимущественно ходит на "морду" сайта, это вполне сойдет – "морда" для всех одинаковая. А как только пользователь авторизовался, в подавляющем большинстве случаев контент ему выдаётся динамический. Всем кэшировать каждому отдельно – кэша не напасёшься. Что же делать?
И тут на сцену выходит концепция так называемого partial caching, которая была известна уже несколько лет назад, но в российских продуктах практически не применялась. В чём её суть: всё, что вы видите на страницах сайта, подразделяется на логические блоки. Причём (что важно!) это блоки, понимаемые обычным человеком. Их очень легко назвать: главное меню, меню категорий каталога, таблица товаров-новинок, голосование или опрос…
Так почему бы не кэшировать их по-отдельности? Просто и тупо кэшировать куски html-кода, из которых "лепится" вся страница. Например, главное меню вы можете не менять годами: пусть лежит себе в кэше. Меню категорий, скорее всего, вы последний раз меняли больше месяца назад. Товары появляются чаще, но и таблицу товаров-новинок наверняка можно обновлять раз в час или раз в день. Поймите главную суть: время жизни кэша для различных блоков должно быть разным, подходящим каждому блоку по смыслу и предназначению. Когда вы задумаетесь, сколько на ваших страницах реально динамических данных, а сколько – безжизненного статичного контента, вы осознаете, что можете ускорить свою систему на порядки.
Конечно же, не бывает бочки мёда без ложки дёгтя. Нельзя тупо кэшировать блоки на N минут. Если я поменял, например, описание товара – то кэш с этим описанием должен обновиться тут же, а не завтра утром. Поэтому здесь перед нами предстаёт во всей красе вторая неотъемлимая часть partial caching – это так называемый cache dependency. В буквальном смысле он означает, что любая модификация объекта должна посылать сигнал для обновления соответствующего кэша (и возможно – надстоящего кэша, если данный кусок является частью большего).
В завершение скажу: самый быстрый сайт – это статический HTML под Nginx на рамдиске. Может быть вам такой и нужен? 🙂 Если нет, то все мероприятия по кэшированию должны производиться с трезвым расчётом и тщательным тестированием результатов.