Ускоряем проект на Symfony через nginx+memcached
Symfony — популярный фреймворк, использующий язык PHP, для ускорения процесса разработки web-приложений. Благодаря гибкой и продуманной структуре классов Symfony, программировать под этот фреймворк становится одним удовольствием, это действительно очень удобно.
Однако, плату за универсальность никто не отменял. И, хотя Symfony предоставляет обширные возможности для кэширования, оно не всегда спасает, особенно на нагруженных проектах и особенно на страницах, которые состоят из часто меняющихся блоков.
В этой статье я постараюсь описать минусы кэширования в Symfony и приемы их обхода.
Представьте ситуацию, когда есть страница, на которой данные могут меняться только раз в 1-2 суток, тогда мы смело можем ее закэшировать на 24 часа. В Symfony это делается очень просто установкой параметров в файле cache.yml для модулей:
enabled: true
with_layout: true
lifetime: 86400
Это означает, что раз в 24 часа при заходе одного пользователя страница будет перестраиваться, т. е. выполнять запросы к базе данных, исполнять код PHP, еще какие-то действия, после этого результат будет записан в кэш и отдан клиенту. После этого все последующие 24 часа каждый пользователь будет получать эту страницу из кэша — никаких запросов к базе, минимальное исполнение только самого необходимого кода. Красота!
И вот в один прекрасный момент руководство решает добавить на эту страницу блок с последними комментариями пользователей. Все, счастливая жизнь закончилась, пришло время пораскинуть мозгами. Думаю, очень многие веб-разработчики сталкивались с подобной историей.
Проблема в том, что блок с последними комментариями может обновиться как через минуту, так и через 2-3 часа, поэтому кэшировать всю страницу мы не можем, т. к. не понятно какое время жизни выставлять для кэша. Вообще это типичный случай, когда следует применять блочное кэширование, т. е. делим страницу на блоки (или компоненты в Symfony), каждому блоку задаем свои параметры кэширования.
Но такой тип кэширования далек от идеала, т. к. в этом случае Symfony придется выполнить большую часть своего кода. Код исполняется, как обычно, просто перед выполнением каждого блока будет проверка на наличие блока в кэше и, если он там есть и время жизни не истекло, то вместо выполнения этого блока будет просто вставлен html-код из кэша.
При таком блочном кэшировании будет исполняться код Symfony при каждом http-запросе, кроме того, будет как минимум один запрос к базе данных, если пользователь авторизован – Symfony получает данные о пользователе из базы еще до подключения блоков.
Однако, есть другой способ применения кэширования для случая, описанного выше. Для этого нам понадобится (я также привожу описание установки нужного ПО, для тех, кому оно понадобится, все примеры я проверял у себя на Linux SUSE 11.3):
- Nginx в качестве web-сервера
- Фреймворк Symfony
- Memcached и расширение для PHP
Сейчас после открытия в браузере http://localhost мы видим страницу нашего проекта на Symfony.
Итак, у нас все есть для работы, начнем ускорять наш проект. За основу берется то, что Nginx умеет обращаться к memcached по ключу и отдавать значение, которое содержится по этому ключу, в качестве ответа web-сервера.
Memcached — это демон, который хранит данные в оперативной памяти, обращаться к этим данным мы можем по определенному ключу.
Воспользуемся всеми вышеописанными возможностями memcached и nginx. В качестве ключа в memcached мы будем использовать URL-адрес страниц нашего сайта, а в качестве содержимого HTML страницы. Теперь нам нужно настроить nginx так, чтобы он при наличии в memcached значения (html) по ключу (url страницы) отдавал его сразу же в ответе клиенту, в противном случае запрос должен передаваться к Symfony, который его обрабатывает, сохраняет в memcached и отдает ответ для клиента. Таким образом, если в данный момент страница не в кэше, то его добавит в кэш Symfony, и при следующем запросе к этой странице ответ будет получен еще на стороне nginx, не доходя даже до Symfony.
Итак, добавим в наш проект возможность записи ответа сервера в memcached. Для этого воспользуемся механизмом фильтров в Symfony. Создадим в директории lib нашего проекта поддиректорию cache, а в ней создадим файл sfHtmlCacheFilter.class.php. Содержимое этого файла будет следующее:
<?php class sfHtmlCacheFilter extends sfFilter { private static $replacement = array(); private static $multi_replacement = array(); private static $pageLimit = array(); private static $host = ''; /** * Метод вызывается после того, как страница полностью сформирована, * здесь содержимое страницы записывается в memcached, если она указана в конфиге html_cache.yml. * * @param <Object> $filterChain * @return <NULL> */ public function execute ($filterChain) { $filterChain->execute(); $response = $this->getContext()->getResponse(); $request = $this->getContext()->getRequest(); // Если в наших конфигах прописано не использовать кэширование или мы находимся в debug-режиме, то прекращем выполнение. // Здесь также проверяется код ответа - если он не равен 200, то прекращем выполнение. if ( (!sfConfig::get('sf_cache') || count($request->getGetParameters()) || count($request->getPostParameters())) || (sfConfig::get('sf_debug') || !sfConfig::get('sf_no_script_name') || $response->getStatusCode() != 200) ) { return; } $uri = $this->getContext()->getRouting()->getCurrentInternalUri(); self::$host = 'http://'.$request->getHost(); // Получаем запрошенный URI - будущий ключ для memcached $webUri = str_replace(self::$host, '', $request->getUri()); $uriCacheParams = null; // Получаем из конфига страницы, которые подлежат кэшированию $config = self::getConfig(); foreach($config as $item) { foreach($item as $key=>$uriParams) { $routing_uri = preg_replace('/^(.*)\?.*$/i', '$1', $uri); // Сравниваем запрошенный URI с записями в конфиге if(preg_match('#^'.$key.'$#', $webUri)) { $uriCacheParams = $uriParams; break; } } // Если запрошенная страница есть в конфиге, то прекращаем цикл if($uriCacheParams) { break; } } $webUri = self::_decodeUri($webUri); if ($uriCacheParams && $response->getStatusCode() == '200') { // Записываем содержимое ответа в кэш self::setUri(self::$host.$webUri, $response->getContent(), $uriCacheParams['lifetime']); } return; } /** * Получить конфиг тэгов * * @return <Array> */ private static function getConfig() { $conf = sfYaml::load(sfConfig::get('sf_config_dir') . '/html_cache.yml'); return $conf; } /** * Вызывается из методов save() моделей, * удаляет из кэша все страницы, зависящие от заданной модели * * @param <Object> $model - объект, для которого вызывается метод save в модели */ public static function deleteCache($model) { $request = sfContext::getInstance()->getRequest(); $model_name = get_class($model); $config = self::getConfig(); $arUri = array(); // Получаем страницы из конфига по этой модели $arCacheParam = $config[$model_name]; self::$host = 'http://'.$request->getHost(); // Обходим в цикле страницы, строим из них ключи для memcache foreach($arCacheParam as $uri=>$param) { self::$replacement = array(); self::$multi_replacement = array(); self::$pageLimit = array(); $pageLimit = null; $attr = (isset($param['attributes']) ? $param['attributes'] : null); settype($attr, 'array'); foreach($attr as $prop) { $method = 'get'.$prop; $value = false; /** * method_exists не работает для Doctrine, * но работает для Propel, is_callable всегда будет * возвращать true для Doctrine, поэтому существование * метода проверяется таким образом. */ try { $value = $model->$method(); } catch (Exception $e) {} if($value !== false) { if (is_string($value) && strpos($value, ' ') !== false) { $value = self::_decodeUri($value, false); } self::$replacement[] = $value; } elseif(preg_match('/^pageLimit:(\d+)$/', $prop, $matches)) { $pageLimit = $matches[1]; self::$replacement['pageLimit'] = range(1, $pageLimit); } } $isCachable = true; $beforeDeleteCache = (isset($param['beforeDeleteCache']) ? $param['beforeDeleteCache'] : null); settype($beforeDeleteCache, 'array'); foreach($beforeDeleteCache as $callback) { $method = $callback; $isCachable = $model->$method(); } if($isCachable) { $uri = preg_replace_callback('/\(.*\)/U', array('self', 'replace_attr_callback'), $uri); if(self::$multi_replacement) { self::replace_multi_attr(self::$multi_replacement, $uri, $arUri); } else { $arUri[] = self::$host.$uri; } } } // Удаляем массив страниц из memcached if($arUri) { self::deleteUri($arUri); } } /** * Используется для парсинга конфига при удалении страницы из memcached * * @param <Array> $matches * @return <String> */ private static function replace_attr_callback($matches) { $expr = $matches[0]; $arKeys = array_keys(self::$replacement); $key = $arKeys[0]; if(!is_array(self::$replacement[$key]) && $key !== 'pageLimit' && isset(self::$replacement[$key]) && preg_match('/'.$expr.'/', self::$replacement[$key])) { $expr = self::$replacement[$key]; } else { self::$multi_replacement[] = self::$replacement[$key]; $expr = '%MULTI_REPLACEMENT_'.(count(self::$multi_replacement) - 1).'%'; } unset(self::$replacement[$key]); return $expr; } /** * Используется для парсинга конфига при удалении страницы из memcached * * @param <Array> $attr * @param <String> $uri * @param <Array> $arUri * @param <Integer> $key */ private static function replace_multi_attr($attr, $uri, &$arUri, $key = 0) { if(isset($attr[$key])) { foreach($attr[$key] as $ind=>$i) { self::replace_multi_attr($attr, str_replace('%MULTI_REPLACEMENT_'.$key.'%', $i, $uri), $arUri, $key+1); } } if($key == count($attr)) { $arUri[] = self::$host.$uri; } } /** * Возвращает объект для работы с memcached. * * @return <Object> */ private static function getMemcache() { $memcache = new Memcache(); $con = $memcache->connect('127.0.0.1', 11211); return $memcache; } /** * Записываем содержимое страницы в memcached * * @param <String> $uri - ключ в memcached * @param <String> $data - значение в memcached * @param <Integer> $lifetime - время жизни кэша */ private static function setUri($uri, $data, $lifetime) { $memcache = self::getMemcache(); $memcache->set($uri, $data, 0, $lifetime); } /** * Удаляет страницу из memcached * * @param <Array> $arUri - массив ключей memcached, * которые следует удалить */ private static function deleteUri($arUri) { settype($arUri, 'array'); $memcache = self::getMemcache(); foreach($arUri as $uri) { $memcache->delete($uri); } } /** * Метод декодирует URI через rawurldecode, * но символ пробела оставляет в прежнем виде, * т.к. nginx его не декодирует в своей переменной $uri * * @param <string> $uri * @param <boolean> $decode * @return <string> */ private static function _decodeUri($uri, $decode = true) { $url = ($decode ? rawurldecode($uri) : $encodedUri); return str_replace(' ', '%20', $url); } }
Пока нас будет интересовать только первый метод класса execute. Он вызывается после того, как закончено формирование страницы, в этом методе у нас есть возможность обратиться к содержимому страницы, еще до того, как она отправлена клиенту.
Все, что делается в методе execute — это получение текущего адреса страницы, проверка его в конфиге html_cache.yml и, если он там есть, записываем содержимое в memcached.
Теперь создадим сам конфиг html_cache.yml и положим его в папку config нашего проекта:
Index: /: { lifetime: 86400 }
Здесь мы задаем адрес запрошенной страницы («/» – запрос главной страницы сайта), в фигурных скобках перечислены параметры кэширования, мы указали время жизни кэша 86400 секунд, т. е. сутки. Параметр Index сейчас используется только лишь в качестве группировки адресов страниц, но здесь можно также указывать названия моделей Symfony, об этом будет рассказано ниже.
По части Symfony у нас почти все готово, осталось включить использование кэширования и активировать наш фильтр.
Чтобы включить кэширование для секции prod прописываем в файле apps/frontend/config/settings.yml (изменения выделены жирным шрифтом):
1 2 3 4 5 |
prod: .settings: no_script_name: true cache: true logging_enabled: true |
А теперь активируем фильтр, добавляем в файл apps/frontend/config/filters.yml пару строчек:
1 2 3 4 5 6 7 8 9 10 |
rendering: ~ security: ~ # insert your own filters here htmlcache: class: sfHtmlCacheFilter cache: ~ execution: ~ |
Тем самым мы активировали наш класс-фильтр.
Теперь сбрасываем кэш Symfony:
# php symfony cache:clear
И заходим на страницу http://localhost/ . Если все правильно сделали главная страница нашего сайта должна оказаться в memcached, проверяем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# telnet localhost 11211 Trying ::1... telnet: connect to address ::1: Connection refused Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. get http://localhost/ VALUE http://localhost/ 0 2026 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> <link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/sf/sf_default/css/screen.css" /> <link rel="shortcut icon" href="/favicon.ico" /> <!--[if lt IE 7.]> <link rel="stylesheet" type="text/css" media="screen" href="/sf/sf_default/css/ie.css" /> <![endif]--> </head> <body> <div class="sfTContainer"> <a href="http://www.symfony-project.org/"><img alt="symfony PHP Framework" class="sfTLogo" src="/sf/sf_default/images/sfTLogo.png" height="39" width="186" /></a> <div class="sfTMessageContainer sfTMessage"> <img alt="ok" class="sfTMessageIcon" src="/sf/sf_default/images/icons/ok48.png" height="48" width="48" /> <div class="sfTMessageWrap"> <h1>Symfony Project Created</h1> <h5>Congratulations! You have successfully created your symfony project.</h5> </div> </div> <dl class="sfTMessageInfo"> <dt>Project setup successful</dt> <dd>This project uses the symfony libraries. If you see no image in this page, you may need to configure your web server so that it gains access to the <code>symfony_data/web/sf/</code> directory.</dd> <dt>This is a temporary page</dt> <dd>This page is part of the symfony <code>default</code> module. It will disappear as soon as you define a <code>homepage</code> route in your <code>routing.yml</code>.</dd> <dt>What's next</dt> <dd> <ul class="sfTIconList"> <li class="sfTDatabaseMessage">Create your data model</li> <li class="sfTColorMessage">Customize the layout of the generated templates</li> <li class="sfTLinkMessage"><a href="http://www.symfony-project.org/doc">Learn more from the online documentation</a></li> </ul> </dd> </dl> </div> </body> </html> END quit Connection closed by foreign host. |
Отлично! Теперь наш проект может перед отдачей ответа клиенту записывать в memcached HTML страниц.
Ну а теперь осталось настроить nginx, чтобы он мог сам отвечать клиенту, если запрошенная страница присутствует в memcached.
Для этого нам надо добавить еще один upstream для memcached в конфиг nginx и задать правила для проксирования в него запросов. Вот каким должен получиться конфиг для Nginx (изменения выделены жирным шрифтом):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log logs/access.log main; error_log logs/error.log info; sendfile on; keepalive_timeout 65; upstream memcached_backend { server 127.0.0.1:11211; } upstream fcgi_www { server 127.0.0.1:9000; } server { listen 80; server_name localhost; error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } root /srv/www/symfony/web; #%ПУТЬ_ДО_ВАШЕГО_ПРОЕКТА%/web location ~ "^(/css/|/images/|/js/|/uploads/)" { expires 3d; } location /sf/ { root /srv/www/symfony/lib/vendor/symfony/data/web; #%ПУТЬ_ДО_ВАШЕГО_ПРОЕКТА%/lib/vendor/symfony/data/web } location / { fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param HTTP_ACCEPT_ENCODING gzip,deflate; fastcgi_param QUERY_STRING $query_string; fastcgi_param REDIRECT_STATUS 200; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param SCRIPT_FILENAME /srv/www/symfony/web/index.php; #%ПУТЬ_ДО_ВАШЕГО_ПРОЕКТА%/web/index.php fastcgi_param SCRIPT_NAME /index.php; fastcgi_param PATH_INFO $request_uri; # Если это POST-запрос, то передаем обработку сразу в Symfony if ($request_method = POST) { fastcgi_pass fcgi_www; break; } # Если на предыдущем шаге мы не вышли из location'а, то это GET-запрос. # Устанавливаем ключ для memcached, по которому будем искать значение set $memcached_key "http://$host$uri"; # Задаем upstream для memcached memcached_pass memcached_backend; # Устанавливаем тип содержимого, полученного от memcached, у нас хранится HTML default_type text/html; # Указываем nginx'у переходить в именованный location @symfony в случае отсутствия ключа # в memcached или в случае возникновении ошибки при запросе к memcached proxy_intercept_errors on; error_page 404 502 = @symfony; } # Здесь описан именованный location, куда nginx переходит в случае отсутствия ключа или # возникновении ошибки при обращении к memcached. Здесь просто перечислены параметры # fastcgi так же, как было сделано выше. location @symfony { fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param HTTP_ACCEPT_ENCODING gzip,deflate; fastcgi_param QUERY_STRING $query_string; fastcgi_param REDIRECT_STATUS 200; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param SCRIPT_FILENAME /srv/www/symfony/web/index.php; #%ПУТЬ_ДО_ВАШЕГО_ПРОЕКТА%/web/index.php fastcgi_param SCRIPT_NAME /index.php; fastcgi_param PATH_INFO $request_uri; fastcgi_pass fcgi_www; } } } |
Теперь проверяем нет ли ошибок в конфигах Nginx после внесения изменений:
# /etc/init.d/nginx configtest the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok configuration file /usr/local/nginx/conf/nginx.conf test is successful
И говорим Nginx, чтобы он перечитал конфиг:
# /etc/init.d/nginx reload
Reloading nginx: reloaded
Проверяем! Заходим на страницу http://localhost/ , она у нас уже должна лежать в memcached. Лично я в своем браузере Mozilla Firefox даже на примере такой простой странички вижу, что она стала загружаться гораздо быстрее!
Но не будем доверять всему тому, что видим. Проведем небольшой бенчмарк, в этом нам поможет утилита ab2 — инструмент от разработчиков web-сервера Apache.
Сначала закомментируем наши настройки в Nginx, чтобы он перенаправлял все на Symfony.
Вносим следующие изменения в наш конфиг для Nginx, начиная с 71-й строки:
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
fastcgi_param SCRIPT_NAME /index.php; fastcgi_param PATH_INFO $request_uri; fastcgi_pass fcgi_www; # if ($request_method = POST) { # fastcgi_pass fcgi_www; # break; # } # set $memcached_key "http://$host$uri"; # memcached_pass memcached_backend; # default_type text/html; # proxy_intercept_errors on; # error_page 404 502 = @symfony; } location @symfony { |
Затем стандартные команды:
# /etc/init.d/nginx configtest the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok configuration file /usr/local/nginx/conf/nginx.conf test is successful # /etc/init.d/nginx reload Reloading nginx: reloaded
И запускаем утилиту от Apache, отправим на наш сайт 1000 запросов, причем 10 будем выполнять параллельно:
# ab2 -n 1000 -c 10 http://127.0.0.1:80/ This is ApacheBench, Version 2.3 <$Revision: 655654 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: nginx/0.8.54 Server Hostname: 127.0.0.1 Server Port: 80 Document Path: / Document Length: 2026 bytes Concurrency Level: 10 Time taken for tests: 26.412 seconds Complete requests: 1000 Failed requests: 646 (Connect: 0, Receive: 0, Length: 646, Exceptions: 0) Write errors: 0 Non-2xx responses: 646 Total transferred: 1142110 bytes HTML transferred: 964622 bytes Requests per second: 37.86 [#/sec] (mean) Time per request: 264.116 [ms] (mean) Time per request: 26.412 [ms] (mean, across all concurrent requests) Transfer rate: 42.23 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.1 0 1 Processing: 0 264 349.9 3 827 Waiting: 0 264 349.9 3 810 Total: 1 264 349.9 3 827 Percentage of the requests served within a certain time (ms) 50% 3 66% 703 75% 713 80% 716 90% 748 95% 788 98% 790 99% 796 100% 827 (longest request)
У меня после таких тестов отваливался через некоторое время fastcgi. Во время выполнения ab2 попробуйте проверить загрузку страницы в браузере — можно наблюдать постепенное нарастание тормозов при загрузке страницы.
Итак, если вас настигла беда с fastcgi, как и у меня, то запускаем его снова:
php-cgi -a -b 127.0.0.1:9000 -c /etc/php5/fastcgi/php.ini
Далее убираем наши комментарии, сделанные перед запуском теста, релоадим nginx и запускаем ab2 с теми же параметрами:
# /etc/init.d/nginx configtest the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok configuration file /usr/local/nginx/conf/nginx.conf test is successful # /etc/init.d/nginx reload Reloading nginx: reloaded # ab2 -n 1000 -c 10 http://127.0.0.1:80/ This is ApacheBench, Version 2.3 <$Revision: 655654 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: nginx/0.8.54 Server Hostname: 127.0.0.1 Server Port: 80 Document Path: / Document Length: 2026 bytes Concurrency Level: 10 Time taken for tests: 0.337 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 2170000 bytes HTML transferred: 2026000 bytes Requests per second: 2971.65 [#/sec] (mean) Time per request: 3.365 [ms] (mean) Time per request: 0.337 [ms] (mean, across all concurrent requests) Transfer rate: 6297.35 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.4 0 3 Processing: 1 3 0.6 3 5 Waiting: 0 3 0.6 3 4 Total: 1 3 0.5 3 6 Percentage of the requests served within a certain time (ms) 50% 3 66% 3 75% 4 80% 4 90% 4 95% 4 98% 4 99% 4 100% 6 (longest request)
Тут даже говорить нечего, все сказано в первом и втором выводах ab2. Достаточно взглянуть хотя бы на параметр «Time taken for tests», который показывает общее время проведения тестов. В первом случае он равен 26.412 секунд, во втором - 0.337 секунд. Прирост производительности почти в 80 раз!
Конечно, тестирование на локальном компьютере — это не есть тестирование в условиях, приближенных к реальным, однако какое-то представление оно все же дает.
А теперь вернемся к примеру, который был описан в самом начале статьи. Есть страница с постоянным контентом и есть блок с последними комментариями.
Как кэширование, которое мы только что внедрили в Symfony, поможет нам с кэшированием такой страницы? Пока никак. Наш кэш живет только какое-то определенное время, указанное в параметре lifetime, поэтому блок с комментариями будет обновляться только, когда истечет время жизни кэша. Но нас это не устраивает, нужно, чтобы страница обновлялась каждый раз после того, как добавился комментарий. Хорошо бы после добавления комментария удалять из кэша страницу.
Зададимся вопросом: «В какой момент у нас обычно должен обновляться кэш?». Обычно в момент, когда что-то меняется в базе данных. При чем, если изменения производятся в таблице, где хранятся комментарии, то обновляться должны страницы, где выводятся блоки, связанные с комментариями.
Для начала подключим к нашему проекту на Symfony базу данных. Не имеет значения какую базу данных использовать. Если вы знакомы с Symfony, то без труда сможете подключить свою базу данных. В своих же примерах я использую MySQL, пользователь root с паролем secretpass, имя базы данных — symfony.
# mysqladmin -uroot -psecretpass create symfony # php symfony configure:database "mysql:host=localhost;dbname=symfony" root secretpass
Теперь опишем нашу будущую базу данных в Symfony, для этого отредактируем файл config/doctrine/schema.yml:
Comment: columns: text: { type: string(255), notnull: true }
Далее воспользуемся готовыми инструментами Symfony и сгенерируем код для моделей, SQL для базы данных.
# php symfony doctrine:build --model # php symfony doctrine:build --sql # php symfony doctrine:insert-sql
Последняя команда выполняет SQL, чтобы создать необходимые таблицы в базе данных. Вместо этих трех команд можно было бы выполнить php symfony doctrine:build —all, которая выполняет те же 3 действия, но за один раз.
Теперь сгенерируем стандартную функциональность для добавления, удаления, редактирования и просмотра списка объектов модели:
# php symfony doctrine:generate-module --with-show --non-verbose-templates frontend comment Comment
Здесь мы говорим сгенерировать код для нового модуля comment, код генерируется для существующей модели Comment.
Далее необходимо переопределить метод save() для моделей, участвующих в кэшировании. У нас это одна модель Comment, поэтому открываем файл lib/model/doctrine/Comment.class.php и добавляем в класс Comment метод save():
class Comment extends BaseComment { public function save(Doctrine_Connection $conn = null) { parent::save($conn); sfHtmlCacheFilter::deleteCache($this); } }
Мы добавили метод save(), который переопределяет метод save() класса Doctrine_Record. В нашем методе мы вызываем метод save() родительского класса, которым и окажется Doctrine_Record::save().
Далее вызывается метод нашего класса-фильтра sfHtmlCacheFilter::deleteCache($this), которому в качестве параметра передается сохраняемый объект. Таким образом, этот новый метод не делает ничего нового, кроме как добавляется метод для очистки кэша при каждом сохранении объекта.
Удаляем кэш Symfony, чтобы наши изменения вступили в силу.
# php symfony cache:clear
Все, теперь зайдем по адресу http://localhost/comment и увидим сгенерированную Symfony страницу. Нажимаем на ссылку «New», добавляем первую запись, возвращаемся назад к списку и видим только что добавленный комментарий.
Выше уже упоминалось, что в конфиге html_cache.yml можно прописывать зависимости страниц от моделей. Пришло время рассказать об этом подробнее.
Настройки в файле html_cache.yml используется следующим образом.
Перед отдачей любой страницы клиенту проверяется наличие адреса этой страницы в конфиге. Если он есть, то происходит запись содержимого страницы в кэш.
При сохранении модели вызывается метод класса-фильтра для удаления из кэша страниц, зависящих от текущей модели. Модели перечислены, как элементы первого уровня, на втором уровне перечислены зависящие от этих моделей адреса страниц.
Сейчас у нас в файле html_cache.yml на первом уровне прописана несуществующая модель Index, на втором уровне прописана главная страница. Т. к. модели Index у нас нет, то эта настройка просто говорит о том, что неоходимо записывать в кэш содержимое главной страницы, но никакого удаления ее из кэша не происходит.
Для такой главной страницы это и не нужно, т. к. там выводится всегда одна и та же информация. А вот со страницей списка комментариев ситуация иная — если для нее мы применяем кэширование, то необходимо удалять кэш этой страницы при каждом добавлении комментария.
Добавим такую настройку. Страница со списком комментариев располагается по адресу http://localhost/comment. Добавляем в файл config/html_cache.yml следующую запись:
Comment: /comment: { lifetime: 86400 }
Теперь наша страница http://localhost/comment будет отдаваться клиенту еще на стороне nginx при наличии ее в кэше. В противном случае nginx передаст управление Symfony, который сформирует и запишет содержимое страницы в кэш.
Таким образом, мы организовали зависимость страниц от изменяющихся моделей — при изменении определенных моделей очищается кэш зависящих от этих моделей страниц.
В файле html_cache.yml можно использовать регулярные выражения, а также подставлять в адреса страниц результаты выполнения методов объектов. Применим кэширование к детальным страницам комментариев, которые располагаются по адресам вида - http://localhost/comment/show/id/1 .
Добавим к модели Comment следующую зависимость:
1 2 3 4 |
Comment: /comment: { lifetime: 86400 } /comment/show/id/(\d+): { lifetime: 86400, attributes: [Id] } |
Здесь мы указали в адресе регулярное выражение, которое пропускает только числовые значения ID комментариев. Мы также указали новый параметр attributes, который является массивом. В этом массиве объявляются методы модели, возвращаемые значения которых будут подставлены по порядку вместо регулярных выражений, заключенных в скобки. Причем методы могут возвращать не только обычные значения, но и массивы, в этом случае для удаления из кэша будет сформирован массив адресов страниц.
В данном примере вместо регулярного выражения (\d+) будет подставлен результат вызова метода getId() для объекта модели Comment. Это справедливо только при сохранении объекта и удалении кэша для него. При добавлении страницы в кэш будет произведено лишь сравнение текущего адреса страницы с регулярным выражением /comment/show/id/(\d+) и, если адрес пройдет проверку, то страница добавится в кэш.
После произведенных изменений в файле html_cache.yml страницы вида http://localhost/comment/show/id/1 постигнет участь предыдущих рассматриваемых страниц — они будут писаться в кэш.
Помимо параметров lifetime и attributes можно использовать параметр beforeDeleteCache, в который передается название метода модели (полное название). Этот метод должен возвращать true или false. При получении false кэш для записи с этим параметром удаляться не будет.
На этом все. Теперь наш проект на Symfony стал не просто быстрым, а мегабыстрым. Ответы клиент получает не от php-скриптов на сервере и даже не с диска, а из памяти сервера.
Спасибо за внимание! Чтобы ваши сайты всегда летали!
UPD: Спасибо Sergei за найденную ошибку. Все описанное работало только с латинскими буквами в url'е, но не работало с русскими. Внесены нужные изменения в класс-фильтр для Symfony, а также в конфиге nginx в качестве ключа для memcached теперь используется переменная $uri, а не $request_uri
На эту статью оставлено 127 комментариев
23 Март 2011
Статья отличная)
Но как быть если на странице есть модули, содержимое которых зависит от того, какой пользователь авторизованный?
23 Март 2011
Спасибо за вашу оценку )
Тоже можно реализовать с применением всего описанного в статье, но плюс к этому nginx+ssi. nginx можно заставить проверять авторизации. Если коротко: кэшируем в памяти данную область так, как описано в статье, но места, зависящие от авторизованности пользователя прописываем через ssi-выражение, в котором ставим условие что-то типа:
<!–# if expr=”$user_id != 0″ –>
Авторизован
<!–# else –>
НЕ авторизован
<!–# endif –>
Переменную $user_id можно получать прямо из nginx с использованием перлового модуля – все зависит от проекта, как храняться сессии и каким образом они используются.
26 Март 2011
Если урл на русском языке, то ключ для memcache не получится
26 Март 2011
Ключ в мемкеш записывается в кодированном виде, однако nginx перед обращением к мемкешу почему-то заменяет все символы процентов на %25, поэтому запрос, содержащий в url’е русские буквы, отправлялся всегда к бэкенду (Symfony).
Вы правы, все описанное работало только с латинскими буквами в url’е.
Я внес необходимые изменения в код класса-фильтра, а также вместо использования переменной $request_uri в конфиге nginx теперь используется переменная $uri, в которой хранится декодированная строка.
Спасибо за найденную ошибку!
28 Март 2011
Спасибо)
11 Апр 2011
Это просто офигенно.
26 Апр 2011
У меня почему-то в memcache нормально сохраняет страницы до 20000 символов, если больше то выводится абракадабра. Можно это исправить в настройках, или это ограничение memcached ?
26 Апр 2011
В memcached присутствует ограничение в 1Mb. Обычно страница весит гораздо меньше. Страницы размером больше 1Mb не так уж и часто встретишь.
Если нужно хранить именно такие страницы, то лучше посмотреть в сторону других key-value-хранилищ.
Все-таки, думаю, что проблема не в ограничении memcached, т.к. 20000 символов далеко не 1Mb.
По поводу абракадабры. Проверьте настройки клиента memcached в php выводом, например, phpinfo(). Параметр memcache.compress_threshold должен быть выключен (равен 0).
Попробуйте еще вывести в какой-нибудь лог результат $response->getContent() в 65-й строке перед записью в memcached. Может быть все-таки данные некорректны еще перед самой записью в хранилище?
26 Апр 2011
Установка memcache.compress_threshold в 0 помогла, там было значение 20000. Спасибо за помощь и за прекрасную статью!
26 Апр 2011
Спасибо, рад, что проблема решилась.
04 Окт 2012
Из-за множество тамбуляций в html коде memcache записывает такую байду
xњн=isWrџЕ_с мБu іЖе»пwё№‘Ї+zmcЩёw’к|9jЪ J¬В4qUн$ф…д*w7г®І¦€§’ђg$XP:UМПз‹ФcBB;*њ|M1ЌОOM®ѕЛ›SіщЩ|aЄФV№ЃЛњЯBфюЦ“uМfХТlО”їE–sєaБ-[W6”Ч•јЩ‘sЧЎ(ЄZmѓ¶вUЫАg’ѓbтҐZ7МN+ z‘ЏБ‚Bљ**ЯnґSа6ЌMЏ— Фc”аЮВ—Ч cUгiДFи)MWщЅ¤¬jЅ©XХЋЕНРєТґxјЂfUЌ6Ч«-CUљўщъ@РХ¶bYUА«1D Ш…D
04 Окт 2012
Тут дело в другом. См. комментарий http://job-interview.ru/articles/post/300/#372 , начиная с 3-го абзаца
04 Окт 2012
большое спасибо
04 Окт 2012
А как быть с авторизацией ? Проверять через nginx есть ли авторизация ? К примеру шапка меняться в зависимости авторизирован пользователь или нет
04 Окт 2012
Да, как вариант, через nginx при помощи perl-модуля, из которого можно проверять и файл сессии, и в мемкеш обратиться, но это уже зависит от вашего проекта.
04 Окт 2012
А что по поводе SSI ? не как не получается его использовать. Не хочет работать ((
Он будет работать только на страницах .shtml ?
04 Окт 2012
Нет, он работает на страницах html и на всех остальных, которые вы пропишете в конфиге нгинкс.
Например, вы можете в нгинкс установить переменную $user_id, как описано в документации – http://nginx.org/ru/docs/http/ngx_http_perl_module.html#perl_set.
Перловая функция выполниться, когда вы в ssi выражении обратитесь к этой переменной $user_id. Например, вот так:
НЕ авторизован
Ваша функция на перле может обращаться к мемкешу или читать файл сессии, доставать оттуда данные и возвращать, например, id пользователя, если он хранится у вас в сессии.
04 Окт 2012
ВСе это супер, но сам ssi не работает в php, нельзя вставить его как часть html кода и он заработает
05 Окт 2012
Что выдумаете по этому поводу.
Большое спасибо. Да я как раз про извращенность сейчас и соображаю. После того как я страницу начал отдавать с помощью nginx + memcached скорость увеличилась в десятки раз, но из кеша же ) И вопрос стал о динамиеских данных.
Я подумал что данные пользователя не нужно учитывать в СЕО и всем пофигу кроме самого польpователя, то эти данные грузить через ajax. И сами post запросы не кешировать. Как такая идея? Получается что вся страница будет закеширована а через js данные будут грузиться актуальные и более того ajax можно индексировать для поисковика.
05 Окт 2012
Php тут совсем не при чем. Ssi обрабатывает web-сервер, в данном случае nginx. Php может отдать ssi так же, как и обычный html, тут никаких проблем быть не может.
Я вам советую все-таки дальше пробовать, где-то вы допускаете ошибку. Попробуйте на самом простейшем примере. Например, создайте простую хтмл-страницу на сервере с вставкой ssi, добейтесь, чтобы вставка заработала. Кстати, вы случайно не забыли в конфиге nginx поставить ssi=on?
05 Окт 2012
Можно и так, конечно. Дело вкуса. Но мне, например, этот вариант как-то не по душе.
Во-первых, визуально этот блок будет подгружаться не сразу, а, скорее всего, после загрузки всей страницы.
Во-вторых, это лишний запрос на сервер.
В-третьих, я не уверен, что поисковики его учтут, хотя в сео я не силен.
05 Окт 2012
Да все заработало. Только теперь придеться все ссылки гнать через nginx а не через роутер symfony
08 Окт 2012
Чтоб SSI заработал в symfony нужно поменять в setting.yml
compressed: false
и будет счастье
08 Окт 2012
Конфиг не совсем правильный. Нужно так
set $memcached_key “http://$host$request_uri”;
01 Май 2018
Ꮤith ever riѕing cokst of phоto printerѕ and with the increasіng demand foor consumer digiсams and DSLR cameras, an inexpensive solugion f᧐r photo printing would have to be adⅾressed.
If you want to measurе the speed in dpi (dots per іnch) formаt, then the speeɗ is аround 2400 x 600.
Most remanufactureгs will saᴠe you betweеn 25% and 50%
off new OEM priceѕ.
Ⴝtop by my homepagе Cartridge on Wheels
01 Май 2018
I likе meeting utile infοгmation,this pⲟst has got me evеn more info!
Look ingo my webpage :: Dehumidifier South bay area
01 Май 2018
Ꮋi, Steve here at Bridges.
Fantastic “ Ускоряем проект на
Symfony через nginx+memcached / Статьи / Работа для программистов” іs really an origіnal subject
I just wanted to say thjat I liked reading your post.
Feel free to vіsit my һomepage sparks bathroom cabinet styles
01 Май 2018
Wіth kniting as a general favored hobbhy to engage in іn numerous international lⲟcations arounbd
the world, there iѕ a huɡe number of retailers all offering various thicknesses of knitting yarn, and also a
range of knitting extraѕ tbat are said to Ьe more or less required
by variouѕ projects.
Among the initial accessories that will become invaluable for you aѕ a knitter is the knitting bag.
This type of accеѕsory iss not gߋiong to оnlʏ givе ʏou the ideal place
to keep your unfinished projects, үour yѕrn as well
aas y᧐ur knnitting needles, but iit wіll alsߋ let you travel your supplies
al᧐ng ᴡithout worrying tһat thеy’rе going to end up comрromised.
Here is my web site :: Hassan
02 Май 2018
Ηowdy! Quick question that’s entіrelʏ off topic.
Ɗo you know how to make your site mobiⅼe friendly? My web site looks weird when vіewing from my iphone4.
I’m tring to find a theme or plugin that might
be able to resoⅼve this ⲣгoblem. If you һave any ѕuggestions,
plеase ѕһare. Appreciate it!
Here is my site … general contractors reno nv
02 Май 2018
I have to thank you for the efforts you have put in writіng tһіs blog.
I aɑm hoping to check out the same high-grade blog posts from you later on as well.
In fact, your creatіve writing abilities has encouraged
me to get my very own site now
Review my blog; wall air conditioners
02 Май 2018
The pacқagtes ofver dіfferent levels of service, all custom deѕiogned to casst an effect oon tthе ᴡeЬsite in their own way.
Along with search engine optіmization, the compаny
servess other Internet marketing solutions such as Payy
Perr Click Μanagement, Socіal Media Optimization, link building services,
webste design, multimedia, web development andd servicеs software deνelopment,
to name a few. Ꭲhe ѕecond tupe consiѕts of an offshore SEO firmѕ
that operate in countries such aas Indiɑ, Phiⅼippines, Vietnam and China.
Ꮇy web bⅼog – Search Engine Optimization Pricing
08 Май 2018
Wһat’s up collеagues, its wߋnderful pаragrɑph
cߋncerning teachingand fully defined, keep
it up all the time.
Visit my website – heater repair san jose
09 Май 2018
Excellent website. Lots of usefuⅼ information here.
Ι am sending it to a few pals ans aⅼѕo sharig in deliciοus.
Αnd naturally, thanks on your sweat!
My web sitе: 50 gallon water heater electric lowes
11 Май 2018
Lօwering prices in exchange fοrr а larger ρortion of target markеts seems
like a wise maneuver. Βuild a shott ⅼist of wһoever suits your
original dеsign best, perhaps basing it on prices or quality of work, and contact a few
candidates from yߋr list. The the maіn thing “fact” is that we do NOᎢ managemjent the google or the results viewed to ⲣeople, nor dpes ANYSEO businesѕ.
Also viѕit myу wеb-site – Internet marketing Reno
22 Сен 2018
Hi i am kavin, its my first time to commenting anyplace,
when i read this paragraph i thought i could also make comment due to this sensible piece of writing.
My web site: buy 3d models
04 Окт 2018
Its like you read my thoughts! You appear to understand a lot approximately this,
such as you wrote the ebook in it or something.
I think that you simply can do with some % to
power the message house a little bit, however other than that, this is excellent blog.
A great read. I’ll certainly be back.
05 Окт 2018
Hmm is anyone else having problems with the images on this
blog loading? I’m trying to determine if its a problem on my end or if it’s the blog.
Any feed-back would be greatly appreciated.
06 Окт 2018
Having read this I thought it was very enlightening. I appreciate you taking the time and energy to
put this content together. I once again find myself personally spending
a lot of time both reading and commenting. But so what, it
was still worthwhile!
07 Окт 2018
Nice blog here! Also your website loads up very fast! What host
are you using? Can I get your affiliate link to your host?
I wish my web site loaded up as quickly as yours lol
07 Окт 2018
Hmm it appears like your site ate my first comment (it
was extremely long) so I guess I’ll just sum it up what I had written and
say, I’m thoroughly enjoying your blog. I as
well am an aspiring blog writer but I’m still new to everything.
Do you have any tips for novice blog writers? I’d really appreciate it.
08 Окт 2018
I need to to thank you for this wonderful read!!
I definitely loved every bit of it. I have you bookmarked to check out new things you post…
08 Окт 2018
Sweet blog! I found it while browsing on Yahoo News. Do you have
any suggestions on how to get listed in Yahoo News?
I’ve been trying for a while but I never seem to get there!
Cheers
10 Окт 2018
Have you ever considered about including a little bit more
than just your articles? I mean, what you say is valuable and all.
However think of if you added some great visuals or videos to give your posts more, “pop”!
Your content is excellent but with pics and video clips, this blog could
definitely be one of the best in its field.
Wonderful blog!
11 Окт 2018
Hey I know this is off topic but I was wondering if
you knew of any widgets I could add to my blog that automatically tweet my newest twitter updates.
I’ve been looking for a plug-in like this for quite
some time and was hoping maybe you would have some experience with
something like this. Please let me know if
you run into anything. I truly enjoy reading your blog and I look forward to your
new updates.
17 Окт 2018
What’s up colleagues, its enormous piece of writing about teachingand
fully defined, keep it up all the time.
17 Окт 2018
I needed to thank you for this good read!! I definitely enjoyed every bit of it.
I have you book marked to check out new things you post…
18 Окт 2018
If you want to obtain a great deal from this piece of writing then you
have to apply these methods to your won blog.
19 Окт 2018
Very energetic blog, I liked that bit. Will there be
a part 2?
19 Окт 2018
Nice post. I was checking constantly this blog and I am impressed!
Very helpful information specifically the last part I care
for such information much. I was looking for this certain information for a very long time.
Thank you and best of luck.
23 Окт 2018
It’s awesome in favor of me to have a website, which is helpful for my know-how.
thanks admin
25 Окт 2018
Hi mates, how is everything, and what you would like to say about this paragraph, in my
view its truly amazing for me.
29 Окт 2018
I visited several web pages but the audio quality for audio songs existing at this website is actually wonderful.
30 Окт 2018
It is appropriate time to make some plans
for the future and it is time to be happy. I’ve read this post and if I could I want to suggest you few interesting things
or suggestions. Perhaps you could write next articles referring to this article.
I want to read more things about it!
03 Ноя 2018
Quality content is the crucial to invite the users to visit the web site, that’s what this site is
providing.
03 Ноя 2018
Why users still make use of to read news papers when in this technological
globe everything is presented on web?
04 Ноя 2018
I have read so many posts about the blogger lovers except this post
is really a fastidious article, keep it up.
07 Ноя 2018
Thank you for another great article. Where else could anybody get that kind of information in such an ideal method of
writing? I have a presentation next week, and I’m on the look for such info.
09 Ноя 2018
That is very attention-grabbing, You are an overly professional
blogger. I have joined your feed and look ahead to in quest of more of your great post.
Additionally, I’ve shared your site in my social networks
10 Ноя 2018
Hi, just wanted to tell you, I liked this article. It was funny.
Keep on posting!
Feel free to surf to my web page … 3dcgstore 3d models
11 Ноя 2018
Can I simply say what a comfort to uncover an individual who truly knows what they’re discussing on the net.
You certainly realize how to bring a problem to light and make it important.
A lot more people really need to read this and understand this
side of your story. I was surprised that you aren’t more popular since you most certainly have the gift.
12 Ноя 2018
Right away I am going to do my breakfast, later than having my breakfast coming over again to read additional news.
13 Ноя 2018
Thank you for sharing your thoughts. I truly
appreciate your efforts and I will be waiting for your next post thank you once again.
14 Ноя 2018
I’ve been exploring for a little bit for any high quality articles or weblog posts on this kind of space .
Exploring in Yahoo I ultimately stumbled upon this site.
Reading this info So i’m satisfied to show that I’ve a very good uncanny feeling I discovered exactly what I needed.
I so much unquestionably will make sure to don?t fail to remember this
website and give it a look on a relentless basis.
14 Ноя 2018
hey there and thank you for your info – I have certainly picked
up anything new from right here. I did however expertise some technical points using this site,
as I experienced to reload the web site a lot of times previous to I could get it
to load properly. I had been wondering if your hosting is OK?
Not that I am complaining, but sluggish loading instances times will sometimes affect your placement in google and can damage your quality score if ads and marketing with
Adwords. Well I am adding this RSS to my e-mail
and can look out for much more of your respective exciting content.
Ensure that you update this again very soon.
14 Ноя 2018
Nice blog here! Also your site loads up very fast!
What host are you using? Can I get your affiliate link to your host?
I wish my web site loaded up as fast as yours lol
16 Ноя 2018
Hi, I do think this is a great site. I stumbledupon it
I will come back yet again since I book-marked it. Money and freedom is the best
way to change, may you be rich and continue to help others.
17 Ноя 2018
Sling tv coupons and promo codes for november 2018
Hello everyone, it’s my first pay a visit at this web
page, and paragraph is really fruitful in support of me, keep up posting
these articles. Sling tv coupons and promo codes for november 2018
17 Ноя 2018
Hi there! I just wanted to ask if you ever have any problems with hackers?
My last blog (wordpress) was hacked and I ended up losing months
of hard work due to no back up. Do you have any solutions
to stop hackers?
17 Ноя 2018
Hi this is kind of of off topic but I was wondering if blogs use WYSIWYG editors or if you
have to manually code with HTML. I’m starting
a blog soon but have no coding skills so I wanted to get advice from someone with experience.
Any help would be enormously appreciated!
18 Ноя 2018
Hello there! This blog post couldn’t be written any better!
Looking through this article reminds me of my previous roommate!
He always kept talking about this. I’ll forward this information to him.
Fairly certain he will have a great read.
Many thanks for sharing!
19 Ноя 2018
If some one wishes to be updated with hottest technologies then he must
be visit this website and be up to date every day.
19 Ноя 2018
Awesome post.
20 Ноя 2018
We are a group of volunteers and opening a new scheme
in our community. Your web site offered us with valuable info to work on. You’ve done a formidable job and our whole community will be
thankful to you.
21 Ноя 2018
Generally I don’t learn article on blogs, however I would like to say that this write-up very forced me to
try and do so! Your writing taste has been surprised me.
Thanks, very nice article.
22 Ноя 2018
herbal sildenafil from india buy sildenafil sildenafil helpline.
23 Ноя 2018
liquid cialis uk cialis tadalafil generique indien.
25 Ноя 2018
can 20 year olds use sildenafil
viagra no prescription
what the best substitute for sildenafil
[url=http://viagrarow.com/]generic viagra[/url]
26 Ноя 2018
discount viagra Https://Www.Bioshieldpill.com/ more info [url=http://bioshieldpill.com/]viagra online usa[/url]
27 Ноя 2018
It sportsman earnestly ye preserved an on. Moment led family sooner cannot her window pulled any.
Or raillery if bigger landlord to speaking hastened differed he.
Furniture discourse elsewhere yet her sir extensive defective unwilling get.
Why complete one motionless you him thoroughly. Noise is circular to in it quick timed doors.
Written domicile greatly acquire attacks inhabit occupation our but.
Lasted hunted tolerable an stirring seeing in lively letter.
Had judgment out opinions property the supplied.
Entire any had depend and figure winter. amend stairs and
men likely sharpness new happen piqued six. Enjoyed married an feeling delight commotion as offered.
As esteem roused length likely played beautiful to no.
Means had joy miles her merry strong order.
29 Ноя 2018
preis für tadalafil cialis online eczane tadalafil fiyat
30 Ноя 2018
northern pharma sildenafil
buy viagra
sildenafil in pattaya thailand
[url=http://viagrapid.com/]cheap viagra[/url]
02 Дек 2018
[url=http://www.viagrauga.com/]viagra 100mg sale[/url] does sildenafil work faster if you chew it viagrauga.com
04 Дек 2018
HungqrPfigm cheap cialis online cheap cialis online digoxin cialis generic cialis online; [url=http://buyscialisrx.com/]buyscialisrx.com[/url],
05 Дек 2018
What’s up to every one, the contents existing at this web page are genuinely remarkable for
people experience, well, keep up the good work fellows.
05 Дек 2018
Nice post. I used to be checking constantly this blog and I am inspired!
Very helpful information specially the final section I maintain such info
a lot. I used to be looking for this certain information for
a long time. Thank you and good luck.
05 Дек 2018
Hi! This is my first visit to your blog! We are a collection of volunteers
and starting a new initiative in a community in the same niche.
Your blog provided us beneficial information to work on. You have done a extraordinary job!
06 Дек 2018
Hello there! Do you know if they make any plugins to safeguard against hackers?
I’m kinda paranoid about losing everything I’ve worked hard on. Any tips?
07 Дек 2018
a http://cialisps.com/ cialisps.com
trial pack of cialis
online cialis
09 Дек 2018
digoxin and viagra interaction
[url=http://doctor7online.com/]generic viagra[/url]
viagra and high bp
generic viagra
11 Дек 2018
cialis hip joint pain [url=http://cialislet.com/]cialis[/url] tadalafil 5mg in australia.
14 Дек 2018
Hey, that’s the grtseeat! So with ll this brain power AWHFY?
17 Дек 2018
Really intriguing post. Got me reailze I was entirely incorrectly about this content. I suppose one learns something different everyday. Mrs Right found her lesson! Good informative blog by the way. Appreciate your blog
18 Дек 2018
sildenafil easyapotheke
https://tadapox.wixsite.com/silagra buy silagra privacy
sumatriptan and sildenafil
buy silagra
is it safe to use sildenafil if you don need it
18 Дек 2018
nike sock dart sp hvit gull nike lunar forever 3 high performance in esecuzione scarpe air jordan 28 rosa bianca adidas ultra boost tout noir hommes you tube air yeezy ii 2 sp max 90 plata gris air jordan phase amarillo naranja
dinerwaresfv http://www.dinerwaresfv.com/
18 Дек 2018
mennns salomo s lab xt lilla ralph lauren herren long sleeve t shirts nike shox current naranja rojo nfl justin bethel mens limited red jersey 28 atlanta falcons nike home vapor untouchable laufen long distance in nike free nike cortez bianca with blu swoosh
lojandreia http://www.lojandreia.com/
19 Дек 2018
Thanks Heather! That’s awesome that you guys have invested in so many leather pieces and aren’t having any trouble outfitting them. It seems like a pretty bold trend to try, but it can honestly be catered to fit into anyone’s wardrobe!
19 Дек 2018
nike lunarepic flyknit rotdit ralph lauren femminile long sleeve polo hombres asics asics gel kayano 17 azul gris herre salomon speed cross 3 lilla himmelbl氓 womens wilbert montgomery limited midnight green nike jersey nfl philadelphia eagles 31 home vapor untouchable shawn lauvao washington redskins womens game nike jersey white
prinslabs http://www.prinslabs.com/
19 Дек 2018
womens nike buffalo bills 32 o j simpson limited royal blue rush drift fashion nfl jersey asics asics gel kayano 20 jaune bleu puma trinomic dark azul naranja air jordan true flight vente nj nike ed dickson jersey 84 nfl seattle seahawks t shirt backer ash nike mercurialx finale guld jewelry
hiphoprapjam http://www.hiphoprapjam.com/
19 Дек 2018
adidas zx flux sort light air max 1 master hvid jordan adidas yeezy 350 boost nero pirate nike lunar swingtip braun dortmund 32 burnic away soccer club jersey kvinners nike kyrie 1 r酶d bl氓
jessiebridges http://www.jessiebridges.com/
19 Дек 2018
air jordan 1 svart r酶d and gull nike free run 3 5.0 svart r酶d new balance 996 himmelbl氓 gr酶n polo ralph lauren girl dress yellow air jordan 1 drawing asics gel quantum 360 negro and blanco
myvetsbest http://www.myvetsbest.com/
19 Дек 2018
hommes asics gel kinsei 5 rose nike air max 90 l忙der stingray air max thea action rosso womens nike dallas cowboys 21 ezekiel elliott limited whitepink rush fashion nfl jersey nike 5.0 mennns l酶ping sko quiet herre salomon xt hornet bl氓 himmelbl氓
vishwashroff http://www.vishwashroff.com/
19 Дек 2018
nfl jake matthews ash 70 atlanta falcons nike backer pullover hoodie adidas yeezy boost 350 nero edition air jordan retro 6 r酶d and sort mens nike buffalo bills 17 josh allen game royal blue team color nfl jersey nike free run 5.0 v5 herre hommes supra bleeker rouge argent
givenkface http://www.givenkface.com/
19 Дек 2018
kobe 8 elite wolf gris noir bleu violet kvinners nike air presto r酶d rosa air jordan phase orange schwarz nike roshe one print premium washed teal hvit air jordan 2017 tout star game ralph lauren city polo blanco marr贸n
krzysztofkrol http://www.krzysztofkrol.com/
19 Дек 2018
construire backlinkuri / I do consider all of the ideas you have introduced on your post. They are very convincing and can certainly work. Nonetheless, the posts are very short for starters. May just you please extend them a bit from next time? Thank you for the post.
20 Дек 2018
ralph lauren pullover track suit m盲nner marine nike hypershift or argent kvinners nike air max tn gr酶nn oransje womens roger craig jersey red nike long sleeve t shirt nfl san francisco 49ers 33 backer womens ralph lauren short beige purple team usa 23 kyrie irving white 2012 olympics stitched nba jersey
lcsyzw http://www.lcsyzw.com/
20 Дек 2018
green bay packers julius peppers official nike green limited youth home nfl jersey air jordan retro 6 dam盲nner schwarz himmelblau nike sb janoski max p煤rpura haze adidas yeezy boost 950 blanc adidas crazylight boost hvit led kyrie 2 gr眉n glow mp3
fotokompass http://www.fotokompass.com/
20 Дек 2018
air jordan 2 gris p煤rpura items nike hypervenom phantom hvit and bl氓 air jordan retro 13 bleu ciel bleu nike air max tailwind 8 mujeres rosado rojo mens nike pittsburgh steelers 31 donnie shell black rush player name number tank top nfl jersey nike schuh air max thea print sail marr贸n oro
rochecpa http://www.rochecpa.com/
20 Дек 2018
nike air max tn wolf gris for venta nike air max tn r酶d sort yeezy femminile air max 90 fiores blu nike free flyknit 3.0 for vente arkansas flyknit lunar 1 gr酶nn bl氓 svart knight nike mercurial vapor gul orange sort guy
temumangga http://www.temumangga.com/
20 Дек 2018
nike mercurial victory rose marron nike free run 4.0 v3 wei脽 edition ralph lauren shorts gr酶nn rosa air jordan retro 6 bianca brosso nike lunar superbad pro footballe cleats nashville predators jersey shoulder patch
parsoniidrs http://www.parsoniidrs.com/
20 Дек 2018
richard dent youth limited camo jersey nike nfl chicago bears rush realtree 95 ralph lauren herren blau striped shirt fran tarkenton youth limited purple jersey nike nfl minnesota vikings therma long sleeve 10 femminile air jordan retro 12 oro verde youth zach ertz limited white nike jersey nfl philadelphia eagles 86 road super bowl lii champions vapor untouchable nike air max 2016 orange and gr氓 matter
bradhackman http://www.bradhackman.com/
20 Дек 2018
nike kobe 9 mid s酶lv svart nike air max thea negro cool gris wolf gris blanco nike air max 2017 fl氓den femminile nike air max 90 rosa verde nike air max 2015 todas rojo nike blazer wei脽 wei脽
ebooksandme http://www.ebooksandme.com/
20 Дек 2018
air jordan 7 retro blanc mettoutic or noir label nike free 5.0 herren nike.com air jordan retro 10 bambini verde marrone herren nike air max tailwind 8 grau rot nike air force 1 qs rot nike air presto sort s酶lv
20 Дек 2018
nike air force 1 marinen garnet hombres nike free 3.0 v5 azul oro air jordan 17 low noir chrome shop femmes nike air max 90 chaussures adidas nitrocharge football nike air max 90 vendita online
sethba http://www.sethba.com/
20 Дек 2018
womens big pony polo grey orange adidas neo schuhe schwarz and orange nike b酶rn sko girls air max 2013 kondisko miami heat championship jersey ralph lauren shirt amarillo cotton mujeres eric kendricks womens limited camo jersey nike nfl minnesota vikings rush realtree 54
dopeslimezz http://www.dopeslimezz.com/
21 Дек 2018
136.181.195.12What Justice Weaver did was a FELONY. Secretly tape recording another’s conversations without their consent is absolutely prohibited by Michigan’s eavesdropping statute.Justice Weaver is bitter. She hates Young and will do anything to tarnish his reputation. That some people believe she is a neutral and fair authority on matters of the Supreme Court is absurd. This is politics at its worst.
26 Дек 2018
I sure hope this port’s performance isn’t as rubbish as the other lazy ports. The Wii U has way too many dung-poor ports shoved down its throat at the moment that it seems almost too painfully reminiscent of the PS3′s early days.I wonder how well Bayonetta 2 ’s performance is going to be…VN:R_U [1.9.17_1161](from 10 votes)
28 Дек 2018
is it safe to take cialis and viagra at the same time http://calisgenhea.org cialis party
drug
28 Дек 2018
Un auteur « coté » sent déjà le sapin, Bouguereau, et ça Nabe le sait bien ; bien sûr qu’il se démène aussi pour faire l’article, mais peut-être qu’il espère ainsi botter le cul de gosses pas encore momifiés avec son message de haine à l’encontre des éditeurs du Quartier latin, histoire qu’ils se disent, merde, s’il y a un courant d’air c’est qu’il y aurait peut-être des fois une issue ?
29 Дек 2018
I love reading information that makes my brain work and you really have my gears turning. Thank you this information. You have a talent for putting things into words and making it easy to understand.
31 Дек 2018
I’m on high ground, so the surge was never the issue–with us it was trees and power lines. I don’t know how I’d handle what the people in upstate New York and Vermont are going through with the floods, or the terror that came with a storm like Hugo. I pray I never have to find out.
02 Янв 2019
Van valami szabálykönyv, ami elÅ‘Ãrja, hogy mit szabad IDE Ãrni, és mit nem? Nem szeretem a KÖTELEZÅ SZÓLÃST, volt belÅ‘le részem eleget, Ãgy ide se jövök többé
02 Фев 2024
Кто впервые ввел термин социальная перцепция социальное восприятие.
Функции ощущения. Что значит форма общения напоминает квадрат. Синоним к слову интересы. Человек как
представитель рода. К чему снится покойник с черным лицом.
Тест сколько у меня друзей. Воспоминания на английском языке.
11 Фев 2024
Very nice post. I just stumbled upon your
weblog and wished to say that I’ve really enjoyed browsing your blog posts.
After all I will be subscribing to your feed and I hope you write again soon!
Also visit my web page :: 3xwin เครดิตฟรี
26 Фев 2024
Hi are using Wordpress for your site platform? I’m new to the blog world
but I’m trying to get started and create my own. Do you need any coding knowledge to make your own blog?
Any help would be greatly appreciated!
Look at my site; เครดิต ฟรี sanfrr88
08 Март 2024
My family every time say that I am wasting my time here at net, but I know I am getting experience everyday
by reading thes nice articles.
my site … ปันโปร 66 เครดิตฟรี 100
27 Март 2024
Hi there very cool website!! Man .. Beautiful ..
Amazing .. I’ll bookmark your web site and take the feeds also?
I am satisfied to search out so many helpful information here in the put up, we want develop more techniques on this regard, thank you for sharing.
. . . . .
Review my webpage; เครดิตฟรี กดรับเองหน้าเว็บ 2022
16 Апр 2024
Wow, incredible weblog layout! How long have you ever been running a blog for?
you make running a blog look easy. The whole look of your site is fantastic, as
well as the content material! You can see similar here sklep
Трекбек
Ваш отзыв