Обновить

Кеширование в Symfony. Идеология HTML-кеширования. Components & partials

Symfony
За 2.5 года использования symfony мне постоянно приходится сталкиваться с проблемой недопонимания программистами на symfony идеи html-кеширования. Цель этого поста — донести до светлых умов symfony-девелоперов осознание парадигмы использования partials & components.



Итак, о какой проблеме идет речь. Любой человек, дошедший до понимания идеи фреймворка, в частности симфони, худо-бедно слышал про оптимизацию. Стремление к преждевременной оптимизации у разработчиков проектов убивает все идеи, на которых построена кеш-система symfony.

Для примера буду использовать наш недавно запущенный проект Эмоции пока не остыли. Суть проекта: продажа горящих дешевых путевок. Очень часто обновляющаяся информация должна агрегироваться в рсс-ленту, показыватьcя на основных страницах сайта. Главная страница — листинг с фильтрами, основной интерфейс сайта, она должна работать с минимальными затратами ресурсов и максимально быстро.

Обновление информации идет очень часто: предложения имеют актуальность несколько дней, информация по ним может часто меняться. Получается такой общественный блог, который пишут специально обученные люди: )

Перейдем к упрощенной реализации.

Наш пример: есть турпутевки — таблица А, есть информация о туроператорах — табица В, есть информация по отелям — таблица С. Там еще много всего есть, но для нас это не имеет значения.

В качестве ORM используем Propel.

Задача: создать страницу листинга товаров. В листинге нужно выводить краткую информацию по отелю, в который поедет турист и по поставщику услуги — туроператору. То же самое при листинге холодильников или телефонов или чего еще можно продавать: информация о производителе, информация о категории товара, о поставщике, о доставке… Суть одна: надо выбрать множество сущностей из базы. Причем не абы-как, а желательно используя парадигмы ORM.

Стандартное решение, которое приходит на ум — выбрать индексированные массивы объектов из таблиц А, В, С, используя в качестве индексов уникальные ключи сущностей. И произвести выборку в основном экшене, на который передается поток управления на странице листинга.

Считаем, что в модели у нас следующие классы: A, B, C, APeer, BPeer, CPeer.

Листинг делаем с некими фильтрами, тонкости которых нас совершенно не волнуют.

  1. <?php
  2. class indexActions extends sfActions {
  3. public function executeListing (sfWebRequest $r) {
  4. $f = new AFormFilter();
  5. $f->bind($r->getParameter('filter'));
  6. if ($f->isValid()) {
  7. $c = $f->buildCriteria();
  8. $this->array_of_a = APeer::doSelect($c);
  9. $this->array_of_b = BPeer::retrieveBySelectedA($this->array_of_a);
  10. $this->array_of_c = CPeer::retrieveBySelectedA($this->array_of_a);
  11. } else {
  12. $this->getUser()->setFlash('error', 'Error while retrieving data.');
  13. }
  14. }
  15. }


Что мы тут сделали: По фильтрам, полученным из запроса, вернули объект Criteria из объекта AFormFilter instanceof sfFormFilter. Далее выбрали необходимые нам записи из таблиц В и С, например по id всех выбранных сущностей из таблицы A и проиндексировали. И сделали все это в созданных нами методах retrieveBySelectedA в классах BPeer и CPeer. 3 целевых обращения к базе. Неплохо.

Далее мы создаем /actions/index/templates/listingSuccess.php, в котором выводим листинг элементов.

Естественно можно еще попрятать код для максимальной абстракции, я этим заниматься не буду, нам важна идея.

Включаем кеш. Первое, с чем мы сталкиваемся — что при наличии параметров (GET или POST запроса) Экшн не кешируется. Ок, что же делать: выносить запрос к базе в компонент, передавая ему лишь массив пришедших параметров.

Ладно, а если у нас не страница листинга, а обычная рядовая индексная страница. Можно закешировать все с лейаутом, указав в /actions/index/config/cache.yml следующий параметр:

  1. list:
  2. enabled: on
  3. with_layout: true


Это подойдет для железобетонной статической страницы. Но что если у нас есть авторизация, значит хедер явно отличается для каждого пользователя. Кешировать всю страницу целиком нельзя. Вывод — выносить все в партиалы и кмпоненты.

Симфони замечателен тем, что кеширует любой элемент страницы, добавленный через PartialHelper: include_partial и include_component. При этом кеширует вообще все элементы, те если был очищен кеш родительского элемента — кеш дочерних элементов будет сохранен.

