Pull to refresh

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

Reading time8 min
Views3.6K
За 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.    
  4.     public function executeListing (sfWebRequest $r) {
  5.        
  6.         $f = new AFormFilter();
  7.         $f->bind($r->getParameter('filter'));
  8.         if ($f->isValid()) {
  9.            
  10.             $c = $f->buildCriteria();
  11.             $this->array_of_a = APeer::doSelect($c);
  12.  
  13.             $this->array_of_b = BPeer::retrieveBySelectedA($this->array_of_a);
  14.             $this->array_of_c = CPeer::retrieveBySelectedA($this->array_of_a);
  15.         } else {
  16.            
  17.             $this->getUser()->setFlash('error', 'Error while retrieving data.');
  18.         }
  19.     }
  20. }


Что мы тут сделали: По фильтрам, полученным из запроса, вернули объект 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.  
  3. /* actions.class.php */
  4. class indexActions extends sfActions {
  5.    
  6.     public function executeListing (sfWebRequest $r) {
  7.        
  8.        
  9.         $this->filterParams = $r->getParameter('filter');
  10.     }
  11. }
  12. /* end of actions.class.php */
  13.  
  14. /* components.class.php */
  15. class IndexComponents extends sfComponents {
  16.  
  17.     public function executeListingBlock() {
  18.        
  19.         $f = new AFormFilter();
  20.         $f->bind($this->filterParams);
  21.        
  22.         if ($f->isValid()) {
  23.            
  24.             $c = $f->buildCriteria();
  25.             $this->array_of_a = APeer::doSelect($c);
  26.  
  27.             $this->array_of_b = BPeer::retrieveBySelectedA($this->array_of_a);
  28.             $this->array_of_c = CPeer::retrieveBySelectedA($this->array_of_a);
  29.         } else {
  30.            
  31.             $this->getUser()->setFlash('error', 'Error while retrieving data.');
  32.         }
  33.     }
  34. }
  35. /* 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.  
  6. $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.  
  4.     public function executeListingBlock() {
  5.        
  6.         $f = new AFormFilter();
  7.         $f->bind($this->filterParams);
  8.        
  9.         if ($f->isValid()) {
  10.            
  11.             $c = $f->buildCriteria();
  12.             $this->array_of_a = APeer::doSelect($c);
  13.  
  14.         } else {
  15.            
  16.             $this->getUser()->setFlash('error', 'Error while retrieving data.');
  17.         }
  18.     }
  19.    
  20.     public function executeOfferItem() {
  21.        
  22.         $this->b = $this->a->getBRelatedByB();
  23.         $this->c = $this->a->getCRelatedByC();
  24.     }
  25. }
  26. /* 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-кеширования. Не нужно изобретать велосипедов, все уже работает, причем очень просто и функционально. Если все парни из моей команды это поймут, может престану с ними ругаться: )
Tags:
Hubs:
+12
Comments23

Articles

Change theme settings