За 2.5 года использования symfony мне постоянно приходится сталкиваться с проблемой недопонимания программистами на symfony идеи html-кеширования. Цель этого поста — донести до светлых умов symfony-девелоперов осознание парадигмы использования partials & components.
Итак, о какой проблеме идет речь. Любой человек, дошедший до понимания идеи фреймворка, в частности симфони, худо-бедно слышал про оптимизацию. Стремление к преждевременной оптимизации у разработчиков проектов убивает все идеи, на которых построена кеш-система symfony.
Для примера буду использовать наш недавно запущенный проект Эмоции пока не остыли. Суть проекта: продажа горящих дешевых путевок. Очень часто обновляющаяся информация должна агрегироваться в рсс-ленту, показыватьcя на основных страницах сайта. Главная страница — листинг с фильтрами, основной интерфейс сайта, она должна работать с минимальными затратами ресурсов и максимально быстро.
Обновление информации идет очень часто: предложения имеют актуальность несколько дней, информация по ним может часто меняться. Получается такой общественный блог, который пишут специально обученные люди: )
Перейдем к упрощенной реализации.
Наш пример: есть турпутевки — таблица А, есть информация о туроператорах — табица В, есть информация по отелям — таблица С. Там еще много всего есть, но для нас это не имеет значения.
В качестве ORM используем Propel.
Задача: создать страницу листинга товаров. В листинге нужно выводить краткую информацию по отелю, в который поедет турист и по поставщику услуги — туроператору. То же самое при листинге холодильников или телефонов или чего еще можно продавать: информация о производителе, информация о категории товара, о поставщике, о доставке… Суть одна: надо выбрать множество сущностей из базы. Причем не абы-как, а желательно используя парадигмы ORM.
Стандартное решение, которое приходит на ум — выбрать индексированные массивы объектов из таблиц А, В, С, используя в качестве индексов уникальные ключи сущностей. И произвести выборку в основном экшене, на который передается поток управления на странице листинга.
Считаем, что в модели у нас следующие классы: A, B, C, APeer, BPeer, CPeer.
Листинг делаем с некими фильтрами, тонкости которых нас совершенно не волнуют.
Что мы тут сделали: По фильтрам, полученным из запроса, вернули объект Criteria из объекта AFormFilter instanceof sfFormFilter. Далее выбрали необходимые нам записи из таблиц В и С, например по id всех выбранных сущностей из таблицы A и проиндексировали. И сделали все это в созданных нами методах retrieveBySelectedA в классах BPeer и CPeer. 3 целевых обращения к базе. Неплохо.
Далее мы создаем /actions/index/templates/listingSuccess.php, в котором выводим листинг элементов.
Естественно можно еще попрятать код для максимальной абстракции, я этим заниматься не буду, нам важна идея.
Включаем кеш. Первое, с чем мы сталкиваемся — что при наличии параметров (GET или POST запроса) Экшн не кешируется. Ок, что же делать: выносить запрос к базе в компонент, передавая ему лишь массив пришедших параметров.
Ладно, а если у нас не страница листинга, а обычная рядовая индексная страница. Можно закешировать все с лейаутом, указав в /actions/index/config/cache.yml следующий параметр:
Это подойдет для железобетонной статической страницы. Но что если у нас есть авторизация, значит хедер явно отличается для каждого пользователя. Кешировать всю страницу целиком нельзя. Вывод — выносить все в партиалы и кмпоненты.
Симфони замечателен тем, что кеширует любой элемент страницы, добавленный через PartialHelper: include_partial и include_component. При этом кеширует вообще все элементы, те если был очищен кеш родительского элемента — кеш дочерних элементов будет сохранен.
Итак, вернемся к странице листинга. Выносим выборку в отдельный компонент:
Отлично, теперь у нас для одинаковых параметров фильтров будет подтягиваться закешированный шаблон, и таким образом количество целевых запросов — 0. Для новых параметров оно по-прежнему равняется 3.
Все хорошо, пока не добавляется новый товар. Как правильные ребята, мы вешаем на экшн сохранения в админке следующий код:
Кеш очистили. Для всех запросов опять по 3 обращения к базе.
Система благополучно работает. В действительности, конечно, все сложнее. Первая проблема, с которой мы столкнулись — ORM кушала много памяти. Попробовали оптимизировать, переведя на обычный sql-запрос с выборкой только необходимых значений. Возникли проблемы с недостаточностью абстракции — я все же за стандарты кода, накладываемые фреймворком, и не люблю изобретатения велосипедов. Все получилось точно как в учебниках: код, написанный одним человеком, с большими трудозатратами модифицировался другими разработчиками. Оооочень хотелось использовать адекватно абстрагированный ORM.
Естетственно, дошли до кеширования каждого предложения, при этом вынесли дополнительные запросы в отдельный компонент для предложений:
Да, на первый взгляд я пошел на преступление. Беззаботно обращаюсь к базе по 2 раза ради каждого элемента, за что не раз упрекался коллегами, не желающими мыслить в масштабах всей системы в целом.
Давайте все супер упрощенно посчитаем:
Пусть у меня 5 фильтров, в каждом из которых 3 варианта, средняя выборка возвращает 100 результатов. Обновление происходит каждые 5 минут (добавление / изменение 1 записи). В сутки пускай имеем 50к хостов на этой странице, что для ровного счета 2000 хостов в час.
Первый вариант помним, да? — 3 запроса для каждого варианта. Имеем 45 ~= 50 запросов на полное покрытие всех вариантов. Тогда если у нас есть ~150 хостов в 5 минут, то считаем ~50 запросов к базе и 100 чтений из кеша (конечно, я сильно утрирую, считая что каждый 5минутный период покрываются все варианты). За сутки имеем: 50 * 12 * 24 ~=15к целевых чтений из базы.
Второй вариант с кешированием отдельно взятого предложения:
чтобы покрыть все варианты необходимо выполнить: 15вариантов * (1запрос + 100предложений * 2доп.запроса) = 3015 целевых чтений. Каждые 5 минут происходит изменение:
3запроса * 12 * 24 ~= 900 запросов. Итого в сутки имеем примерно 4к целевых чтений из базы.
Таким неочевидным образом мы сократили количество запросов в 4 раза. Что если у нас какой-то блог, где юзеры постоянно апдейтят информацию — мы имеем сильное уменьшение периода обновления, где выгода использования более детального кеширования становится сильно заметнее, переходя уже в порядки.
О времени генерации страницы я рассуждать побоюсь, все зависит от настроек. В своем проекте мы использовали XCache для ViewCache:
Разумеется, время генерации страницы при использовании стандартного sfFileCache несопоставимо больше. Но вопрос конфигурации системы давайте оставим в стороне.
Помимо выигрыша в количестве целевых обращений к базе мы имеем код с супер очевидной прозрачной логикой и максимальной абстракцией, объяснять выгоду от которого думаю не стоит.
При применении ORM в симфони по умолчанию не предусмотрено кеширование самих запросов. Это не потому, что разработчики глупые, а потому, что тут используется кеш другого уровня. Другая идея.
К минусам такого подхода стоит отнести порой очень запутанную логику удаления элементов из кеша. Допустим, пользователь изменяет данные о загруженном им видео, при этом придется очистить кеш для всех элементов с этим видео на страницах листинга (для всех вариантов), для детальной страницы, для элементов из rss-feed если такой имеется и тд. Обо всем этом приходится задумываться и в нужный момент помнить, поэтому в качестве рекомендации могу посоветовать сразу проектировать систему с учетом кеширования.
В нашем проекте админка физически находится в другом контейнере, что затрудняет управление кешем. Поэтому пришлось сделать простенький XMLRPC-интерфейс для этого.
Вынесение затратных операций в отдельные компоненты и есть основная идея html-кеширования. Не нужно изобретать велосипедов, все уже работает, причем очень просто и функционально. Если все парни из моей команды это поймут, может престану с ними ругаться: )
Итак, о какой проблеме идет речь. Любой человек, дошедший до понимания идеи фреймворка, в частности симфони, худо-бедно слышал про оптимизацию. Стремление к преждевременной оптимизации у разработчиков проектов убивает все идеи, на которых построена кеш-система symfony.
Для примера буду использовать наш недавно запущенный проект Эмоции пока не остыли. Суть проекта: продажа горящих дешевых путевок. Очень часто обновляющаяся информация должна агрегироваться в рсс-ленту, показыватьcя на основных страницах сайта. Главная страница — листинг с фильтрами, основной интерфейс сайта, она должна работать с минимальными затратами ресурсов и максимально быстро.
Обновление информации идет очень часто: предложения имеют актуальность несколько дней, информация по ним может часто меняться. Получается такой общественный блог, который пишут специально обученные люди: )
Перейдем к упрощенной реализации.
Наш пример: есть турпутевки — таблица А, есть информация о туроператорах — табица В, есть информация по отелям — таблица С. Там еще много всего есть, но для нас это не имеет значения.
В качестве ORM используем Propel.
Задача: создать страницу листинга товаров. В листинге нужно выводить краткую информацию по отелю, в который поедет турист и по поставщику услуги — туроператору. То же самое при листинге холодильников или телефонов или чего еще можно продавать: информация о производителе, информация о категории товара, о поставщике, о доставке… Суть одна: надо выбрать множество сущностей из базы. Причем не абы-как, а желательно используя парадигмы ORM.
Стандартное решение, которое приходит на ум — выбрать индексированные массивы объектов из таблиц А, В, С, используя в качестве индексов уникальные ключи сущностей. И произвести выборку в основном экшене, на который передается поток управления на странице листинга.
Считаем, что в модели у нас следующие классы: A, B, C, APeer, BPeer, CPeer.
Листинг делаем с некими фильтрами, тонкости которых нас совершенно не волнуют.
- <?php
- class indexActions extends sfActions {
- public function executeListing (sfWebRequest $r) {
- $f = new AFormFilter();
- $f->bind($r->getParameter('filter'));
- if ($f->isValid()) {
- $c = $f->buildCriteria();
- $this->array_of_a = APeer::doSelect($c);
- $this->array_of_b = BPeer::retrieveBySelectedA($this->array_of_a);
- $this->array_of_c = CPeer::retrieveBySelectedA($this->array_of_a);
- } else {
- $this->getUser()->setFlash('error', 'Error while retrieving data.');
- }
- }
- }
Что мы тут сделали: По фильтрам, полученным из запроса, вернули объект Criteria из объекта AFormFilter instanceof sfFormFilter. Далее выбрали необходимые нам записи из таблиц В и С, например по id всех выбранных сущностей из таблицы A и проиндексировали. И сделали все это в созданных нами методах retrieveBySelectedA в классах BPeer и CPeer. 3 целевых обращения к базе. Неплохо.
Далее мы создаем /actions/index/templates/listingSuccess.php, в котором выводим листинг элементов.
Естественно можно еще попрятать код для максимальной абстракции, я этим заниматься не буду, нам важна идея.
Включаем кеш. Первое, с чем мы сталкиваемся — что при наличии параметров (GET или POST запроса) Экшн не кешируется. Ок, что же делать: выносить запрос к базе в компонент, передавая ему лишь массив пришедших параметров.
Ладно, а если у нас не страница листинга, а обычная рядовая индексная страница. Можно закешировать все с лейаутом, указав в /actions/index/config/cache.yml следующий параметр:
- list:
- enabled: on
- with_layout: true
Это подойдет для железобетонной статической страницы. Но что если у нас есть авторизация, значит хедер явно отличается для каждого пользователя. Кешировать всю страницу целиком нельзя. Вывод — выносить все в партиалы и кмпоненты.
Симфони замечателен тем, что кеширует любой элемент страницы, добавленный через PartialHelper: include_partial и include_component. При этом кеширует вообще все элементы, те если был очищен кеш родительского элемента — кеш дочерних элементов будет сохранен.
Итак, вернемся к странице листинга. Выносим выборку в отдельный компонент:
- <?php
- /* actions.class.php */
- class indexActions extends sfActions {
- public function executeListing (sfWebRequest $r) {
- $this->filterParams = $r->getParameter('filter');
- }
- }
- /* end of actions.class.php */
- /* components.class.php */
- class IndexComponents extends sfComponents {
- public function executeListingBlock() {
- $f = new AFormFilter();
- $f->bind($this->filterParams);
- if ($f->isValid()) {
- $c = $f->buildCriteria();
- $this->array_of_a = APeer::doSelect($c);
- $this->array_of_b = BPeer::retrieveBySelectedA($this->array_of_a);
- $this->array_of_c = CPeer::retrieveBySelectedA($this->array_of_a);
- } else {
- $this->getUser()->setFlash('error', 'Error while retrieving data.');
- }
- }
- }
- /* end of components.class.php */
- /* listingSuccess.php */
- <?php include_component('index', 'listingBlock', array('filterParams' => $filterParams)) ?>
- /* end of listingSuccess.php */
Отлично, теперь у нас для одинаковых параметров фильтров будет подтягиваться закешированный шаблон, и таким образом количество целевых запросов — 0. Для новых параметров оно по-прежнему равняется 3.
Все хорошо, пока не добавляется новый товар. Как правильные ребята, мы вешаем на экшн сохранения в админке следующий код:
- <?php
- $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
- sfContext::createInstance($configuration, 'frontend');
- $cacheManager = sfContext::getInstance('frontend')->getViewCacheManager();
- $cacheManager->remove('@sf_cache_partial?module=index&action=_listingBlock&sf_cache_key=*');
Кеш очистили. Для всех запросов опять по 3 обращения к базе.
Система благополучно работает. В действительности, конечно, все сложнее. Первая проблема, с которой мы столкнулись — ORM кушала много памяти. Попробовали оптимизировать, переведя на обычный sql-запрос с выборкой только необходимых значений. Возникли проблемы с недостаточностью абстракции — я все же за стандарты кода, накладываемые фреймворком, и не люблю изобретатения велосипедов. Все получилось точно как в учебниках: код, написанный одним человеком, с большими трудозатратами модифицировался другими разработчиками. Оооочень хотелось использовать адекватно абстрагированный ORM.
Естетственно, дошли до кеширования каждого предложения, при этом вынесли дополнительные запросы в отдельный компонент для предложений:
- /* components.class.php */
- class IndexComponents extends sfComponents {
- public function executeListingBlock() {
- $f = new AFormFilter();
- $f->bind($this->filterParams);
- if ($f->isValid()) {
- $c = $f->buildCriteria();
- $this->array_of_a = APeer::doSelect($c);
- } else {
- $this->getUser()->setFlash('error', 'Error while retrieving data.');
- }
- }
- public function executeOfferItem() {
- $this->b = $this->a->getBRelatedByB();
- $this->c = $this->a->getCRelatedByC();
- }
- }
- /* end of components.class.php */
- /* _listingBlock.php */
- <? foreach ($array_of_a as $a): ?>
- <? include_component('index', 'offerItem', array('a' => $a)) ?>
- <? endforeach ?>
Да, на первый взгляд я пошел на преступление. Беззаботно обращаюсь к базе по 2 раза ради каждого элемента, за что не раз упрекался коллегами, не желающими мыслить в масштабах всей системы в целом.
Давайте все супер упрощенно посчитаем:
Пусть у меня 5 фильтров, в каждом из которых 3 варианта, средняя выборка возвращает 100 результатов. Обновление происходит каждые 5 минут (добавление / изменение 1 записи). В сутки пускай имеем 50к хостов на этой странице, что для ровного счета 2000 хостов в час.
Первый вариант помним, да? — 3 запроса для каждого варианта. Имеем 45 ~= 50 запросов на полное покрытие всех вариантов. Тогда если у нас есть ~150 хостов в 5 минут, то считаем ~50 запросов к базе и 100 чтений из кеша (конечно, я сильно утрирую, считая что каждый 5минутный период покрываются все варианты). За сутки имеем: 50 * 12 * 24 ~=15к целевых чтений из базы.
Второй вариант с кешированием отдельно взятого предложения:
чтобы покрыть все варианты необходимо выполнить: 15вариантов * (1запрос + 100предложений * 2доп.запроса) = 3015 целевых чтений. Каждые 5 минут происходит изменение:
3запроса * 12 * 24 ~= 900 запросов. Итого в сутки имеем примерно 4к целевых чтений из базы.
Таким неочевидным образом мы сократили количество запросов в 4 раза. Что если у нас какой-то блог, где юзеры постоянно апдейтят информацию — мы имеем сильное уменьшение периода обновления, где выгода использования более детального кеширования становится сильно заметнее, переходя уже в порядки.
О времени генерации страницы я рассуждать побоюсь, все зависит от настроек. В своем проекте мы использовали XCache для ViewCache:
- /* factories.yml */
- view_cache:
- class: sfXCacheCache
- param:
- automaticCleaningFactor: 0
- storeCacheInfo: true
Разумеется, время генерации страницы при использовании стандартного sfFileCache несопоставимо больше. Но вопрос конфигурации системы давайте оставим в стороне.
Помимо выигрыша в количестве целевых обращений к базе мы имеем код с супер очевидной прозрачной логикой и максимальной абстракцией, объяснять выгоду от которого думаю не стоит.
При применении ORM в симфони по умолчанию не предусмотрено кеширование самих запросов. Это не потому, что разработчики глупые, а потому, что тут используется кеш другого уровня. Другая идея.
К минусам такого подхода стоит отнести порой очень запутанную логику удаления элементов из кеша. Допустим, пользователь изменяет данные о загруженном им видео, при этом придется очистить кеш для всех элементов с этим видео на страницах листинга (для всех вариантов), для детальной страницы, для элементов из rss-feed если такой имеется и тд. Обо всем этом приходится задумываться и в нужный момент помнить, поэтому в качестве рекомендации могу посоветовать сразу проектировать систему с учетом кеширования.
В нашем проекте админка физически находится в другом контейнере, что затрудняет управление кешем. Поэтому пришлось сделать простенький XMLRPC-интерфейс для этого.
Вынесение затратных операций в отдельные компоненты и есть основная идея html-кеширования. Не нужно изобретать велосипедов, все уже работает, причем очень просто и функционально. Если все парни из моей команды это поймут, может престану с ними ругаться: )