Итак, вернемся к странице листинга. Выносим выборку в отдельный компонент:

  1. <?php
  2. /* actions.class.php */
  3. class indexActions extends sfActions {
  4. public function executeListing (sfWebRequest $r) {
  5. $this->filterParams = $r->getParameter('filter');
  6. }
  7. }
  8. /* end of actions.class.php */
  9. /* components.class.php */
  10. class IndexComponents extends sfComponents {
  11. public function executeListingBlock() {
  12. $f = new AFormFilter();
  13. $f->bind($this->filterParams);
  14. if ($f->isValid()) {
  15. $c = $f->buildCriteria();
  16. $this->array_of_a = APeer::doSelect($c);
  17. $this->array_of_b = BPeer::retrieveBySelectedA($this->array_of_a);
  18. $this->array_of_c = CPeer::retrieveBySelectedA($this->array_of_a);
  19. } else {
  20. $this->getUser()->setFlash('error', 'Error while retrieving data.');
  21. }
  22. }
  23. }
  24. /* end of components.class.php */


  1. /* listingSuccess.php */
  2. <?php include_component('index', 'listingBlock', array('filterParams' => $filterParams)) ?>
  3. /* end of listingSuccess.php */


Отлично, теперь у нас для одинаковых параметров фильтров будет подтягиваться закешированный шаблон, и таким образом количество целевых запросов — 0. Для новых параметров оно по-прежнему равняется 3.

Все хорошо, пока не добавляется новый товар. Как правильные ребята, мы вешаем на экшн сохранения в админке следующий код:

  1. <?php
  2. $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
  3. sfContext::createInstance($configuration, 'frontend');
  4. $cacheManager = sfContext::getInstance('frontend')->getViewCacheManager();
  5. $cacheManager->remove('@sf_cache_partial?module=index&action=_listingBlock&sf_cache_key=*');


Кеш очистили. Для всех запросов опять по 3 обращения к базе.

Система благополучно работает. В действительности, конечно, все сложнее. Первая проблема, с которой мы столкнулись — ORM кушала много памяти. Попробовали оптимизировать, переведя на обычный sql-запрос с выборкой только необходимых значений. Возникли проблемы с недостаточностью абстракции — я все же за стандарты кода, накладываемые фреймворком, и не люблю изобретатения велосипедов. Все получилось точно как в учебниках: код, написанный одним человеком, с большими трудозатратами модифицировался другими разработчиками. Оооочень хотелось использовать адекватно абстрагированный ORM.

Естетственно, дошли до кеширования каждого предложения, при этом вынесли дополнительные запросы в отдельный компонент для предложений:

  1. /* components.class.php */
  2. class IndexComponents extends sfComponents {
  3. public function executeListingBlock() {
  4. $f = new AFormFilter();
  5. $f->bind($this->filterParams);
  6. if ($f->isValid()) {
  7. $c = $f->buildCriteria();
  8. $this->array_of_a = APeer::doSelect($c);
  9. } else {
  10. $this->getUser()->setFlash('error', 'Error while retrieving data.');
  11. }
  12. }
  13. public function executeOfferItem() {
  14. $this->b = $this->a->getBRelatedByB();
  15. $this->c = $this->a->getCRelatedByC();
  16. }
  17. }
  18. /* end of components.class.php */


  1. /* _listingBlock.php */
  2. <? foreach ($array_of_a as $a): ?>
  3. <? include_component('index', 'offerItem', array('a' => $a)) ?>
  4. <? 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:

  1. /* factories.yml */
  2. view_cache:
  3. class: sfXCacheCache
  4. param:
  5. automaticCleaningFactor: 0
  6. storeCacheInfo: true


Разумеется, время генерации страницы при использовании стандартного sfFileCache несопоставимо больше. Но вопрос конфигурации системы давайте оставим в стороне.

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

При применении ORM в симфони по умолчанию не предусмотрено кеширование самих запросов. Это не потому, что разработчики глупые, а потому, что тут используется кеш другого уровня. Другая идея.

К минусам такого подхода стоит отнести порой очень запутанную логику удаления элементов из кеша. Допустим, пользователь изменяет данные о загруженном им видео, при этом придется очистить кеш для всех элементов с этим видео на страницах листинга (для всех вариантов), для детальной страницы, для элементов из rss-feed если такой имеется и тд. Обо всем этом приходится задумываться и в нужный момент помнить, поэтому в качестве рекомендации могу посоветовать сразу проектировать систему с учетом кеширования.

В нашем проекте админка физически находится в другом контейнере, что затрудняет управление кешем. Поэтому пришлось сделать простенький XMLRPC-интерфейс для этого.

Вынесение затратных операций в отдельные компоненты и есть основная идея html-кеширования. Не нужно изобретать велосипедов, все уже работает, причем очень просто и функционально. Если все парни из моей команды это поймут, может престану с ними ругаться: )
Теги:кешированиеоптимизацияsymfonycomponents
Хабы: Symfony
Рейтинг +12
Количество просмотров 2,8k Добавить в закладки 21
Комментарии
Комментарии 23

Похожие публикации

Symfony Middle developer
от 150 000 ₽RetailCRMМожно удаленно
Backend-разработчик (Symfony)
от 110 000 ₽CREATIVEТюменьМожно удаленно
PHP разработчик (Symfony)
от 120 000 до 220 000 ₽getUNIQМосква
PHP разработчик (Symfony, микросервисы)
от 120 000 до 200 000 ₽ДВИЖМоскваМожно удаленно
Senior PHP разработчик (Symfony/Phalcon/Lavarel)
от 2 500 до 3 500 €Digital LineМосква

Лучшие публикации за сутки